Capstone โ€” Productionizing a Multi-Container App

Docker Intermediate/Advanced
Chapter 9 ยท Capstone: Productionizing a Multi-Container App

๐Ÿ Capstone: Productionizing a Multi-Container App

This capstone builds a small, real Task API + database system, combining multi-stage builds (Ch.1โ€“2), Compose (Ch.3โ€“5), security hardening (Ch.6), and CI integration (Ch.7) into one production-ready setup.

The Application: A Task API + Database

A Node.js api service backed by a PostgreSQL db service โ€” small enough to stay readable here, real enough to exercise every chapter in this course.

Step 1: Multi-Stage, Optimized Build (Chapters 1โ€“2)

FROM node:18 AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build FROM node:18-slim RUN useradd --create-home appuser WORKDIR /home/appuser/app COPY --from=builder --chown=appuser:appuser /app/dist ./dist COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules USER appuser CMD ["node", "dist/server.js"]

Dependencies are copied and installed before the source (Chapter 2's cache-ordering rule), the final stage only receives compiled output and production dependencies (Chapter 1), and the container already runs as a non-root user (Chapter 6, folded in early rather than bolted on later).

Step 2: Compose With Healthchecks & Named Resources (Chapters 3โ€“5)

networks: backend: name: taskapi-backend volumes: db-data: name: taskapi-db-data services: db: image: postgres:16 networks: [backend] volumes: - db-data:/var/lib/postgresql/data healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] interval: 5s retries: 10 api: build: . networks: [backend] depends_on: db: condition: service_healthy

The database's actual data lives in a named volume (Chapter 5), the two services share an explicitly named custom network (Chapter 4), and api waits for db's healthcheck to genuinely pass (Chapter 3) rather than merely starting.

Step 3: Security Hardening (Chapter 6)

services: api: build: . read_only: true tmpfs: - /tmp environment: DATABASE_URL: ${DATABASE_URL} # from CI/host env, never hardcoded

The api container's filesystem is read-only at runtime, with /tmp mounted as tmpfs for anything genuinely needing to write. DATABASE_URL is injected from the environment, never baked into the image or the Compose file itself.

Step 4: CI Integration (Chapter 7)

# CI pipeline step docker build --cache-from myapp/api:latest -t myapp/api:$GIT_SHA -t myapp/api:latest . trivy image --exit-code 1 --severity CRITICAL myapp/api:$GIT_SHA docker push myapp/api:$GIT_SHA docker push myapp/api:latest # convenience tag only โ€” production deploys the SHA tag, never this one

Putting It All Together

Course ConceptWhere It's Used in This App
Multi-stage builds (Ch.1)Builder stage compiles; final stage ships only dist + production deps
Cache-ordering / base image choice (Ch.2)Dependencies copied before source; slim final base
Healthchecks + depends_on (Ch.3)api waits for db's real readiness, not just container start
Named networks (Ch.4)taskapi-backend โ€” isolates and connects the two services
Named volumes (Ch.5)taskapi-db-data โ€” the database's data survives container removal
Non-root, read-only, no baked-in secrets (Ch.6)appuser, read_only: true, DATABASE_URL from environment
SHA/latest tagging, registry push, scan gate (Ch.7)The full CI build/scan/push sequence

๐Ÿ’ป Coding Challenges

Challenge 1: Add a .dockerignore

Write a .dockerignore file for this capstone's build context, excluding node_modules, .git, and any local .env file.

Goal: Practice applying Chapter 2's .dockerignore guidance to this specific project.

โ†’ Solution

Challenge 2: Add an Override File for Local Development

Write a docker-compose.override.yml that bind-mounts the local source directory into api for live-reloading during development, without changing the base docker-compose.yml.

Goal: Practice combining Chapter 3's override-file pattern with Chapter 5's bind-mount use case, layered on top of this capstone's base file.

โ†’ Solution

Challenge 3: Explain the Registry Authentication Step

Extend Step 4's CI snippet with the registry login step, and explain why it must never hardcode credentials directly in the pipeline file.

Goal: Practice applying Chapter 7's credential-handling guidance to this capstone's own CI step.

โ†’ Solution

โš ๏ธ Gotcha: read_only and Persistent Data Aren't in Conflict

It can look contradictory at first glance: db writes real data to disk constantly, yet Chapter 6's read_only hardening seems to say containers shouldn't write anything. The resolution is that read_only locks down the container's own filesystem layer โ€” it says nothing about a separately mounted named volume, which remains fully writable regardless. If db here were hardened with read_only: true, its data directory would still need an explicit writable exception (either accepting that the volume mount itself isn't affected by read_only, or adding a tmpfs/volume override for the specific paths that must write) โ€” the two techniques from Chapters 5 and 6 are complementary, not contradictory, but combining them correctly requires understanding that a volume mount and the container's own filesystem are genuinely separate things.

๐ŸŽ‰ Course Complete

That's the full Docker Intermediate/Advanced course โ€” from multi-stage builds and image optimization through Compose in depth, container networking, production data management, security hardening, CI integration, and orchestration orientation, finishing with a real, production-shaped multi-container application. Together with Docker for Beginners, this completes both Docker courses on the site.