Docker Compose for local development
Real applications are rarely a single container — an app needs a database, maybe a cache, maybe a couple of
services. Starting each with a long docker run command, in the right order, wired together, by hand, is
tedious and error-prone. Docker Compose describes the whole stack in one file and starts it with one
command. It is how you run a multi-container app locally, and it makes "clone the repo and run it" actually
work. This lesson is Compose, using the greetings app plus a Redis cache.
What Compose does
Docker Compose lets you define a multi-container application in a single YAML file (compose.yaml) — which
images to run, how they connect, their ports, their environment, their storage — and then manage the whole
thing as a unit:
docker compose up -d # start the whole stack in the background
docker compose ps # what is running
docker compose logs -f # follow logs from all services
docker compose down # stop and remove the whole stack
Instead of several docker run commands in the right order with the right flags, you write the stack down once
and bring it up with one command — reproducibly, the same for everyone on the team. This is the standard way to
run a project's full stack in local development.
The greetings stack
Here is the real compose.yaml for the greetings app and its Redis cache — verified to run:
services:
app:
build: . # build the image from the Dockerfile here
ports:
- "3000:3000" # host:container
environment:
GREETING: "Hello"
REDIS_URL: "redis://redis:6379" # note the hostname 'redis' — see below
depends_on:
- redis
redis:
image: redis:7-alpine # pull a ready-made image
volumes:
- redis-data:/data # persist Redis's data (the volumes lesson)
volumes:
redis-data:
Read it as a description of the whole system: two services (app and redis), what each runs (build: .
builds your Dockerfile; image: redis:7-alpine pulls a ready image), how the app is configured, and that the
app depends on redis. docker compose up builds the app image, pulls Redis, creates a network, and starts both
— wired together.
Services find each other by name
The line REDIS_URL: "redis://redis:6379" shows the single most useful thing Compose does: it puts every
service on a shared network where each service is reachable by its service name. The app connects to Redis
at the hostname redis — not an IP address — because Compose runs a small DNS that resolves the service name
redis to that container. So containers talk to each other by name, and you never hard-code an IP (which would
change anyway).
This is why the greetings app reads REDIS_URL from the environment (the docker-basics lesson): Compose sets
it to redis://redis:6379, and the app connects. Run the stack and hit it, and the visit counter — stored in
Redis — increments across requests:
$ curl localhost:3000/ -> {"message":"Hello from ...","visits":1}
$ curl localhost:3000/ -> {"message":"Hello from ...","visits":2}
The app container and the Redis container are cooperating over the Compose network, exactly as they would with real services in production. Service-name networking is the feature that makes multi-container development pleasant.
build vs image, and depends_on
Two details worth understanding in that file:
build: .vsimage: redis:7-alpine. A service either builds its image from a Dockerfile (build: ., for your code) or uses a pre-built image from a registry (image:, for things like databases and caches you do not write yourself). A typical stack builds your app and pulls its dependencies.depends_on— tells Compose to startredisbeforeapp. Note the caveat: it waits for the container to start, not for Redis to be fully ready to accept connections. For robustness the app should tolerate Redis not being ready yet and retry — which is why the greetings app connects to Redis lazily and keeps working (returningvisits: null) if Redis is momentarily unavailable, rather than crashing. Building apps that tolerate their dependencies not being instantly ready is a real production concern, not just a Compose quirk.
Compose is for development (and small deployments)
An important scope note: Compose is excellent for local development and for simple single-machine deployments, and it is where you will use it most. But it runs everything on one machine — it does not spread containers across a fleet, restart them on another node if one dies, or scale to many machines. That job belongs to an orchestrator — Kubernetes, the next module. The progression is deliberate: Compose teaches you to describe a multi-container stack declaratively (services, networking, config, volumes), and Kubernetes takes those same ideas to a production cluster. Master Compose locally, and Kubernetes' concepts will feel familiar rather than alien.
Check your work
Compose defines a multi-container app in one compose.yaml and manages it as a unit: docker compose up -d / ps / logs -f / down. Replaces several ordered docker run commands — reproducible for the whole
team; the standard way to run a project's full stack locally.
A service either build:s its image from a Dockerfile (your code) or uses a pre-built image:
(databases/caches). depends_on sets start order — but waits for start, not ready, so apps should tolerate
a not-yet-ready dependency (greetings connects to Redis lazily and keeps working if it's down).
Service-name networking (the key feature): Compose puts services on a shared network and resolves each
service name to its container, so the app reaches Redis at hostname redis (via REDIS_URL), never a
hard-coded IP. The greetings visit counter (in Redis) increments across requests — the containers cooperate
over the Compose network.
Scope: Compose is for local dev and simple single-machine deploys — it runs on one machine, does not spread/heal/scale across a fleet. That is the orchestrator's job (Kubernetes, next). Compose's declarative ideas carry straight over.
Practice
- Write a
compose.yamlfor the greetings app plus Redis;docker compose up -dand hit the app. - Show the visit counter incrementing across requests, and explain how the app reaches Redis by name.
- Explain the difference between
build:andimage:for a service, with an example of each. - Explain what
depends_onguarantees and what it does not, and why the app must tolerate Redis not being ready. - Use
docker compose logs -fto watch both services, thendocker compose downto tear the stack down. - Explain why Compose is for local development and what job Kubernetes does that Compose does not.
Official documentation
- Docker — Compose overview — What Compose is and how to use it.
- Docker — Compose file reference — Services, networks, volumes and every field.
- Docker — Networking in Compose — How services reach each other by name.
Next: volumes, networks and persistence.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship