Deploy a Laravel app with Reverb Workers and Kamal
For my current startup project, I wanted to try out Laravel with Kamal. This turned into quite a challenge because I needed real-time communication with Reverb and Job Queues, but couldn't find any resources online about doing this with Kamal. After figuring it out, I want to share this guide for others (and my future self).
Prepare your Laravel App
Add Trusted Proxies middleware
Kamal handles SSL for you, but your Laravel app will only receive HTTP
traffic. This causes issues with all your route
helpers since they'll generate http
URLs. Clicking these links will result in an error like:
Mixed Content: The page was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint. This request has been blocked; the content must be served over HTTPS.
To fix this, we can use Laravel's Trusted Proxies middleware to trust the Kamal proxy. Add the following middleware to your application's bootstrap/app.php
:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
See the Laravel documentation for more details.
Containerize your Laravel App
Based on a recommendation from a Laravel employee on Reddit, I used the production-ready Docker images from serversideup.net.
Here's my Dockerfile that works with Inertia:
FROM serversideup/php:8.3-fpm-nginx AS base
# We need to temporarily switch to the root user to perform admin-level tasks
USER root
# Add support for handling image metadata (EXIF) in PHP
RUN install-php-extensions exif
# Set up JavaScript tools we'll need
ARG NODE_VERSION=20.18.0
ENV PATH=/usr/local/node/bin:$PATH
RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \
/tmp/node-build-master/bin/node-build "${NODE_VERSION}" /usr/local/node && \
# Enable package management tools (npm will be set up based on our project needs)
corepack enable && \
rm -rf /tmp/node-build-master
# Switch back to a regular user for security
USER www-data
FROM base
# Configuration settings for our web server and PHP
# They come from: https://serversideup.net/open-source/docker-php/docs/reference/
# Tell NGINX not to use SSL (HTTPS) since Kamal-proxy handles that separately
ENV SSL_MODE="off"
# See: https://serversideup.net/open-source/docker-php/docs/laravel/laravel-automations
ENV AUTORUN_ENABLED="true"
# Turn on PHP's code caching to make the app faster
ENV PHP_OPCACHE_ENABLE="1"
# Set up a health check URL to monitor if the app is running
ENV HEALTHCHECK_PATH="/up"
# Copy our application files into the container and set proper ownership
COPY . /var/www/html
# Install PHP dependencies and optimize them for better performance
RUN composer install --no-interaction --prefer-dist --optimize-autoloader
# Install JavaScript dependencies and build the frontend assets for production
# Then clean up unnecessary files to keep the image size small
RUN npm install --immutable && \
npm run build && \
rm -rf node_modules
Kamal
Add a secret for the app key
Inside .kamal/secrets
add APP_KEY=$APP_KEY
so it will be loaded from your .env file.
Adjust your Kamal Config
First, ensure you have Kamal installed on your machine and have run kamal init
. If not, check the Kamal installation guide before continuing.
For using Reverb and Workers with Docker, it's recommended to run them in separate containers with the same image. See the documentation for Laravel Reverb and Laravel Queue.
# This line loads secret settings from a .env file without exposing them publicly
<% require "dotenv"; Dotenv.load(".env") %>
# Basic settings for your application
service: jango-fleet-dispatch
image: jango-fleet/dispatch
# List of servers and their roles
servers:
# Main web server configuration
web:
- 142.132.228.232
# Background task processor (queue) server configuration
queue:
hosts:
- 142.132.228.232
cmd: php /var/www/html/artisan queue:work --tries=3
options:
health-cmd: healthcheck-queue
env:
clear:
APP_NAME: "Queue"
APP_ENV: "production"
APP_DEBUG: false
APP_URL: "http://localhost"
ASSET_URL: "http://localhost"
DB_CONNECTION: "sqlite"
DB_DATABASE: "/var/www/html/database/database.sqlite"
SESSION_DRIVER: "database"
CACHE_STORE: "database"
BROADCAST_CONNECTION: reverb
BROADCAST_DRIVER: reverb
QUEUE_CONNECTION: database
REVERB_APP_ID: "439873"
REVERB_APP_KEY: "*****"
REVERB_APP_SECRET: "*****"
REVERB_HOST: "reverb.example.com"
REVERB_PORT: "443"
REVERB_SCHEME: "https"
VITE_REVERB_APP_KEY: "*****"
VITE_REVERB_HOST: "reverb.example.com"
VITE_REVERB_PORT: "443"
VITE_REVERB_SCHEME: "https"
# Real-time communication (websocket) server configuration
reverb:
hosts:
- 142.132.228.232
cmd: php /var/www/html/artisan --port=8000 reverb:start --host=0.0.0.0
options:
health-cmd: healthcheck-reverb
proxy:
ssl: true
host: reverb.example.com
app_port: 8000
env:
clear:
APP_NAME: "Reverb"
APP_ENV: "production"
APP_DEBUG: false
APP_URL: "https://reverb.example.com"
ASSET_URL: "https://reverb.example.com"
DB_CONNECTION: "sqlite"
DB_DATABASE: "/var/www/html/database/database.sqlite"
MAIL_MAILER: "log"
SESSION_DRIVER: "database"
CACHE_STORE: "database"
BROADCAST_CONNECTION: reverb
BROADCAST_DRIVER: reverb
REVERB_APP_ID: "*****"
REVERB_APP_KEY: "*****"
REVERB_APP_SECRET: "*****"
REVERB_HOST: "reverb.example.com"
REVERB_PORT: "443"
REVERB_SCHEME: "https"
VITE_REVERB_APP_KEY: "*****"
VITE_REVERB_HOST: "reverb.example.com"
VITE_REVERB_PORT: "443"
VITE_REVERB_SCHEME: "https"
# Settings for secure HTTPS connections
proxy:
ssl: true
host: example.com
app_port: 8080
# Connection details for where your Docker images are stored
registry:
server: ****
username: ****
password: ****
# Settings for building the Docker image
builder:
arch: arm64
# Environment variables that will be available to your application
env:
clear:
# Basic Application Settings
APP_NAME: "Laravel blog" # The name of your application
APP_ENV: "production" # Tells Laravel this is a live production environment
APP_DEBUG: false # Disables detailed error messages for security
APP_URL: "https://example.com" # Your website's main address
ASSET_URL: "https://example.com" # Where to find your CSS, JS, and images
# Database Configuration
DB_CONNECTION: "sqlite" # Using SQLite as the database type
DB_DATABASE: "/var/www/html/database/database.sqlite" # Where the database file is stored
# System Services Configuration
MAIL_MAILER: "log" # Store emails in log files instead of sending
SESSION_DRIVER: "database" # Store user sessions in the database
CACHE_STORE: "database" # Store cached data in the database
QUEUE_CONNECTION: database # Use database for background tasks
# Real-time Communication Settings
BROADCAST_CONNECTION: reverb # Use Reverb for real-time features
BROADCAST_DRIVER: reverb # The system handling real-time messages
# Reverb Server Configuration (for real-time features)
REVERB_APP_ID: "439873" # Unique identifier for your Reverb app
REVERB_APP_KEY: "[REDACTED]" # Public key for Reverb authentication
REVERB_APP_SECRET: "[REDACTED]" # Keep this secret! Used for secure communication
REVERB_HOST: "reverb.example.com" # Where your Reverb server runs
REVERB_PORT: "443" # Standard HTTPS port
REVERB_SCHEME: "https" # Use secure HTTPS connection
# Frontend Configuration for Reverb
VITE_REVERB_APP_KEY: "[REDACTED]" # Public key for frontend use
VITE_REVERB_HOST: "reverb.example.com" # Reverb server address for frontend
VITE_REVERB_PORT: "443" # Port for frontend connections
VITE_REVERB_SCHEME: "https" # Security protocol for frontend
# Secrets that should never be exposed in code
secret:
- APP_KEY # Laravel's encryption key (stored separately)
# Persistent storage configuration
volumes:
# Store database files permanently, even when containers restart
- "data:/var/www/html/database"
# Shortcut commands for common tasks
aliases:
# Open an interactive terminal in the container
console: app exec --reuse -i "bash"
# Start Laravel's interactive debugging tool
tinker: app exec --reuse -i "php artisan tinker"
# View real-time logs from the websocket server
reverb-logs: app exec -r reverb "tail -f storage/logs/laravel.log"
And that's it! I hope this helps you deploy your Laravel app with Kamal.