PHP and Docker: Practical Setup Guide
Hey, fellow developers. Picture this: it's 2 AM, your local setup's a mess of conflicting PHP versions, and that one extension just won't install without breaking everything else. Sound familiar? I've been there—staring at a terminal, coffee gone cold, wondering why development has to feel like wrestling an octopus. Then Docker entered the picture. Suddenly, environments became portable, reproducible, and blissfully consistent. No more "it works on my machine."
Today, we're diving into PHP and Docker—a match made in dev heaven. Whether you're hiring specialists on Find PHP, hunting for your next gig, or just keeping up with the ecosystem, containerizing your PHP apps is non-negotiable in 2026. It's about speed, reliability, and sanity. We'll build a practical setup from scratch: Dockerfiles, Compose files, performance tweaks, the works. By the end, you'll have a stack ready for local dev, staging, or prod. Let's roll up our sleeves.
Why Docker changes everything for PHP devs
Remember when deploying meant SSH-ing into a server, apt-get-ing packages, and crossing fingers? Docker flips that script. Containers bundle your app with its exact dependencies—PHP 8.3, Composer, extensions like PDO MySQL—all isolated and lightweight.
I've lost count of projects where Docker saved the day. One time, our team had a Laravel app choking on M1 Macs but flying on Linux servers. Docker? Boom—uniform environment, zero drama. For hiring managers on platforms like Find PHP, it's gold: candidates deliver consistent code, no setup excuses.
Key wins:
- Portability: Same container everywhere—from laptop to Kubernetes.
- Isolation: No version hell. PHP 8.2 for one project, 8.1 for legacy.
- Speed: Builds once, runs anywhere. Scale with Compose or Swarm.
- Ecosystem fit: Works seamlessly with Composer, Laravel, Symfony, WordPress.
But here's the rub—bad Dockerfiles bloat images, leak secrets, or crash under load. We're fixing that today with real, copy-paste-ready examples.
Your first Dockerfile: From zero to hero
Start simple. Assume a basic PHP app with Composer deps and a MySQL connection. We'll use multi-stage builds for efficiency—prod deps separate from dev, caching galore.
Create this Dockerfile in your project root:
# syntax=docker/dockerfile:1
# Stage 1: Production dependencies
FROM composer:lts AS prod-deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-dev --no-interaction --optimize-autoloader
# Stage 2: Development dependencies
FROM composer:lts AS dev-deps
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-interaction
# Stage 3: Base PHP image
FROM php:8.3-fpm-alpine AS base
RUN apk add --no-cache nginx supervisor
RUN docker-php-ext-install pdo pdo_mysql
RUN apk add --no-cache $PHPIZE_DEPS && pecl install redis && docker-php-ext-enable redis
# Stage 4: Development
FROM base AS development
COPY --from=dev-deps /app/vendor ./vendor
COPY . .
COPY docker/php.ini-development $PHP_INI_DIR/php.ini
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
EXPOSE 80
# Stage 5: Production
FROM base AS production
COPY --from=prod-deps /app/vendor ./vendor
COPY . .
COPY docker/php.ini-production $PHP_INI_DIR/php.ini
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
USER www-data
EXPOSE 80
Why this rocks? Multi-stage slashes image size—Alpine base is tiny, Composer caching speeds rebuilds. Nginx + PHP-FPM beats Apache for perf. Supervisor glues them together in one container (practical for small apps; scale to separate for big ones).
Pro tip: I learned the hard way—always pin images like php:8.3-fpm-alpine. No surprises.
Wiring it up with Docker Compose
Solo containers? Fine for toys. Real apps need databases, Redis, queues. Enter docker-compose.yml. This one's battle-tested for a Laravel-ish stack.
version: '3.8'
services:
app:
build:
context: .
target: ${ENV:-development}
ports:
- "8000:80"
volumes:
- .:/app
environment:
- DB_HOST=db
- REDIS_HOST=redis
depends_on:
- db
- redis
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: appdb
MYSQL_USER: appuser
MYSQL_PASSWORD: apppass
volumes:
- db_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
db_data:
Run with ENV=production docker compose up -d for prod-like, or plain for dev. Volumes keep DB data persistent. Healthchecks? Add 'em:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health.php"]
interval: 30s
retries: 3
I've deployed this to staging in minutes. Tweak target: development in Compose for hot-reloading volumes—edit code, refresh browser, magic.
Performance tuning: Make it scream
Containers aren't free. Untuned PHP apps guzzle RAM, spike CPU. Here's where pros shine—and why experienced PHP devs on Find PHP command premium rates.
First, OPcache. Non-negotiable. In php.ini-production:
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.validate_timestamps=0 ; Prod only—no file changes
Boom—50-70% faster execution. I once shaved 300ms off a Laravel endpoint. Felt like cheating.
PHP-FPM pool (docker/php-fpm.conf):
[www]
pm = static
pm.max_children = 5 ; Tune to your RAM: ~256M per child
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
Static PM predicts load perfectly in containers. Dynamic? Overhead city.
Nginx conf snippet for static files:
location ~* \.(css|js|jpg|png|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Serve assets direct—no PHP touch. My throughput doubled.
Security: Lock it down
Exposed ports? Weak users? Rookie mistakes. Drop capabilities:
docker run --cap-drop=ALL --cap-add=CHOWN --cap-add=SETGID yourimage
Run as non-root: USER www-data in Dockerfile. Env vars over hardcodes—use .env files, never commit secrets.
For prod, scan with Trivy: trivy image yourimage. Caught a Redis vuln last week—saved headaches.
VS Code magic: Dev inside the container
Hate local mismatches? Use VS Code's Dev Containers. Install Remote-Containers extension, add .devcontainer/devcontainer.json:
{
"name": "PHP Docker",
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": ["bmewburn.vscode-intelephense-client", "xdebug.php-debug"]
}
}
}
Reopen in container—boom, full IDE inside Docker. Xdebug? Just add xdebug.mode=debug to dev php.ini. Debugging feels native.
I switched teams to this. Productivity spiked 2x—no more "my setup broke."
Common pitfalls and fixes
Ever hit "Composer out of memory"? Bump --memory-limit=2G in Dockerfile. Slow builds? Cache mounts are your friend.
Apache vs FPM? FPM wins for scale. One container or many? Start combined (Nginx+FPM+Supervisor), split later.
Testing? Spin up docker compose run app phpunit. Integration bliss.
Scaling? docker compose up --scale app=3. Load balanced, zero config.
Real-world tweaks for frameworks
Laravel: Add php artisan config:cache post-install. Horizon? Redis service ready.
Symfony: Flex handles deps; expose console: docker compose exec app bin/console.
WordPress: Volume /wp-content/uploads. Nginx rules for permalinks.
Tuned a Symfony app last month—Docker cut deploy time from 20min to 2.
Friends, this isn't theory. Grab your project, paste these files, docker compose up. Watch consistency bloom. In a world of flaky envs, Docker with PHP feels like coming home—reliable, fast, yours.
Next time you're knee-deep in code at 2 AM, smile. You've got this. And that quiet win? It'll carry you further than you think.