Every multi-container scenario in this course — Apache plus Cloudflare
Tunnel, FastAPI plus a database, Claude API scripts with supporting services —
required you to type several docker run commands, remember the
right flags, create networks manually, and tear everything down in the right
order. Docker Compose replaces all of that with a single YAML file and two
commands: docker compose up and docker compose down.
Compose isn't just a convenience wrapper. It gives your infrastructure a
permanent written record, makes it reproducible on any machine, and becomes
the foundation for everything more advanced — Swarm, Kubernetes, CI pipelines.
It's the natural final step of this course.
1. Before and After Compose
Without Compose — Chapter 7 startup
docker network create osztromok-net
docker run -d \
--name osztromok-apache \
--network osztromok-net \
-v "C:\...\website":\
/usr/local/apache2/htdocs:ro \
httpd:2.4
docker run -d \
--name osztromok-tunnel \
--network osztromok-net \
cloudflare/cloudflared:latest \
tunnel --no-autoupdate run \
--token TOKEN_HERE
# Remember to stop both, in order, to clean up
With Compose — same thing
docker compose up -d
# That's it. Compose reads docker-compose.yml,# creates the network, starts both containers# in the right order, names everything# consistently, and remembers every flag.# To stop and clean up everything:
docker compose down
2
Anatomy of a Compose File
A Compose file is a YAML file named docker-compose.yml
(or compose.yml) that lives in your project root. It has three
top-level sections: services, volumes, and
networks.
# docker-compose.yml — annotated skeletonservices: # one entry per containerweb: # service name (used for DNS inside the network)image: nginx:1.27# use an existing image...build: ./app# ...or build from a Dockerfile in ./appports:
- "8080:80"# host:container port mappingvolumes:
- ./site:/usr/share/nginx/html:ro# bind mount
- app-data:/var/lib/data# named volumeenvironment: # env vars (avoid secrets here)
- APP_ENV=productionenv_file: # load from a .env file (use for secrets)
- .envnetworks:
- app-netrestart: unless-stopped# restart policydepends_on: # start db before web
- dbdb:
image: mysql:8.0volumes:
- db-data:/var/lib/mysql# persist database across restartsenvironment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=myappnetworks:
- app-netvolumes: # declare named volumesapp-data:
db-data:
networks: # declare custom networksapp-net:
Services communicate by name. Inside a Compose network,
every service is reachable at its service name as a hostname. The
web service can connect to MySQL at db:3306 —
no IP addresses, no manual network creation, Compose handles all of it.
3
Example: osztromok.com Fallback Stack
Let's rewrite the Chapter 7 fallback (Apache + Cloudflare Tunnel) as a
Compose file. This replaces start-fallback.ps1 entirely.
# First time: build the api image and pull postgres$docker compose up -d --build[+] Building 14.2s api image...
[+] Running 4/4
✔ Network app-net Created
✔ Volume pg-data Created
✔ Container db Started (healthy after 8s)
✔ Container api Started# Run database migrations inside the api container$docker compose exec api alembic upgrade head# Connect to postgres directly for inspection$docker compose exec db psql -U appuser -d appdbappdb=# \dt# Rebuild only the api image (after changing Dockerfile or requirements)$docker compose up -d --build api# Stop stack but KEEP the pg-data volume (data survives)$docker compose down# Stop stack AND delete volumes (fresh database next time)$docker compose down -v
depends_on with condition: service_healthy waits for the
healthcheck to pass before starting the dependent service. Without this, the
API container starts immediately, tries to connect to PostgreSQL before it's
ready, crashes, and Docker restarts it — which usually works eventually but
produces confusing error logs. The healthcheck approach is cleaner.
5
Compose Command Reference
Command
What it does
docker compose up -d
Create and start all services in the background (-d = detach)
docker compose up -d --build
Same but rebuild images first — use after changing Dockerfile or requirements
docker compose down
Stop and remove containers and networks. Volumes are preserved.
docker compose down -v
As above, also delete named volumes. Use when you want a completely fresh state.
docker compose ps
List containers in this Compose project with their status and ports
docker compose logs -f
Follow logs from all services. Add a service name to filter: logs -f api
docker compose exec api bash
Open a shell inside the running api service container
docker compose exec db psql ...
Run a command inside a running service (here: psql in the db container)
docker compose restart api
Restart just one service without touching the others
docker compose stop
Stop containers without removing them — faster to restart than down/up
docker compose pull
Pull latest versions of all images defined with image:
docker compose config
Validate and print the resolved Compose file — useful for debugging variable substitution
6
Override Files: dev vs prod
Compose supports layering files with -f. A common pattern is
a base docker-compose.yml with shared config, and a
docker-compose.override.yml that Compose applies automatically
in development:
# docker-compose.override.yml (dev additions — applied automatically)services:
api:
build: .# build from source in devports:
- "8000:8000"volumes:
- .:/workspace# bind-mount for live reloadcommand: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloaddb:
ports:
- "5432:5432"# expose postgres to host only in dev
# Production — explicit file selection (no override applied)
docker compose -f docker-compose.yml up -d
# Development — automatic (override.yml applied on top)
docker compose up -d
docker compose config shows you exactly what the merged
result looks like before you run it — essential for debugging when you have
multiple override files or complex variable substitution.
7
Profiles: Optional Services
Profiles let you define services that are off by default and only started
when explicitly requested — useful for debugging tools, admin UIs, or services
only needed occasionally:
services:
api:
image: myapp:latest# always starteddb:
image: postgres:16-alpine# always startedadminer: # web-based DB admin UIimage: adminerports:
- "8080:8080"profiles:
- tools# only start with --profile tools
Terminal — profiles
# Normal start — adminer NOT started$docker compose up -d✔ Container api Started
✔ Container db Started# Start with tools profile — adminer now included$docker compose --profile tools up -d✔ Container api Started
✔ Container db Started
✔ Container adminer Started
Exercises
Convert the Chapter 7 fallback to Compose. Create a folder called osztromok-fallback and write the docker-compose.yml from Step 3. Add a .env file with your Cloudflare tunnel token as CF_TUNNEL_TOKEN. Run docker compose up -d and confirm both containers appear in docker compose ps. Visit osztromok.com to confirm the site loads. Then run docker compose down and confirm both containers and the network are gone. Compare how much simpler this is than the PowerShell startup script.
Build the FastAPI + PostgreSQL stack. Set up the full stack from Step 4. Start it with docker compose up -d --build and watch it wait for PostgreSQL's healthcheck before starting the API. Run docker compose logs -f and observe the interleaved log output from both services. Connect to the database with docker compose exec db psql -U appuser -d appdb and run \conninfo to confirm you're inside the container's postgres. Stop and restart — confirm the volume persists (data survives). Then run docker compose down -v and confirm the volume is gone.
Use docker compose exec for development tasks. With the FastAPI stack running, use docker compose exec api to run each of the following without opening a separate terminal: pip list to see installed packages, python -c "import fastapi; print(fastapi.__version__)", and ruff check app/. Notice how Compose knows which container to target just from the service name.
Try override files. Split the FastAPI compose file into a base docker-compose.yml (no ports, no volumes, image-based) and a docker-compose.override.yml (adds ports, bind-mount, build, --reload). Run docker compose config and read the merged output to confirm the override is being applied. Then run docker compose -f docker-compose.yml config to see what production would look like without the override.
Add an optional service with profiles. Add adminer to your FastAPI stack under a tools profile. Run docker compose up -d (without the profile) and confirm adminer isn't started. Then run docker compose --profile tools up -d and visit http://localhost:8080 — log in to your PostgreSQL database using the credentials from your .env file. This is a useful pattern for admin tools you don't want running all the time.
🐳
Docker for Beginners — Complete
You've covered everything from containers vs VMs all the way to multi-service
Compose stacks. You can now pull and run images, manage volumes and networks,
write Dockerfiles, build custom images, run a real web server with a Cloudflare
Tunnel fallback, develop Python apps in a containerised environment, call the
Claude API safely, and orchestrate multi-container stacks with a single
YAML file.
What's next: A Docker Intermediate course would cover
multi-stage builds, Docker Swarm, container registries, CI/CD pipelines with
Docker, and production security hardening. When you're ready, ask for
a course outline.