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:
- Your domain’s DNS A record points to your server’s IP
- Ports 80 and 443 are open in your firewall
- 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=falsein Laravel.env -
APP_ENV=productionin 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.