From one image to a running system. docker-compose, env config, shared networks.
Cloud & DevOps Module 1
Lessons 01–02: think about a single image
docker builddocker runLesson 03: think about a system
┌─────────┐ ┌────────┐
│ api │────▶│postgres│
└────┬────┘ └────────┘
│
┌────▼────┐ ┌────────┐
│ worker │────▶│ redis │
└─────────┘ └────────┘
docker run four times?docker run needs its own -e flags repeated everywhereservices:
api:
build: ./api
ports: ["8000:8000"]
env_file: ./.env
depends_on: [postgres]
worker:
build: ./worker
env_file: ./.env
depends_on: [postgres, redis]
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: ["pgdata:/var/lib/postgresql/data"]
redis:
image: redis:7-alpine
volumes:
pgdata:
docker compose up)<projectdir>_default)postgres, redis, api, worker/etc/hosts editing requiredports: mapping neededapi needs to expose a port to the host (8000:8000) so your browser can reach ithttp://api:8000/health works from inside any container in the stack.
environment: keysenv_file: ./.env.env file is in .gitignoreenvironment: KEY: ${KEY}env_file when both define the same key.env file patternA file literally named .env in the same directory as docker-compose.yml. Compose reads it automatically.
POSTGRES_PASSWORD=devpassword
DATABASE_URL=postgresql://urls:devpassword@postgres:5432/urls
REDIS_URL=redis://redis:6379/0
BASE_URL=http://localhost:8000
.env is in .gitignore. There is a separate .env.example (committed) that documents the required vars without their secret values.
depends_on is weakdepends_on: [postgres] means “start postgres first” — NOT “wait until postgres is ready to accept connections”ECONNREFUSED and crashescondition: service_healthy + healthcheck, or a wait-for-it entrypoint scriptThe best answer is application-level retry — it works everywhere, not just in compose.
postgres:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
api:
depends_on:
postgres:
condition: service_healthy
condition: service_healthy makes api literally wait until pg_isready succeedsstarting → healthy / unhealthyhealthy is reached| Kind | Syntax | Use for |
|---|---|---|
| Named volume | pgdata:/var/lib/... | DB data, anonymous persistent storage |
| Bind mount | ./api:/app | Dev: live code reload from host |
compose down-v to compose down to also remove volumes (destroys DB data)api:
build: ./api
volumes: ["./api:/app"] # bind-mount source
command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
--reloadapi/app.py on your laptop are picked up by uvicorn inside the containerThe production image stays immutable. This override is dev only — put it in docker-compose.override.yml (Homework Task 3).
docker compose up lifecycle| Command | What it does |
|---|---|
up | Build (if needed) + start in foreground |
up -d | Detach (run in background) |
up --build | Force rebuild before starting |
logs -f api | Follow one service’s logs |
exec api /bin/sh | Shell into a running container |
restart worker | Restart one service |
down | Stop + remove containers (volumes kept) |
down -v | Also remove volumes |
ps | List running services |
sample-app/ after this lessonAfter Lesson 03: cloud-devops/sample-app/docker-compose.yml exists; docker compose up brings api + worker + postgres + redis online with one command; .env.example documents required env vars; the API reads DATABASE_URL from the environment.
docker-compose.yml.env.exampleapi/app.py — reads DATABASE_URL from env (even if still using in-memory storage in Plan A)docker compose up is one commandcondition: service_healthy beat plain depends_on for real readiness