What Is Docker Compose and Why Should You Use It?
Running a single Docker container is straightforward. But real-world applications rarely consist of just one container. A typical web application might need a web server, an application runtime, a database, a cache layer, and perhaps a background worker. Managing these containers individually with long docker run commands — remembering ports, volumes, networks, environment variables, and startup order — quickly becomes unmanageable.
Docker Compose solves this by letting you define your entire multi-container application in a single YAML file. Instead of running five separate docker run commands, you write one docker-compose.yml and bring everything up with a single docker compose up. It handles networking between containers, manages volumes, respects dependencies, and makes your deployment reproducible across development, staging, and production environments.
docker compose (with a space). The older standalone docker-compose (with a hyphen) is deprecated. This tutorial uses the modern V2 syntax throughout.
Understanding the docker-compose.yml Structure
A Compose file is built around three primary concepts: services, networks, and volumes. Let us start with a minimal example and build from there.
A Minimal Compose File
This file defines one service called web that runs the official nginx image, maps port 8080 on your host to port 80 in the container, and mounts a local ./html directory as read-only content.
Key Directives Explained
| Directive | Purpose | Example |
|---|---|---|
| image | Docker image to use | mysql:8.0 |
| build | Build from Dockerfile | build: ./app |
| ports | Host:container port mapping | "3306:3306" |
| volumes | Persistent storage | db_data:/var/lib/mysql |
| environment | Environment variables | MYSQL_ROOT_PASSWORD: secret |
| env_file | Load vars from file | env_file: .env |
| depends_on | Startup ordering | depends_on: [db, cache] |
| restart | Restart policy | restart: unless-stopped |
| networks | Network attachment | networks: [frontend, backend] |
| deploy | Resource limits (Swarm/Compose) | deploy: resources: limits: |
| healthcheck | Container health monitoring | healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] |
| command | Override default command | command: ["--max-connections=200"] |
Building a Real-World Stack: WordPress + MySQL + Cache
Let us build a complete, production-ready WordPress stack. This example demonstrates almost every Compose feature you will need in practice.
What Makes This Production-Ready
db service to be healthy (not just started). The MySQL healthcheck runs mysqladmin ping every 10 seconds, and WordPress only starts once MySQL passes 5 consecutive checks.${DB_USER:-wordpress} syntax means the variable falls back to "wordpress" if not set in the .env file. Sensitive values like passwords come from the .env file, never hardcoded.backend network is marked internal: true, meaning containers on it cannot reach the internet. MySQL is only on the backend network — it cannot be accessed from outside. WordPress sits on both networks: frontend for nginx communication, backend for database access.wp_data and db_data are named volumes managed by Docker. They persist across container restarts and docker compose down (unless you pass -v).:ro (read-only), preventing containers from accidentally modifying host files.The .env File
.env file to version control. Add it to your .gitignore immediately. Instead, provide a .env.example file with placeholder values that developers can copy and fill in.
Named Volumes vs Bind Mounts
Docker offers two primary ways to persist data: named volumes and bind mounts. Understanding the difference is crucial for production deployments.
Named Volumes
- Managed by Docker engine
- Stored in
/var/lib/docker/volumes/ - Portable across hosts
- Better performance on macOS/Windows
- Best for: databases, application data
Bind Mounts
- Maps a host directory directly
- Changes visible immediately on both sides
- You control the exact location
- Best for: config files, source code (dev)
Essential Docker Compose Commands
| Command | Description |
|---|---|
docker compose up -d | Start all services in detached (background) mode |
docker compose down | Stop and remove containers, networks (preserves volumes) |
docker compose down -v | Stop, remove, and delete volumes (data loss!) |
docker compose ps | List running containers and their status |
docker compose logs -f | Follow live logs from all services |
docker compose logs -f db | Follow logs from a specific service |
docker compose exec web sh | Open a shell inside a running container |
docker compose build | Build or rebuild images from Dockerfiles |
docker compose pull | Pull latest images from registry |
docker compose restart web | Restart a specific service |
docker compose stop | Stop services without removing containers |
docker compose config | Validate and display the resolved Compose file |
docker compose top | Display running processes inside containers |
Common Workflows
Building Custom Images with Compose
Instead of using pre-built images, you can point Compose to a Dockerfile and build your application image automatically.
Multi-Stage Dockerfile for Production
Multi-stage builds produce tiny production images. The build stage contains Go compilers, source code, and dependencies (often 1GB+). The final stage contains only the compiled binary and CA certificates — typically under 20MB.
Healthchecks: Beyond depends_on
By default, depends_on only waits for a container to start, not to be ready. A MySQL container might be "started" but still initializing its database for another 30 seconds. Healthchecks solve this.
30s grace
Every 10s
Healthy
Production Configuration Tips
Restart Policies
| Policy | Behavior | Use Case |
|---|---|---|
no | Never restart | One-shot tasks, debugging |
always | Always restart, even on clean exit | Critical services that must run |
unless-stopped | Restart unless manually stopped | Best for production |
on-failure:5 | Restart on failure, max 5 attempts | Services with known crash bugs |
Resource Limits
memory limits in production.
Logging Configuration
Without log rotation, container logs can fill your disk. The json-file driver with max-size and max-file keeps logs manageable. Three 10MB files mean at most 30MB of logs per container.
Environment Management
Docker Compose supports several ways to inject environment variables, each appropriate for different scenarios.
Method Comparison
| Method | Security | Best For |
|---|---|---|
| Inline in YAML | Low (visible in VCS) | Non-sensitive defaults |
.env file | Medium (add to .gitignore) | Local development |
env_file: directive | Medium | Per-service env files |
| Shell environment | High (CI/CD injection) | Production, CI/CD pipelines |
| Docker secrets | Highest (encrypted) | Swarm mode production |
Compose Profiles: Optional Services
Not every service needs to run all the time. Profiles let you define optional services that only start when explicitly requested.
Scaling Services
Need more workers to handle load? Compose can scale services horizontally:
"3000:3000"). Multiple containers cannot bind to the same host port. Either remove the port mapping or use a load balancer (like nginx) in front.
Docker Compose with Panelica
Panelica includes built-in Docker Compose management that brings the power of multi-container orchestration into the panel's graphical interface. You can upload docker-compose.yml files, deploy stacks, and manage multi-container applications through the panel's GUI or API without SSH access. The panel handles container lifecycle, shows logs, monitors resource usage, and manages port assignments — all integrated with Panelica's RBAC system so each user can only see and manage their own containers.
Troubleshooting Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Container exits immediately | Main process crashes or exits | docker compose logs servicename |
| Port already in use | Another service on the same port | ss -tlnp | grep :PORT |
| Volume permission denied | UID mismatch between host and container | Match UIDs or use user: directive |
| Cannot connect between services | Wrong hostname or missing network | Use service name as hostname, check networks |
| Changes not reflected | Using cached image | docker compose up -d --build --force-recreate |
| Disk full | Unrotated logs or dangling images | docker system prune -a |
Conclusion
Docker Compose transforms multi-container management from a series of fragile scripts into a single, declarative, version-controlled file. We have covered the complete lifecycle: writing Compose files with services, networks, and volumes; using healthchecks for reliable startup ordering; managing environments securely; optimizing images with multi-stage builds; and configuring production essentials like restart policies, resource limits, and log rotation.
The key takeaways: always use named volumes for persistent data, never hardcode secrets, set memory limits on every service, use healthchecks instead of naive depends_on, and keep your images small with multi-stage builds. Start with the WordPress stack example in this tutorial, adapt it to your application, and you will have a reproducible, production-ready deployment in minutes.