Section 3 · Local Environments with Docker Compose
Compose Fundamentals: Services and Networks
18 min3 min readSpot the bug
Lesson at a glance
Describe notes-api and Postgres in one compose.yaml, start and inspect the stack with docker compose, and connect services by name over Compose’s default network.
- 1The first compose.yaml
- 2Run the stack
- 3How the services find each other
- 4Splitting networks when it matters
- 5Where Compose fits
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
notes-api needs Postgres. You could start both with two docker run commands, a hand-made network and a page of flags, and then explain it all again to the next person who joins the team. Compose replaces that with one file in the repository that describes the whole stack, and one command that makes it real.
The first compose.yaml
Create compose.yaml next to the Dockerfile:
name: notes
services:
api:
build: .
image: notes-api:dev
ports:
- "8080:3000"
environment:
PORT: "3000"
DATABASE_URL: postgres://notes:notes@db:5432/notes
db:
image: postgres:17
environment:
POSTGRES_USER: notes
POSTGRES_PASSWORD: notes
POSTGRES_DB: notesTwo services. api is built from the Dockerfile in this directory and tagged notes-api:dev; db uses the official Postgres 17 image, whose entrypoint creates the user and database from those three variables on first start. name: notes sets the project name, which prefixes everything Compose creates.
When a service has both build: and image:, Compose builds from the Dockerfile and tags the result with the image: name. That gives the local image a predictable name you can also run by hand, instead of the default name Compose would generate from the project and service names.
There’s no version: line. Compose v2 implements the Compose Specification, and if you copy an old file that starts with version: "3.8", Compose ignores it and warns that the attribute is obsolete. Delete it.
The command is docker compose, with a space. That’s Compose v2, a Docker CLI plugin. The old Python docker-compose with a hyphen has been retired for years, and tutorials that use it are out of date in other ways too.
Run the stack
docker compose up -d --build
docker compose ps
docker compose logs -f api
curl localhost:8080/readyzup builds the image if needed, creates a network, starts both containers and returns. --build forces a rebuild so code changes are picked up. /readyz now answers ready, because the API can reach the database.
docker compose ps shows the containers Compose created, named after the project, the service and a number: notes-api-1 and notes-db-1. The number exists because a service can run several identical containers. Every command takes service names, not container names, so you rarely type the full name.
A few more you’ll use daily:
docker compose exec db psql -U notes -d notes # SQL shell in the db container
docker compose restart api # restart one service
docker compose down # stop and remove containers and networkup is declarative: it compares the file to what’s running and changes only the difference. Edit the API’s environment and run up -d again, and Compose recreates api while db keeps running. Kubernetes works on the same idea at a much larger scale; you’ll meet it in section 4.
How the services find each other
Compose creates a network for the project, notes_default, and attaches every service to it. On that network, Docker’s DNS resolves each service name to its container. That’s why DATABASE_URL says @db:5432.
Notice that db has no ports: entry. The API reaches it over the project network; nothing on your laptop or your office network can. If you want to point a desktop SQL client at it, publish it on loopback only: "127.0.0.1:5432:5432".
Splitting networks when it matters
For bigger stacks you can declare networks and attach each service only where it belongs. A database on a backend network can’t be reached by a web front end that only joins frontend:
services:
web:
networks: [frontend]
api:
networks: [frontend, backend]
db:
networks: [backend]
networks:
frontend:
backend:For notes-api, the default network is enough. Reach for this when a stack has components that should never talk to each other.
Where Compose fits
Compose is excellent for three jobs: local development, integration tests in CI (start the stack, run the tests, tear it down), and small services that live on a single host. What it doesn’t do is spread containers across machines, move them when a machine dies, or roll out a new version gradually while watching health checks. Those are an orchestrator’s jobs, and they’re what Kubernetes does in section 4. The habits you build here carry straight over: declare the desired state in a file, let a tool make it so, and connect services by name.
One problem remains. Run docker compose down, then up again, and every note is gone: Postgres stored its data in the container’s writable layer, which down deleted. The next lesson gives the database somewhere permanent to live.
Key takeaways
- A
compose.yamldeclares services, networks and volumes;docker compose upcreates whatever is missing and leaves the rest alone. - Compose v2 is the
docker composeplugin; the top-levelversion:key is obsolete and should be removed. - Every service joins the project’s default network and is reachable by its service name, so the API connects to
db:5432, notlocalhost. - Publish only the ports you need from the host; a database that only the API talks to needs no
ports:entry at all.