Docker has become an essential tool for modern PHP developers. It eliminates the classic "works on my machine" problem and creates consistent environments from development to production.
In this guide, I'll take you from zero Docker knowledge to running a full PHP application in production containers.
Why Docker for PHP?
Without Docker, setting up a PHP environment means installing PHP, a web server, database server, extensions, and tools individually. Docker solves this:
- Consistent environments — Everyone runs the exact same stack
- Easy onboarding — New developers run
docker compose upand they're ready - Isolation — Projects don't conflict with each other
- Production parity — What runs locally runs the same in production
Your First PHP Dockerfile
FROM php:8.3-fpm-alpine
RUN apk add --no-cache \
git curl zip unzip libpng-dev libjpeg-turbo-dev \
freetype-dev icu-dev oniguruma-dev libxml2-dev
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install pdo pdo_mysql mbstring exif pcntl bcmath gd intl opcache
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN chown -R www-data:www-data storage bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
Use Alpine-based images when possible. They are significantly smaller (50MB vs 400MB+) and have a smaller attack surface.
Docker Compose for Local Development
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/var/www/html
depends_on:
- mysql
- redis
nginx:
image: nginx:alpine
ports:
- '8080:80'
volumes:
- .:/var/www/html
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: laravel
MYSQL_ROOT_PASSWORD: secret
volumes:
- mysql-data:/var/lib/mysql
ports:
- '3306:3306'
redis:
image: redis:alpine
ports:
- '6379:6379'
volumes:
mysql-data:
Nginx Configuration
server {
listen 80;
server_name localhost;
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 $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
Multi-Stage Builds for Production
Multi-stage builds let you use separate stages for building and running your app. The final image only contains what is needed:
# Stage 1: Install PHP dependencies
FROM composer:latest AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
# Stage 2: Build frontend assets
FROM node:18-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources/ ./resources/
COPY vite.config.js ./
RUN npm run build
# Stage 3: Final production image
FROM php:8.3-fpm-alpine
RUN docker-php-ext-install pdo pdo_mysql mbstring gd intl opcache
WORKDIR /var/www/html
COPY . .
COPY --from=vendor /app/vendor ./vendor
COPY --from=frontend /app/public/build ./public/build
RUN chown -R www-data:www-data storage bootstrap/cache
EXPOSE 9000
CMD ["php-fpm"]
Multi-stage builds can reduce your final image size by 60-80%. Build dependencies like Composer and Node.js are not included in the production image.
CI/CD with Docker
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install
- run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -f Dockerfile.production -t my-app:latest .
- run: docker push my-app:latest
Conclusion
Docker is an essential skill for modern PHP development. It eliminates environment inconsistencies, simplifies onboarding, and creates a clear path from development to production.
Start with the basics — a simple Dockerfile and docker-compose.yml. Get comfortable running your daily workflow inside containers. Then gradually adopt multi-stage builds, health checks, and CI/CD integration.
The investment pays off quickly: fewer "works on my machine" issues, faster onboarding, and confidence that what you test locally is what runs in production.
Comments