RizTech Academy logo
RizTech Academy
ContainersLesson 5 of 635 min

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: . vs image: 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 start redis before app. 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 (returning visits: 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

  1. Write a compose.yaml for the greetings app plus Redis; docker compose up -d and hit the app.
  2. Show the visit counter incrementing across requests, and explain how the app reaches Redis by name.
  3. Explain the difference between build: and image: for a service, with an example of each.
  4. Explain what depends_on guarantees and what it does not, and why the app must tolerate Redis not being ready.
  5. Use docker compose logs -f to watch both services, then docker compose down to tear the stack down.
  6. Explain why Compose is for local development and what job Kubernetes does that Compose does not.

Official documentation

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