All Modules Why Compose The File Hands-on Lab Cheat Sheet

Docker Compose

Run your app and its database together — with one command.

Module 7 · We give the notes app a real database. Free · No cloud account needed.

Beginner Containers ~50 min

What You'll Learn

By the end of this module you will be able to:

  • Explain why running multiple containers by hand is painful — and how Compose fixes it
  • Read and write a docker-compose.yml file
  • Run a multi-container app (web + database) with a single docker compose up
  • Use volumes so your data survives container restarts
  • Let containers talk to each other over a Compose network by service name
  • Pass config with environment variables and order startup with depends_on

Prerequisites: Module 6 — Docker. You should already be able to build an image and run a container.

Why Compose Exists

In Module 6 you ran a single container. But real apps are rarely alone — a web app needs a database, maybe a cache, maybe a worker. Wiring those up by hand gets ugly fast:

# The painful manual way — multiple long commands, in the right order, every time docker network create notes-net docker run -d --name db --network notes-net -e POSTGRES_PASSWORD=secret postgres:16 docker run -d --name web --network notes-net -p 8080:5000 -e DATABASE_URL=... notes-app

You'd have to remember every flag, the right order, the network name — and type it all again after every reboot. Docker Compose replaces all of that with one file and one command.

The whole idea

Describe all your containers, their config, and how they connect in a single docker-compose.yml file. Then start the entire stack with docker compose up — and tear it down with docker compose down.

"compose" vs "compose" (the dash)

Modern Docker uses docker compose (space, built in). Older tutorials show docker-compose (hyphen, a separate tool). If the space version errors, try the hyphen one — they behave the same.

Anatomy of a Compose File

A Compose file is written in YAML — indentation matters (use spaces, never tabs). Four words cover almost everything:

KeyWhat it defines
servicesYour containers. Each service = one container (web, db, …).
volumesNamed storage that lives outside a container so data survives restarts.
networksHow services reach each other. Compose makes one automatically — services find each other by name.
environmentConfig values (passwords, URLs) passed into a container.

The #1 beginner trap: YAML indentation

YAML uses 2 spaces per level and no tabs. A single misaligned line breaks the whole file. If you get a cryptic parse error, check your spacing first.

Hands-on Lab: App + Database

We'll upgrade the notes app from Module 6 so it actually saves notes in a PostgreSQL database — all orchestrated by Compose. Start in your notes-app folder from last module.

1

Update the app to use a database — app.py

The app now reads/writes notes in Postgres. Note how it reads the connection string from an environment variable — never hard-code secrets.

import os import psycopg2 from flask import Flask, request, redirect app = Flask(__name__) def get_db(): return psycopg2.connect(os.environ["DATABASE_URL"]) def init_db(): with get_db() as conn, conn.cursor() as cur: cur.execute("CREATE TABLE IF NOT EXISTS notes (id SERIAL PRIMARY KEY, body TEXT)") @app.route("/") def home(): with get_db() as conn, conn.cursor() as cur: cur.execute("SELECT body FROM notes ORDER BY id DESC") items = "".join(f"<li>{r[0]}</li>" for r in cur.fetchall()) return f"""<h1>Notes 🐳</h1> <form method="post" action="/add"> <input name="body"><button>Add</button> </form><ul>{items}</ul>""" @app.route("/add", methods=["POST"]) def add(): with get_db() as conn, conn.cursor() as cur: cur.execute("INSERT INTO notes (body) VALUES (%s)", (request.form["body"],)) return redirect("/") if __name__ == "__main__": init_db() app.run(host="0.0.0.0", port=5000)
2

Add the database driver — requirements.txt

flask==3.0.3 psycopg2-binary==2.9.9

Your Dockerfile from Module 6 stays exactly the same — it already installs from requirements.txt. Compose will build it for you.

3

Write the orchestration — docker-compose.yml

Create this file in the project root. This is the heart of the module — read every line, we explain them next.

services: web: build: . ports: - "8080:5000" environment: DATABASE_URL: postgresql://notes:secret@db:5432/notesdb depends_on: - db db: image: postgres:16 environment: POSTGRES_USER: notes POSTGRES_PASSWORD: secret POSTGRES_DB: notesdb volumes: - db-data:/var/lib/postgresql/data volumes: db-data:

The magic line: @db

Look at the DATABASE_URL — the host is literally db, the name of the other service. Compose's built-in network lets containers reach each other by service name. No IP addresses, ever.

4

Start the whole stack — one command

docker compose up --build

Compose builds your web image, pulls Postgres, creates the network and volume, and starts both containers in order. Watch the logs from both services stream together. Open http://localhost:8080, add a few notes. ✅

The "aha!" moment

Two containers, a network, and persistent storage — all from one file and one command. That's the leap from Module 6.

5

Prove the data persists

Stop the stack, then bring it back — your notes are still there, because they live in the db-data volume, not the container.

docker compose down # stop & remove containers (volume kept) docker compose up # back up — your notes survived 🎉

Careful: down -v deletes data

docker compose down keeps named volumes. Adding -v (docker compose down -v) deletes the volume too — your notes are gone. Use it only when you want a clean slate.

6

Run it in the background & inspect

docker compose up -d # detached (background) docker compose ps # list this project's containers docker compose logs -f web # follow just the web service's logs docker compose exec db psql -U notes -d notesdb -c "SELECT * FROM notes;"

That last command runs psql inside the database container and queries your notes directly — proof the web and db containers are truly talking.

The Compose File, Key by Key

KeyWhat it does
build: .Build this service from the Dockerfile in the current folder (our web app).
image: postgres:16Use a ready-made image from the registry instead of building (the database).
ports: "8080:5000"Map host port 8080 → container port 5000, same as -p in Module 6.
environmentInject config into the container — DB credentials, connection URL.
depends_on: dbStart db before web (controls order, not readiness — see note).
volumes: db-data:/var/lib/...Mount the named volume into Postgres's data directory so data persists.
volumes: (top level)Declare the named volume db-data that Docker manages for you.

depends_on doesn't wait for "ready"

It waits for the db container to start, not for Postgres to be accepting connections. In production you add a healthcheck or retry logic. For this lab, Postgres starts fast enough; if you ever hit a connection error on first boot, just re-run docker compose up.

Two Kinds of Storage

You'll meet both. Know the difference:

TypeSyntaxUse it for
Named volumedb-data:/var/lib/...Persistent data Docker manages (databases). What we used.
Bind mount./app:/appMap a host folder into the container — great for live-editing code in dev.

Dev tip: live code reload

Add a bind mount to the web service (volumes: ["./:/app"]) and your code edits show up inside the container without rebuilding — a huge speed-up while developing.

Compose Cheat Sheet

The commands you'll use every day. Bookmark this.

CommandWhat it does
docker compose upBuild (if needed) and start all services
docker compose up --buildForce a rebuild, then start
docker compose up -dStart in the background (detached)
docker compose downStop & remove containers and network (keeps volumes)
docker compose down -vSame, but also delete named volumes (⚠ data loss)
docker compose psList this project's containers
docker compose logs -fFollow logs from all services
docker compose logs -f webFollow logs from one service
docker compose exec web bashOpen a shell inside a running service
docker compose buildBuild/rebuild images without starting
docker compose restart webRestart a single service
docker compose stopStop without removing

Troubleshooting

SymptomLikely cause & fix
yaml: line N: ... parse errorIndentation — use 2 spaces, no tabs, consistent alignment.
web can't connect to db on first rundb not ready yet. Re-run docker compose up, or add a healthcheck.
Notes disappear after restartYou ran down -v, or the volume isn't mounted to the data path.
Code change not reflectedRebuild: docker compose up --build (or add a bind mount for dev).
port is already allocated8080 in use — change to "8081:5000".
docker compose not foundOlder install — use the hyphen form docker-compose.

Your Challenge

Make it yours before moving on:

  • Add a third service: Adminer (a web DB viewer) and browse your notes table in the browser.
  • Move the database password into a .env file instead of hard-coding it.
  • Add a bind mount to web so code edits apply without rebuilding.
  • Bonus: give db a healthcheck so web waits until it's truly ready.
# add under services: — then open http://localhost:8081 adminer: image: adminer ports: - "8081:8080" depends_on: - db # In Adminer: System=PostgreSQL, Server=db, User=notes, Password=secret, DB=notesdb

Recap & What's Next

You can now

Define a multi-container app in one file, run it with one command, persist data with volumes, and let services talk over a Compose network. This is how most teams run apps locally.

Next up: Module 8 — Kubernetes Intro, where you'll take this same app and orchestrate it on a real (local) cluster with Minikube — the step from one machine to many.

Docker Compose

Objectives Why Compose Anatomy Hands-on Lab File Explained Storage Types Cheat Sheet Troubleshooting Challenge Recap