· 2 min read · Docker, Laravel, PHP, DevOps
Dockerising a Laravel app with Docker Compose
A practical Docker Compose setup for Laravel: PHP-FPM, Nginx, MySQL and a queue worker in separate containers, with persistent data, environment files and a production checklist.
"It works on my machine" stops being a problem once the machine is part of the project. Docker Compose lets you describe a Laravel app's whole environment, the PHP version, the web server, the database and the background workers, in one file that runs the same on a laptop and on a server.
This is the setup I start from. Each part of the app gets its own container, so each one can be updated, restarted or scaled without touching the others.
The four services
- app: PHP-FPM running your Laravel code.
- web: Nginx, serving static files and passing PHP requests to
app. - db: MySQL, with its data on a named volume so it survives rebuilds.
- queue: the same image as
app, runningphp artisan queue:work.
The Dockerfile
FROM php:8.3-fpm-alpine
RUN apk add --no-cache icu-dev libzip-dev \
&& docker-php-ext-install pdo_mysql intl zip opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction
COPY . .
RUN composer dump-autoload --optimize \
&& chown -R www-data:www-data storage bootstrap/cache
USER www-dataCopying composer.json and composer.lock before the rest of the code means Docker can reuse the dependency layer. Changing a controller doesn't trigger a full composer install, so rebuilds take seconds.
docker-compose.yml
services:
app:
build: .
env_file: .env
depends_on:
db:
condition: service_healthy
volumes:
- storage:/var/www/html/storage
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./public:/var/www/html/public:ro
depends_on:
- app
queue:
build: .
env_file: .env
command: php artisan queue:work --tries=3 --max-time=3600
restart: unless-stopped
depends_on:
- db
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- mysql:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
retries: 10
volumes:
mysql:
storage:Inside the Compose network, containers reach each other by service name. In Laravel's .env, DB_HOST is simply db, not 127.0.0.1.
The Nginx config
server {
listen 80;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME /var/www/html/public$fastcgi_script_name;
include fastcgi_params;
}
}Everyday commands
docker compose up -d --build # start everything
docker compose exec app php artisan migrate
docker compose logs -f queue # watch the worker
docker compose restart queue # pick up new code in the workerQueue workers keep the app in memory, so they don't see new code until they restart. Add docker compose restart queue (or php artisan queue:restart) to every deploy.
Before it goes to production
- Set
APP_ENV=productionandAPP_DEBUG=false, and runphp artisan config:cacheandroute:cacheon start-up. - Don't publish the MySQL port. Only Nginx needs to be reachable from outside.
- Put a reverse proxy with HTTPS in front, such as Caddy or Traefik, or terminate TLS in Nginx.
- Back up the
mysqlvolume on a schedule withmysqldump, and test a restore. - Add a
schedulerservice runningphp artisan schedule:workif the app has scheduled tasks.
For moving existing sites onto a server like this, I've written up the full process in moving from shared hosting to a Dockerised VPS.
Checklist
- One container per job: PHP-FPM, Nginx, MySQL, queue worker
- Copy Composer files first so dependency layers are cached
- Named volumes for the database and storage
- A health check so the app waits for MySQL
- Restart queue workers on every deploy
- Keep the database port private and back it up
Want a Laravel app containerised or moved to a VPS? Tell me about it.