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 --chown=www-data:www-data . /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.