Docker Compose solves the problem that shows up on day two of every project: your application is no longer alone, it needs a database, maybe Redis for caching, and you want to bring everything up with a single command. A well-written docker-compose.yml describes that whole stack so any teammate can run it without reading docs.
The real minimal stack: app + database
This is the starting point of 90% of backend projects:
services:
app:
image: miapp:latest
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://app:cambiar@db:5432/app
restart: unless-stopped
depends_on:
- db
db:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=cambiar
- POSTGRES_DB=app
restart: unless-stopped
volumes:
pgdata:
With this, docker compose up -d brings up both connected containers.
The detail that unlocks everything: internal DNS by service name
Look at the connection string: postgres://app:cambiar@db:5432/app. The host is db — the service name in the YAML. Compose creates an internal network where each service registers its name as hostname. Your application needs no IPs and cannot use localhost (which inside the container points at the container itself): it resolves db directly.
This is why Compose stacks work with zero extra configuration: Docker creates and populates the network by itself.
Watch out with depends_on: it controls startup order, not availability. It guarantees the Postgres container starts before the app, but Postgres takes a few seconds to accept connections after booting. If your app crashes on startup because the DB is not ready yet, you need retry logic in the application (or healthchecks with depends_on: condition: service_healthy).
Named volumes vs bind mounts
The most important distinction in the entire file:
Named volume (pgdata:/var/lib/postgresql/data): Docker manages storage in its own area. Correct choice for database data: native performance, portability across systems, persistence even if you delete containers (only gone after docker compose down -v).
Bind mount (./nginx.conf:/etc/nginx/nginx.conf:ro): maps a folder or file from your machine into the container. Ideal for config you edit live and for development with hot-reload (./src:/app/src). The :ro suffix mounts it read-only.
A classic mistake is using a bind mount for DB data in development and then wondering why production (where those local paths do not exist) breaks everything: named volumes are the portable version.
Ports: host:container
"3000:3000" means port 3000 on your machine → port 3000 in the container. They may differ: "8080:80" exposes an internal nginx on your port 8080. Two caveats:
- If two services publish the same host port, the second one fails to start.
- Ports between services on the internal network do NOT need publishing: your app reaches Postgres at
db:5432even without publishing 5432. In serious production, don't even publish the DB port — keep it reachable only from the internal network.
The restart policy you actually want
Compose offers four values and one clear winner for servers:
no(default): never restarts. Only makes sense during active development.on-failure: restarts only when the process exits with an error code. Reasonable for jobs that can fail occasionally.always: always restarts, even after a manualdocker stopplus daemon restart. Surprisingly annoying: you shut the service down and it comes back.unless-stopped: like always but respects a manual stop. It is the right default for production: survives server reboots and does not resurrect when you decide to stop it.
No version: key
Older tutorials show a first line version: "3.8". It is obsolete: the current Compose specification ignored it first and removed it later. A modern YAML starts directly at services:. If some tool demands the key, that tool is outdated.
Sensitive variables outside the YAML
The example includes POSTGRES_PASSWORD=cambiar for brevity, but in a real repository those credentials must come from the environment:
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
Compose reads .env from the same directory automatically. .env goes into .gitignore; the compose file goes to the repo.
Generate your stack visually
Adding PostgreSQL, MySQL, MariaDB, Redis, MongoDB, Nginx or a Node app with their image, port, volume and variable presets is just clicks in our Docker Compose Generator: configure each service, mark dependencies with depends_on and copy the final YAML.
FAQ
Is Compose good for production? For small VPS and self-hosting, yes — it is a reasonable alternative to Kubernetes when K8s would be shooting flies with a cannon. For multi-node orchestration with autoscaling, you will need something more.
How do I see a service's logs? docker compose logs -f app. With -f you follow the live stream; filter by service name.
Can I run commands inside a container? docker compose exec db psql -U app opens a Postgres console inside the db container without installing anything on your machine.
Build your stack with ready-made presets using the Docker Compose Generator, free and no sign-up.