When Docker Compose is the right answer
Docker Compose is still a strong production choice for small and medium stacks when one host is enough, the service boundary is clear, and the team values simple operations over platform sprawl.
A lot of deployment conversations start with the same anxious question: do we need Kubernetes?
Usually, no. If the system is a web app, a database, Redis, a queue, and a worker, Docker Compose on a solid host is often the cleaner production choice. It is easy to understand, easy to back up, and easy to debug when the person on call is tired.
Compose is not a toy because it started in local development. It is a tool with a narrow shape. When the project fits that shape, it works well.
The shape Compose likes
Compose is strongest when the whole application can live on one machine without pretending otherwise. Think one app server, one database, one cache, one background worker, maybe a reverse proxy and an admin service.
That setup gives you a few practical advantages:
- The deployment is readable in one file.
- Services can talk over a private Docker network.
- Volumes are easy to locate and back up.
- Logs are close to the containers.
- Rollback can be as simple as reverting the compose file and redeploying.
There is less platform to admire, but also less platform to maintain.
When it is a good production fit
Compose is a good answer when traffic fits on one host, downtime requirements are modest, and one team owns the whole stack. It also works well when the application has a simple data model and the main operational risks are backups, patches, disk space, and clear deployment steps.
We like it for internal tools, small SaaS products, CMS sites with custom services around them, API backends, dashboards, and early products that need boring reliability before they need a cluster.
The important phrase is "one host." You can make Compose do clever things around multiple machines, but that is usually a sign that the system has outgrown the tool.
Where Compose starts to hurt
Move away from Compose when you need several machines for capacity or availability. Move away when different teams need to deploy independently. Move away when rolling deploys, autoscaling, policy, service discovery, and network rules become central to the business.
Also move away if the compose file turns into a giant shared junk drawer. Once every service has special environment variables, secret paths, health checks, dependencies, sidecars, and manual deployment notes, the simplicity is gone.
At that point, Kubernetes, Nomad, Swarm, or a managed application platform may be worth the overhead.
The production rules we care about
Pin image versions. Avoid latest for anything that matters. If a deployment breaks, you need to know which image changed.
Use health checks for services that other services depend on. A container that has started is not the same as an application that is ready.
Keep data in obvious places. We prefer bind mounts under a documented host path for customer data because backup and restore are easier to reason about.
Treat secrets deliberately. Do not leave production credentials scattered through a compose file in a shared repository. Use environment files with correct permissions, a secret manager, or a deployment process that injects secrets outside source control.
Write the restore procedure before you need it. Backing up /srv/app is only half the job. The other half is proving that a fresh host can come back from that backup.
A small example
A boring production service might look like this:
services:
web:
image: ghcr.io/example/app:2026-06-01
restart: unless-stopped
env_file:
- /srv/example/env/web.env
depends_on:
db:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 10s
timeout: 3s
retries: 3
db:
image: postgres:16.4-alpine
restart: unless-stopped
volumes:
- /srv/example/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 3s
retries: 5
This is not glamorous. That is why it is useful. Anyone who understands Docker can read it, see where the data lives, and reason about what starts first.
The migration path
Compose does not have to be the final architecture. A healthy path is to start with Compose, learn the workload, and move only when the limits are real.
When the time comes, the migration is easier if you already have pinned images, health checks, clean secrets, clear volumes, and documented ports. Those habits carry over to Kubernetes or any other platform.
Our Docker Compose Stacks service is built around that approach. We host and maintain the machine, you bring the compose file, and we keep the operational path clean. If the stack outgrows one host, we can move it without pretending the first version was a mistake.
Try this production sanity check
Before putting a Compose stack in front of users, make the file prove itself:
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100
Check that the important services are actually healthy:
docker compose exec db pg_isready || true
docker compose exec redis redis-cli ping || true
curl -fsS http://127.0.0.1:8080/health
Practice backup and restore while the stakes are low:
sudo tar -czf backup-$(date +%F).tar.gz /srv/example
sudo tar -tzf backup-$(date +%F).tar.gz | head
Write the restore steps in the repo. A minimal restore drill should answer:
1. Which directories contain customer data?
2. Which environment files contain secrets?
3. Which image tags were running?
4. How do we start the stack on a clean host?
5. How do we verify the app after restore?
For a live host, keep a short operator checklist:
df -h
free -h
docker system df
docker compose ps
journalctl -u docker --since "1 hour ago" --no-pager
If these commands already feel too manual for the team, Compose may not be the wrong tool, but the operating process is not finished yet.