Back

Deploying Laravel with Docker and Caddy

Containerize your Laravel application with Docker and serve it with Caddy with automatic or custom SSL certificates

Deploying Laravel with Docker and Caddy

Docker provides consistent environments across development and production, while Caddy offers automatic HTTPS with zero configuration. This guide walks through containerizing a Laravel application and deploying it with Caddy as a reverse proxy.


Prerequisites

  • Ubuntu server with root access
  • Domain name pointing to your server’s IP
  • Docker and Docker Compose installed

Install Docker

If Docker isn’t installed, run:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

Log out and back in for group changes to take effect.

Verify installation:

docker --version
docker compose version

Project Structure

Create the following structure for your Laravel project:

laravel-app/
├── docker/
│   ├── php/
│   │   └── Dockerfile
│   └── caddy/
│       └── Caddyfile
├── src/
│   └── (Laravel application files)
├── docker-compose.yml
└── .env

PHP Dockerfile

Create docker/php/Dockerfile:

FROM php:8.3-fpm-alpine

# Install system dependencies
RUN apk add --no-cache \
    git \
    curl \
    libpng-dev \
    libxml2-dev \
    zip \
    unzip \
    oniguruma-dev \
    freetype-dev \
    libjpeg-turbo-dev \
    libzip-dev \
    icu-dev \
    postgresql-dev

# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
        pdo_pgsql \
        mbstring \
        exif \
        pcntl \
        bcmath \
        gd \
        xml \
        zip \
        intl \
        opcache

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www/html

# Copy application
COPY src/ /var/www/html/

# Install dependencies
RUN composer install --optimize-autoloader --no-dev --no-interaction

# Set permissions
RUN chown -R www-data:www-data /var/www/html \
    && chmod -R 755 /var/www/html/storage \
    && chmod -R 755 /var/www/html/bootstrap/cache

# PHP configuration for production
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# OPcache configuration
RUN echo "opcache.enable=1" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
    && echo "opcache.memory_consumption=128" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
    && echo "opcache.interned_strings_buffer=8" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
    && echo "opcache.max_accelerated_files=10000" >> "$PHP_INI_DIR/conf.d/opcache.ini" \
    && echo "opcache.validate_timestamps=0" >> "$PHP_INI_DIR/conf.d/opcache.ini"

USER www-data

EXPOSE 9000

CMD ["php-fpm"]

Caddyfile

Create docker/caddy/Caddyfile:

{
    email your@email.com
}

yourdomain.com {
    root * /var/www/html/public

    encode gzip

    php_fastcgi php:9000

    file_server

    # Handle Laravel routes
    @notStatic {
        not path /build/* /favicon.ico /robots.txt
        file {
            try_files {path} {path}/ /index.php?{query}
        }
    }
    rewrite @notStatic /index.php?{query}

    # Security headers
    header {
        X-Frame-Options "SAMEORIGIN"
        X-Content-Type-Options "nosniff"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # Deny access to hidden files
    @hidden path */.*
    respond @hidden 404

    log {
        output file /var/log/caddy/access.log
        format json
    }
}

Caddy automatically obtains and renews SSL certificates from Let’s Encrypt.


Docker Compose

Create docker-compose.yml:

services:
  php:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel-php
    restart: unless-stopped
    volumes:
      - ./src:/var/www/html
      - ./docker/php/php-local.ini:/usr/local/etc/php/conf.d/local.ini:ro
    networks:
      - laravel
    depends_on:
      - postgres
      - redis

  caddy:
    image: caddy:2-alpine
    container_name: laravel-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./src:/var/www/html:ro
      - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
      - caddy_logs:/var/log/caddy
    networks:
      - laravel
    depends_on:
      - php

  postgres:
    image: postgres:16-alpine
    container_name: laravel-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${DB_DATABASE}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - laravel
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
      interval: 10s
      timeout: 5s
      retries: 3

  redis:
    image: redis:alpine
    container_name: laravel-redis
    restart: unless-stopped
    volumes:
      - redis_data:/data
    networks:
      - laravel
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  queue:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel-queue
    restart: unless-stopped
    command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600
    volumes:
      - ./src:/var/www/html
    networks:
      - laravel
    depends_on:
      - postgres
      - redis

  scheduler:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel-scheduler
    restart: unless-stopped
    command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done"
    volumes:
      - ./src:/var/www/html
    networks:
      - laravel
    depends_on:
      - postgres
      - redis

networks:
  laravel:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
  caddy_data:
  caddy_config:
  caddy_logs:

Environment Configuration

Create .env in the project root:

DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret_password

Update your Laravel .env in the src/ directory:

APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com

DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret_password

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis

REDIS_HOST=redis
REDIS_PORT=6379

Build and Deploy

Initial Setup

cd /path/to/laravel-app

# Build containers
docker compose build

# Start services
docker compose up -d

# Generate application key
docker compose exec php php artisan key:generate

# Run migrations
docker compose exec php php artisan migrate --force

# Optimize for production
docker compose exec php php artisan config:cache
docker compose exec php php artisan route:cache
docker compose exec php php artisan view:cache

Verify Services

Check all containers are running:

docker compose ps

View logs:

docker compose logs -f

Development Configuration

For local development, create docker-compose.override.yml:

services:
  php:
    volumes:
      - ./src:/var/www/html

  caddy:
    volumes:
      - ./docker/caddy/Caddyfile.dev:/etc/caddy/Caddyfile:ro

Create docker/caddy/Caddyfile.dev for local development:

{
    auto_https off
}

:80 {
    root * /var/www/html/public

    encode gzip

    php_fastcgi php:9000

    file_server

    @notStatic {
        not path /build/* /favicon.ico /robots.txt
        file {
            try_files {path} {path}/ /index.php?{query}
        }
    }
    rewrite @notStatic /index.php?{query}
}

Useful Commands

Container Management

# Start services
docker compose up -d

# Stop services
docker compose down

# Rebuild containers
docker compose build --no-cache

# View logs
docker compose logs -f php
docker compose logs -f caddy

# Shell into PHP container
docker compose exec php sh

Laravel Commands

# Run artisan commands
docker compose exec php php artisan migrate
docker compose exec php php artisan tinker
docker compose exec php php artisan queue:restart

# Clear caches
docker compose exec php php artisan cache:clear
docker compose exec php php artisan config:clear

# Install new dependencies
docker compose exec php composer require package/name

Database Operations

# Access PostgreSQL CLI
docker compose exec postgres psql -U laravel -d laravel

# Database backup
docker compose exec postgres pg_dump -U laravel laravel > backup.sql

# Restore database
docker compose exec -T postgres psql -U laravel -d laravel < backup.sql

SSL and Domain Configuration

Caddy handles SSL automatically. Ensure:

  1. Your domain’s DNS A record points to your server’s IP
  2. Ports 80 and 443 are open in your firewall
  3. The email in Caddyfile is valid for Let’s Encrypt notifications
# Open firewall ports (if using UFW)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Caddy will automatically:

  • Obtain SSL certificates from Let’s Encrypt
  • Redirect HTTP to HTTPS
  • Renew certificates before expiry

Updating the Application

Deploy updates with zero downtime:

cd /path/to/laravel-app

# Pull latest changes
git pull origin main

# Rebuild PHP container
docker compose build php

# Restart with new image
docker compose up -d --no-deps php queue scheduler

# Run migrations
docker compose exec php php artisan migrate --force

# Clear and rebuild caches
docker compose exec php php artisan config:cache
docker compose exec php php artisan route:cache
docker compose exec php php artisan view:cache
docker compose exec php php artisan queue:restart

Troubleshooting

Check Container Status

docker compose ps
docker compose logs php
docker compose logs caddy

Permission Issues

docker compose exec php chown -R www-data:www-data /var/www/html/storage
docker compose exec php chmod -R 775 /var/www/html/storage

SSL Certificate Issues

# Check Caddy logs
docker compose logs caddy

# Force certificate renewal
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

Container Won’t Start

# Check for port conflicts
sudo lsof -i :80
sudo lsof -i :443

# Rebuild from scratch
docker compose down -v
docker compose build --no-cache
docker compose up -d

Database Connection Issues

Ensure PostgreSQL container is healthy:

docker compose exec postgres pg_isready -U laravel

Production Checklist

Before going live, verify:

  • APP_DEBUG=false in Laravel .env
  • APP_ENV=production in Laravel .env
  • Strong database passwords set
  • Valid email in Caddyfile for SSL notifications
  • Domain DNS properly configured
  • Firewall allows ports 80 and 443
  • Laravel caches are built (config:cache, route:cache, view:cache)
  • Queue worker is running
  • Scheduler is running
  • Backups are configured

Your Laravel application is now running in Docker containers with Caddy providing automatic HTTPS.

Kuala Lumpur, Malaysia.
© Copyright 2026. All rights reserved.