
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Docker Compose: Multi-Container Apps turn a folder of services into one runnable system. You declare PHP, MySQL, Redis, and a web server in a single compose.yaml file. Then one command starts the whole stack. That pattern matches how real apps run in production, but on your laptop. If you already use Docker Compose for local Laravel development, this guide goes deeper on wiring, persistence, and the mistakes that break teams after the first docker compose up.
compose.yaml file that lists services, networks, and volumes. Run docker compose up -d to start every container together with shared DNS and isolated storage.What is Docker Compose for multi-container apps?
Docker Compose is a tool that reads a YAML manifest and orchestrates several containers as one project. Each service gets its own image, environment, ports, and dependencies. Compose creates a default bridge network so containers reach each other by service name, not by IP address.
A typical Laravel app you containerize from scratch needs at least four moving parts: the PHP application, a database, a cache layer, and a reverse proxy. Without Compose, you start four separate docker run commands and manually link networks. With Compose, the file is the contract. New developers clone the repo, run one command, and get the same stack you use.
Compose v2 is a Docker CLI plugin. You invoke it as docker compose, not the legacy standalone docker-compose binary. The official Docker Compose documentation covers the full command set. For day-to-day work, you need up, down, ps, logs, and exec.
How do you write a compose.yaml for a Laravel stack?
Start with a minimal but complete file. Place it at the project root next to your application code. Use service names that match what your app expects in .env.
Base compose.yaml structure
name: laravel-stack
services:
app:
build:
context: .
dockerfile: docker/php/Dockerfile
volumes:
- .:/var/www/html
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
environment:
DB_HOST: mysql
REDIS_HOST: redis
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- .:/var/www/html:ro
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
mysql:
image: mysql:9.7
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: laravel
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: redis:8.10-alpine
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
Notice three design choices that save hours later. First, DB_HOST: mysql uses the service name, not 127.0.0.1. Inside the app container, localhost points to PHP itself. Second, depends_on with a health check stops Laravel from booting before MySQL accepts connections. Third, named volumes keep database files when you rebuild images.
On projects I maintain, PHP 8.3 or 8.4 images are still common for Laravel 12. Laravel 13 needs PHP 8.3 minimum. Pin your Dockerfile base tag to match your framework version. A mismatch here produces silent extension errors or Composer platform failures.
Environment file handling
Compose can load variables from .env in the project directory automatically. Keep secrets out of Git. Use env_file: .env on the app service, or map individual keys with ${DB_PASSWORD} substitution. This mirrors the Symfony environment config pattern for multi-environment apps, but scoped to containers.
How do you start and manage a Docker Compose stack?
The daily workflow is short. Build images when Dockerfiles change. Recreate containers when compose config changes. Inspect logs when something fails after a teammate pulls main.
- Install Docker Engine and the Compose plugin on Ubuntu or your host OS. See the Docker install guide for Ubuntu if you are starting fresh.
- From the project root, run
docker compose up -d --buildto build and detach. - Run
docker compose exec app php artisan migratefor first-time database setup. - Check status with
docker compose psand tail logs withdocker compose logs -f app. - Stop everything with
docker compose down. Add-vonly when you intend to wipe named volumes.
The multi-container setup for local development article covers bind mounts and IDE integration. Bind-mounting your source tree into the app container gives instant code reloads. Named volumes for MySQL stay on the host disk outside the container filesystem.
If a service exits immediately, run docker compose logs mysql before guessing. Permission errors on storage/ and wrong DB_HOST values cause most first-day failures I see on client projects.
How do Docker Compose networks and volumes work?
Every Compose project gets a default network named {project}_default. Containers join it automatically. DNS entries map service names to container IPs. Your Laravel .env should use mysql and redis as hostnames, exactly as declared under services:.
Custom networks isolate stacks. Two projects on one machine can both expose port 8080 if only one publishes it to the host. Internal traffic stays on the bridge. Read the dedicated Docker networking and volumes guide for bridge vs overlay details.
Volume types you will actually use
- Named volumes — best for database data. Survive
docker compose downwithout-v. - Bind mounts — map host paths into containers. Ideal for application code during development.
- Anonymous volumes — created implicitly. Easy to lose track of. Prefer explicit names.
For PostgreSQL instead of MySQL, swap the image to postgres:18 and adjust health checks. The PostgreSQL in Docker for development walkthrough shows the env vars and init scripts.
How does Docker Compose compare to Laravel Sail and raw docker run?
Teams often ask whether to adopt Sail, hand-written Compose, or plain Docker commands. The answer depends on who maintains the stack and where it runs.
| Approach | Best for | Trade-off |
|---|---|---|
| Docker Compose (custom) | Full control, production-like local stacks, mixed services (workers, Mailpit, MinIO) | You own every Dockerfile and upgrade path |
| Laravel Sail | Laravel-only projects with standard defaults | Less flexible for non-PHP sidecars; see Laravel Sail vs Docker Compose |
| docker run scripts | Quick one-off containers, CI smoke tests | No declarative manifest; hard to reproduce across machines |
| Kubernetes | Production at scale, multi-node clusters | Heavy for local dev; overkill for a two-person agency team |
For a booking platform like Adventure Third Pole Trek, I prefer explicit Compose files. They document queue workers, scheduler containers, and Redis in one place. Sail is fine when the stack matches Laravel defaults and nobody needs custom Nginx rules.
Compose is not a production orchestrator on its own. It works on a single host for staging or small deployments. For zero-downtime multi-server setups, you eventually move to Swarm or Kubernetes. The Kubernetes vs Docker Swarm comparison helps frame that decision.
How do you harden Docker Compose for staging and production?
Local Compose files optimize for speed. Production files optimize for immutability, secrets, and resource limits. Treat them as siblings, not copies.
Production-oriented patterns
Build images in CI and push to a registry. Production Compose should reference tagged images, not build: contexts on the server. Follow multi-stage Docker builds for Laravel production to keep images small.
services:
app:
image: registry.example.com/laravel-app:2026.09.1
restart: unless-stopped
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
read_only: true
tmpfs:
- /tmp
Set CPU and memory limits so one runaway queue worker cannot starve the database. The guide to limiting Docker container resources explains cgroups behaviour on Linux hosts.
Use Compose profiles and override files to split dev tools from production services. A compose.override.yaml can add Mailpit locally while production omits it entirely. Never commit production secrets. Inject them from your host environment or a secrets manager at deploy time.
On Ubuntu servers I administer for clients, Compose stacks sit behind host-level Nginx or Traefik with TLS from Let's Encrypt. That mirrors patterns in our Linux system administration service, where PHP-FPM reload and volume backups are part of the same contract as the app code.
Health checks matter in production. Without them, a reverse proxy may route traffic to a PHP container that booted before MySQL was ready. Define checks on every critical dependency. The Compose file healthcheck reference lists supported test formats.
Worker and scheduler services
Real Laravel apps need more than web and database containers. Add a queue worker and a scheduler as separate services sharing the same app image.
queue:
image: registry.example.com/laravel-app:2026.09.1
command: php artisan queue:work --sleep=3 --tries=3
depends_on:
- redis
- mysql
restart: unless-stopped
scheduler:
image: registry.example.com/laravel-app:2026.09.1
command: php artisan schedule:work
depends_on:
- mysql
restart: unless-stopped
One image, three roles — web, queue, scheduler. That is the same pattern I use on production Laravel deployments before GitLab CI pushes a release. It aligns with the twelve-factor app principles revisited for 2026: config in environment, processes as containers, logs to stdout.
What are common Docker Compose mistakes on real projects?
Most failures are configuration, not Docker bugs. These show up repeatedly when a team adopts Compose after months of native PHP on the host.
- Using 127.0.0.1 for database host — wrong inside containers. Use the service name.
- Publishing MySQL port 3306 on production — exposes the database to the LAN or internet. Keep it internal.
- Running
docker compose down -vcasually — deletes named volumes and wipes local data. - Omitting platform pins on Apple Silicon — add
platform: linux/amd64when images lack arm64 builds. - Mixing Compose file versions incorrectly — modern Compose uses the Compose Specification; avoid obsolete
version: "3.8"keys unless tooling requires them. - Ignoring file permissions on bind mounts — PHP may not write to
storage/. Match UID/GID in the Dockerfile or fix ownership on the host.
Validate YAML before pushing. Paste your manifest into the JSON and YAML formatter tool or run docker compose config to render the merged result. That command catches typos in service names and invalid keys early.
For WordPress or WooCommerce stacks, the same Compose principles apply. Separate MariaDB 12.3, PHP, and Nginx. Pin WooCommerce 11.1 against WordPress 7.1 in documentation for your team. Our WordPress development service often starts with a reproducible local Compose stack before any theme work.
When you outgrow a single server, Compose files become documentation for what Kubernetes Deployments must recreate. Treat the service list as the source of truth for your architecture review. Enterprise teams sometimes engage us through enterprise application development once Compose-proven stacks need HA clustering.
Key Takeaways
- Define every service, network, and volume in
compose.yamlso the full stack starts withdocker compose up -d. - Point application config at service names (
mysql,redis), notlocalhost, inside containers. - Use named volumes for database persistence and bind mounts only where hot reload matters.
- Add health checks and resource limits before calling a Compose stack production-ready.
- Split dev and prod manifests with profiles or override files instead of one bloated YAML file.
- Build and tag images in CI; production Compose should pull immutable tags, not build on the server.
People Also Ask
Can Docker Compose run in production?
Yes, on a single Linux host with proper secrets, restart policies, health checks, and resource limits. Compose is not a cluster scheduler. For multi-node HA you move to Kubernetes or Swarm, but many small and mid-size apps run reliably on one VPS with Compose for years.
How many containers should one Compose project have?
As many as your architecture needs, typically four to eight for a Laravel app: web proxy, PHP, database, cache, queue worker, scheduler, and optional dev tools. If the file exceeds a dozen services, consider profiles or splitting into separate projects linked by external networks.
Do I still need a .env file with Docker Compose?
Yes. Compose loads a project .env for variable substitution in YAML. Application containers also need runtime env vars for Laravel or Symfony. Keep secrets out of Git and inject them on the server at deploy time.
What is the difference between docker compose and docker-compose?
docker compose is the current V2 plugin integrated into the Docker CLI. docker-compose is the legacy Python binary. New projects should use V2 syntax and commands exclusively.
Ship your next stack with confidence
Docker Compose: Multi-Container Apps give you a repeatable contract between developers, CI, and staging. Start with a honest service list, add health checks, and split dev from production configs before the file grows messy. If you want help dockerizing a Laravel, Symfony, or WordPress system for your team in Nepal or abroad, review our portfolio of shipped platforms or reach out through contact us to plan your stack. For ongoing server work after launch, see support and maintenance and the broader custom software development offering on kokil.com.np.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

