
Every tutorial from before 2023 tells you to put a `version: '3'` line at the top of your file and to run `docker-compose` with a hyphen. Both are wrong today. The `version:` key is deprecated under the modern Compose Specification and earns you a warning. The hyphenated `docker-compose` command is a retired Python binary. The current tool is `docker compose` (with a space), a plugin baked into Docker itself. Those two stale habits are the surface problems. The deeper one is startup order. Most tutorials show `depends_on: db` and leave it there. That only tells Docker to start the database container before the app container. It does not wait for Postgres to actually accept connections. Your app crashes on first boot, you add a `sleep 5` hack, and the tutorial moves on. This tutorial does it properly.

Time needed
About 30 minutes
Cost
Free (Docker Desktop is free for personal use)
Difficulty
Beginner, no prior Docker experience required
What you will have
A web app and a Postgres database running together
Docker Desktop bundles the Docker daemon, the Docker CLI, and the `docker compose` plugin in one installer. Download it for your OS from the official site below. Once installed, open a terminal and run the check command. If you see a version number, you are ready.
docker compose versionMake a new folder for your project and move into it. Everything in this tutorial lives in this folder.
mkdir my-app && cd my-appCreate a file called `compose.yaml` in the project folder. This is the modern filename. Compose still reads `docker-compose.yml` for backwards compatibility, but `compose.yaml` is the current convention. Paste this example. It defines two services: a Postgres database and a simple Node.js web app. Read the comments before running anything.
services:
db:
image: postgres:16
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: myapp
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
web:
image: node:20-slim
working_dir: /app
volumes:
- .:/app
command: node server.js
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/myapp
depends_on:
db:
condition: service_healthy
volumes:
db_data:The `db` service pulls the official Postgres 16 image. The `volumes:` block mounts a named volume called `db_data` so your database survives a `docker compose down`. The `healthcheck:` block runs `pg_isready` every five seconds. Only when that check passes does Compose allow the `web` service to start. The `web` service sets `DATABASE_URL` using `db` as the hostname, not `localhost`. Inside a Docker network, services reach each other by their service name. Using `localhost` here is the second most common beginner mistake: it resolves to the web container itself, not the database. The `depends_on:` block with `condition: service_healthy` is the correct way to sequence startup. Without it, Node tries to connect before Postgres has finished initialising its data directory and the connection fails.
Never put passwords directly in `compose.yaml`. Compose reads a `.env` file in the same folder automatically. Create one now with your database password.
DB_PASSWORD=change-this-to-something-strongIf you are using Git, add `.env` to your `.gitignore` immediately. Committing credentials is the fastest way to leak them.
echo ".env" >> .gitignoreRun the following command from your project folder. The `-d` flag runs containers in the background. Compose pulls the images on first run, which takes a minute.
docker compose up -dWatch the logs to see the health check in action. You will see Postgres initialise its data directory, then the health check pass, and then the web service start. This is what `service_healthy` buys you.
docker compose logs -fThe `ps` command shows you every container Compose started and its current status. A healthy database shows `(healthy)` next to the container name.
docker compose psWhen you are done, stop and remove the containers. Your database data is safe in the `db_data` named volume and survives this command. Run `docker volume prune` separately if you want to delete it.
docker compose downOld mistake 1
version: at the top. Deprecated. Delete it.
Old mistake 2
docker-compose with a hyphen. Use docker compose.
Old mistake 3
depends_on: db with no condition. Does not wait for readiness.
Old mistake 4
localhost as the DB host. Use the service name: db.
Old mistake 5
Password in compose.yaml. Use a .env file instead.
The `docker system prune` command does not remove named volumes by default. If you ever need to free disk space from old volumes, run `docker volume prune` or `docker system prune --volumes` explicitly. On a machine that has been running Compose stacks for a while, orphaned volumes can take several gigabytes.