Deploy Laravel with Docker
A complete walkthrough of containerizing a Laravel app — from Dockerfile to a production-ready docker-compose stack with Nginx and MySQL.
Every Laravel project eventually hits the same wall: it works perfectly on your machine, but deploying it anywhere else means fighting PHP versions, missing extensions, and a server config nobody remembers writing. Docker removes that entire category of problem by packaging the app together with everything it needs to run.
Why containerize a Laravel app
A container bundles your code, the PHP runtime, and system dependencies into one portable unit. Once it works locally, the same image runs identically in staging and production — no more "it works on my machine."
The goal isn't just running Laravel in a container. It's making the container the single source of truth for how the app runs, everywhere.
The base Dockerfile
Start with a slim PHP-FPM image and layer in only the extensions Laravel actually needs:
FROM php:8.3-fpm-alpine
RUN apk add --no-cache \
libpng-dev libzip-dev oniguruma-dev \
&& docker-php-ext-install pdo_mysql mbstring zip gd
WORKDIR /var/www
COPY . .
RUN composer install --optimize-autoloader --no-dev
Keep the image lean
Skip anything you don't need at runtime — dev dependencies, test fixtures, and build tools all belong in a separate build stage, not the final image.
Compose: Nginx, PHP-FPM, and MySQL
A single container rarely ships alone. docker-compose.yml ties the app, web server, and database together as one stack:
services:
app:
build: .
volumes:
- ./:/var/www
nginx:
image: nginx:alpine
ports:
- "80:80"
depends_on:
- app
db:
image: mariadb:11
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
A few lessons from production
- Run
php artisan config:cacheduring the image build, not at container start — it shaves real time off cold boots. - Mount storage and cache as named volumes so redeploys don't wipe uploaded files.
- Keep secrets out of the image entirely; inject them as environment variables at runtime.
None of this is exotic — it's the same handful of decisions, made deliberately, that turn "it runs in Docker" into "it deploys the same way every time."