A capstone warm-up. Given a Python web app with a requirements.txt and a Postgres database, the two files from lessons 3-1 and 5-1 that turn it into a one-command runnable stack are a Dockerfile and a docker-compose.yml.
| File | Job |
|---|---|
Dockerfile | packages the app into an image |
docker-compose.yml | declares the app plus db stack |
With both in place, docker compose up -d boots everything. Those are exactly the two files written in this lesson.
The app we're shipping
A small notes API called notesy: Python, a server.py listening on port 8000, storing notes in Postgres, with a /health endpoint (lesson 9-2) that checks its database connection. The repo:
notesy/ ├── server.py ├── requirements.txt ├── tests/ │ └── test_notes.py └── .git/
The mission across two lessons, using only things you have learned:
- Dockerfile → image (unit 3)
- compose file with app + db, volume, healthcheck (units 4, 5, 9)
- GitHub Actions pipeline: test → build → push → deploy (units 7, 8)
Step zero, before any building: .dockerignore, from lesson 3-3:
.git __pycache__ .env
The Dockerfile
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "server.py"]
| Line | Lesson it comes from |
|---|---|
| pinned slim base | 3-1 and 6-2 |
| dependencies copied before code | 3-3, the cache order |
EXPOSE 8000 | documentation of the listening port |
a single CMD | 3-1 |
Building and smoke-testing it locally:
docker build -t notesy:dev . docker run --rm -p 8080:8000 notesy:dev
--rm auto-removes the container when it stops, which is handy for throwaway runs. The app starts and then crashes, because there is no database. That is correct behavior, and exactly why compose exists.
The compose file
services:
app:
build: .
ports:
- "8080:8000"
environment:
DATABASE_URL: postgres://notesy:devpass@db:5432/notesy
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_USER: notesy
POSTGRES_PASSWORD: devpass
POSTGRES_DB: notesy
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "notesy"]
interval: 5s
retries: 5
volumes:
dbdata:| Detail | Lesson |
|---|---|
| service names as hostnames | 4-2 and 5-1 |
| env config matching on both sides | 5-2 |
| a named volume so notes survive | 4-1 |
condition: service_healthy gating startup | 9-2 |
pg_isready is Postgres's own readiness probe command, which is why the healthcheck needs no custom script.
After docker compose up -d, a curl localhost:8080/health answers ok. The stack is alive.
Why the URL says db and not localhost
DATABASE_URL uses the hostname db because inside the app container localhost is the app itself, and services reach each other by service name on the compose network.
This comes straight from lessons 4-2 and 5-1. Each container has its own localhost, and compose's network resolves service names through DNS.
| Host in the URL | Reaches |
|---|---|
localhost:5432 | nothing, the app container has no Postgres |
db:5432 | the Postgres container |
Renaming the service would mean updating the URL to match, since the service name is the DNS record. That coupling is worth noticing in review, because a rename that looks cosmetic breaks the connection string.
The line that guarantees the notes survived
The line is the volume mount under the db service, - dbdata:/var/lib/postgresql/data.
Postgres writes into the named volume dbdata, which lives outside any container, so down followed by up finds the same data waiting.
| Command | Effect on dbdata |
|---|---|
docker compose down | untouched |
docker compose up -d | remounted into the fresh container |
docker compose down -v | deleted |
down removes containers and the network but keeps named volumes, so the fresh db container mounts the same data. Only down -v would destroy it, which is why that flag deserves a pause before pressing enter.