Support

Lesson 2 of 6 ·12 min read

Compose and Container Networking

One command to run your API, database, and cache together, and the networking idea that makes it work.

Fast answer: Docker Compose describes several containers (your API, PostgreSQL, Redis) in one file and starts them together with docker compose up. Docker puts them on a shared network with built-in DNS, so your API reaches the database at the hostname db instead of an IP address. The principle underneath is the one that carries all the way to Kubernetes: services find each other by name.

The localhost trap

Suppose you have three containers running: a Flask API, PostgreSQL, and Redis. How does Flask talk to PostgreSQL? The natural first guess:

Flask → localhost:5432   ✗

It fails, and the reason teaches you the core idea. Inside the Flask container, localhost means this Flask container. Not your machine, and not the PostgreSQL container. Each container has its own isolated network view, exactly like each has its own filesystem.

Networks and built-in DNS

Docker's answer is the network: put containers on a shared network and Docker runs a small DNS service inside it, resolving container names to the right container.

           Docker network
  ┌─────────────────────────────┐
  │   ┌────────┐                │
  │   │ Flask  │                │
  │   └───┬────┘                │
  │       │  postgres:5432      │
  │       ▼                     │
  │   ┌────────────┐            │
  │   │ PostgreSQL │            │
  │   └────────────┘            │
  │   ┌────────┐                │
  │   │ Redis  │                │
  │   └────────┘                │
  └─────────────────────────────┘

Instead of an IP address, the connection string simply names the service:

Terminal
postgresql://user:password@postgres:5432/mydatabase

This matters because containers are disposable: recreate one and its internal IP changes, but its name does not. Hardcoded IPs break on every restart; names keep working.

Compose: the stack in one file

You could create the network and start each container by hand, in the right order, with the right flags, every time. Compose replaces that ritual with a file:

compose.yaml
services:
  api:
    build: .
    ports:
      - "8000:5000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/mydatabase
    depends_on:
      - db

  db:
    image: postgres:17
    volumes:
      - dbdata:/var/lib/postgresql/data

  redis:
    image: redis

volumes:
  dbdata:
Terminal
docker compose up
docker compose down

Compose creates the network automatically, and each service is reachable at its service name: the API connects to db:5432 because the service is called db. Notice the pieces from Lesson 1 reappearing with their jobs unchanged: a port mapping for the one service the outside world should reach, an environment variable delivering configuration, a named volume protecting the database's data.

depends_on controls start order, with a catch worth knowing now: it waits for the database container to start, not for the database inside it to be ready to accept connections. Real setups add a health check or retry logic in the app. Filing that distinction away early will save you a confusing morning later.

Try it: diagnose the broken hostname

In the Compose file, temporarily change @db:5432 to @localhost:5432 and predict the failure before running it. Then restore db.

Explain the fix: the API and database are separate network namespaces. localhost points back to the API container; db is resolved by Docker's DNS to the database container.

If container networking still feels abstract

Think of each container as a separate house. Every house calls itself "here"; that is localhost. To reach another house you need its address. Docker's network is the neighbourhood directory, and the Compose service name is the stable name in that directory.

What Compose is, and is not, for

Compose is the standard way to run a multi-service app in development: one file in the repo, docker compose up, and every teammate has the whole stack. It can also be a perfectly reasonable way to run a small production system on a single server.

What it does not do is turn one machine into many. If that server fails, everything on it fails together. Backups, OS updates, HTTPS, and failover all remain your job. When those limits start to bite, you are ready for the second half of this course; when they do not, one server running Compose is a legitimate production architecture, not a temporary embarrassment.

Check your understanding

Answer before opening each explanation.

Why is localhost:5432 wrong inside the API container?

Hint: Ask which machine "local" refers to.

Answer: It means port 5432 inside the API container itself. The database is a different container, so the connection string must use its Compose service name: db:5432.

Why does a service name survive container replacement when an IP address may not?

Hint: Separate identity from location.

Answer: A replacement container can receive a new internal IP. Docker's DNS updates the stable service name to the current address, so clients do not need to know that location changed.

Why does only the api service publish a port?

Hint: Which service must a browser outside the Docker network reach?

Answer: The API is the public entry point. The API can reach Postgres and Redis through the private Docker network, so those services do not need host ports unless a host-side tool must connect to them.

Why can the first database query fail even with depends_on: db?

Hint: Starting a container and being ready are different events.

Answer: depends_on establishes start order, but PostgreSQL may still be initialising after its container starts. Add a health check and readiness condition, or make the API retry transient connection failures.