back to blog

Deploying Laravel Applications with Kamal: A Real-World Example

What is Kamal?

Kamal is an deployment tool created by the team behind Ruby on Rails that simplifies Docker-based deployments. It’s designed to make deployment as simple as possible while still providing the flexibility and power of Docker. While it was originally created for Rails applications, it works excellently with Laravel and other PHP applications.

Why Choose Kamal?

Before diving into the implementation, let’s discuss why you might want to consider Kamal for your Laravel deployments:

  1. Simple Configuration: Kamal uses a single YAML file for most configuration
  2. Zero-downtime Deployments: Built-in support for rolling deployments
  3. Docker-based: Leverages Docker’s containerization benefits
  4. Built-in Health Checks: Automatic monitoring of your application’s health
  5. SSL Support: Easy SSL configuration with Let’s Encrypt
  6. Multi-server Support: Can deploy to multiple servers with ease

Real-World Example: Deploying Ticketi

Let me walk you through how I deployed Ticketi, a Laravel-based ticketing system, using Kamal. This will serve as a practical example of Kamal in action.

Prerequisites

Before we begin, make sure you have:

  • Docker installed on both your local and production machines
  • SSH access to your production server
  • Git installed
  • Ruby installed (for Kamal)
  • A GitHub account (we’ll be using GitHub Container Registry)

Step 1: Setting Up the Dockerfile

First, let’s look at our Dockerfile. We’re using a multi-stage build to optimize our image size and build process:

FROM serversideup/php:8.3-fpm-nginx-alpine AS base

ENV AUTORUN_ENABLED=1
ENV PHP_OPCACHE_ENABLE=1
ENV SSL_MODE="off"

# PHP extensions and dependencies
RUN apk add --update busybox-suid python3 make g++ sqlite sqlite-dev && \
    install-php-extensions pdo_sqlite bcmath gd exif

This base image from ServerSideUp comes with PHP-FPM and Nginx pre-configured, saving us significant setup time. We’re using Alpine Linux for a smaller image size, and we’re only installing the PHP extensions we actually need.

Step 2: Configuring Kamal

The heart of our deployment configuration lives in config/deploy.yml. Here’s our production setup:

service: kamal
image: ghcr.io/devalade/ticketi

servers:
  web:
    - ticketi.devalade.me
  cron:
    hosts:
      - ticketi.devalade.me
    cmd: php artisan schedule:work
    options:
      health-cmd: healthcheck-schedule

proxy:
  ssl: true
  host: ticketi.devalade.me
  app_port: 8080

Let’s break down what’s happening here:

  1. Service Configuration: We’re defining our service name and Docker image location
  2. Server Setup: We’re configuring both web and cron servers
  3. SSL: We’re enabling SSL for secure connections
  4. Health Checks: We’ve added a health check for our scheduled tasks

Step 3: Environment and Storage Configuration

One of the key aspects of any deployment is managing environment variables and persistent storage. Here’s how we’ve configured ours:

env:
  clear:
    APP_NAME: "Ticketi"
    APP_ENV: "production"
    APP_DEBUG: false
    APP_URL: "https://ticketi.devalade.me"
    DB_CONNECTION: "sqlite"
    DB_DATABASE: "/var/www/html/data/database.sqlite"
    STORAGE_MEDIA_DISK_PATH: "/app-media/"

For persistent storage, we’ve configured two volumes:

volumes:
  - "data:/var/www/html/data"
  - "/storage:/app-media/"

This ensures our database and media files persist between deployments.

Step 4: Setting Up Redis for Caching

We’ve also included Redis for improved performance:

accessories:
  redis:
    image: valkey/valkey:8
    host: ticketi.devalade.me
    directories:
      - redis-data:/data

Step 5: Deployment Process

With everything configured, deployment is surprisingly simple:

  1. Initial Setup:

    kamal setup
  2. Deploy:

    kamal deploy

Kamal handles everything else - pulling the image, setting up containers, configuring the proxy, and ensuring zero-downtime deployment.

Useful Commands for Management

Here are some commands I frequently use for managing the deployment:

# View logs
kamal logs

# Access the shell
kamal shell

# Access Laravel Tinker
kamal tinker

# Restart services
kamal restart

Common Challenges and Solutions

During our deployment, we encountered and solved several common issues:

  1. Database Permissions: Ensure your data directory has the correct permissions (775) and is owned by www-data
  2. Asset Building: Make sure your assets are built during the Docker build process
  3. Redis Connection: If Redis isn’t connecting, verify the accessory service is running

Security Best Practices

We’ve implemented several security measures:

  1. Environment Variables: Sensitive data is stored in secret environment variables
  2. SSL: Enabled by default for all traffic
  3. File Permissions: Proper user and group settings for all directories
  4. Container Security: Running as non-root user (www-data)

Conclusion

Kamal has proven to be an excellent choice for deploying our Laravel application. It provides a perfect balance between simplicity and flexibility, making it suitable for both small and large applications. While it might have a slight learning curve if you’re new to Docker, the benefits of having a robust, scalable deployment solution make it worth the investment.

The complete setup we’ve discussed here is running in production for Ticketi, handling our needs efficiently. Whether you’re deploying your first Laravel application or looking to improve your existing deployment process, Kamal is definitely worth considering.

Resources for Further Learning


Have you tried Kamal for your Laravel deployments? I’d love to hear about your experiences in the comments below!