EduFlow

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.

  1. 1The first compose.yaml
  2. 2Run the stack
  3. 3How the services find each other
  4. 4Splitting networks when it matters
  5. 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:

compose.yaml
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: notes

Two 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

Terminal
docker compose up -d --build
docker compose ps
docker compose logs -f api
curl localhost:8080/readyz

up 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:

Terminal
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 network

up 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.

Your laptop reaches the api container through published port 8080 mapped to 3000. Inside the notes_default network, api connects to db on port 5432 by service name. The database has no published port. your laptop localhost:8080 network: notes_default api :3000 db :5432 -p 8080:3000 db:5432 no published port
Only the API is published; the database is reachable by name inside the project network

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:

YAML
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.yaml declares services, networks and volumes; docker compose up creates whatever is missing and leaves the rest alone.
  • Compose v2 is the docker compose plugin; the top-level version: 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, not localhost.
  • Publish only the ports you need from the host; a database that only the API talks to needs no ports: entry at all.

Further reading

Check your understanding

4 questions

Question 1 of 4

Inside the api container, which DATABASE_URL host reaches the Postgres service named db?

Question 2 of 4

You add version: "3.8" at the top of compose.yaml. What does current Docker Compose do with it?

Question 3 of 4

Postgres in your Compose stack is only used by api. Which ports: setting should db have?

Question 4 of 4

You change only the api service’s environment in compose.yaml and run docker compose up -d. What happens to the running db container?

Practice

Spot the bug

Two Compose bugs, one connection

With this compose.yaml, the API logs ECONNREFUSED 127.0.0.1:5432, and a colleague on the office Wi-Fi just connected to your database with the password notes. Select the two lines responsible.

Select the line(s) you think are wrong, then check.