Live
Tangible Certification: Turning a Kubernetes Badge into a Gold NecklaceGoogle Data Cloud GA updates: agent‑centric tooling, hybrid Spanner, and expanded Lakehouse catalogCloudflare folds Deno runtime into Workers: practical impact on serverless deploymentsManaging Copilot Code Review Costs and License Scope with New Org‑Level ControlsEdge-native multimodal decisions: Clef-omni adds audio/video to Workers AI, Clef-flash gets cheaper, and Clef inference speeds upDetecting Silent Agent Failures: Verifying Outcomes Instead of Run StatusLocal Sandboxing and Multi‑Account Support Arrive in GitHub Copilot CLI, App, and VS CodeKubernetes shifts to cgroup v2: practical steps for engineersTangible Certification: Turning a Kubernetes Badge into a Gold NecklaceGoogle Data Cloud GA updates: agent‑centric tooling, hybrid Spanner, and expanded Lakehouse catalogCloudflare folds Deno runtime into Workers: practical impact on serverless deploymentsManaging Copilot Code Review Costs and License Scope with New Org‑Level ControlsEdge-native multimodal decisions: Clef-omni adds audio/video to Workers AI, Clef-flash gets cheaper, and Clef inference speeds upDetecting Silent Agent Failures: Verifying Outcomes Instead of Run StatusLocal Sandboxing and Multi‑Account Support Arrive in GitHub Copilot CLI, App, and VS CodeKubernetes shifts to cgroup v2: practical steps for engineers
Kubernetes

Docker Compose Complete Guide: Multi-Container Apps from Scratch

Overview

This tutorial walks you through building and managing multi-container applications using Docker Compose. You'll go from a blank directory to a fully functional stack with a web app, database, and reverse proxy — all wired together with a single YAML file.

Prerequisites:

  • Docker Engine 20.10+ installed
  • Docker Compose v2 (ships with Docker Desktop or install via apt install docker-compose-plugin)
  • Basic understanding of containers and networking concepts
  • Familiarity with YAML syntax

Why this matters for the CKAD: The CKAD exam tests your ability to design and deploy multi-container pods, manage ConfigMaps and Secrets, define resource limits, and handle inter-container communication. Docker Compose mirrors these patterns — services map to containers, networks map to pod networking, and volumes map to PersistentVolumeClaims. Mastering Compose builds the mental model you need before working with Kubernetes manifests.

Getting Started

Verify your installation before writing any configuration:

docker --version
docker compose version

Create a project directory and the core file structure:

mkdir cloudninjas-app && cd cloudninjas-app
mkdir -p app nginx
touch docker-compose.yml app/Dockerfile nginx/nginx.conf

Here is the minimal docker-compose.yml to validate everything is wired up:

version: "3.9"

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

Start it and confirm it works:

docker compose up -d
curl http://localhost:8080

Stop and remove containers before moving to the full stack:

docker compose down

Building a Real Multi-Container Stack

A production-realistic stack has three concerns: application runtime, data persistence, and traffic routing. The following docker-compose.yml wires all three together.

version: "3.9"

services:

  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_NAME=cloudninjas
      - DB_USER=admin
      - DB_PASSWORD=${DB_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - backend
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: cloudninjas
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d cloudninjas"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

  proxy:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    networks:
      - backend
    restart: unless-stopped

volumes:
  db_data:

networks:
  backend:
    driver: bridge

Store secrets in a .env file — never hard-code credentials in the Compose file:

echo "DB_PASSWORD=SecurePass123!" > .env
echo ".env" >> .gitignore

The Nginx config that proxies traffic to the app service by DNS name (service name resolution is automatic on the Docker network):

# nginx/nginx.conf
server {
    listen 80;

    location / {
        proxy_pass         http://app:5000;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }
}

Common pitfall: Using depends_on with just a service name only waits for the container to start, not for the process inside it to be ready. Always pair it with a healthcheck and condition: service_healthy when your app depends on a database being fully initialized.

Volumes, Networks, and Environment Management

Named volumes persist data beyond container lifecycles. The db_data volume above survives docker compose down but is removed with:

# Remove containers AND volumes (destructive — wipes database)
docker compose down -v

Use bind mounts for development so local file changes reflect immediately inside the container:

  app:
    build: ./app
    volumes:
      - ./app:/usr/src/app        # bind mount for live reload
      - /usr/src/app/node_modules # anonymous volume to protect node_modules

For environment management, Compose supports multiple patterns. Prefer env_file for larger configurations:

  app:
    env_file:
      - .env
      - .env.local   # overrides, not committed to git

Inspect what Compose will resolve at runtime before deploying:

# Show the fully interpolated config
docker compose config

# Show environment variables for a specific service
docker compose run --rm app env

Networking best practices:

  • Define explicit named networks — avoid the default network in production stacks
  • Use multiple networks to enforce isolation: put your database on a backend network not accessible to the proxy
  • Service names are the DNS hostnames — db, app, proxy resolve automatically within the same network

A hardened two-network layout looks like this:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true   # no external internet access

services:
  proxy:
    networks: [frontend, backend]
  app:
    networks: [backend]
  db:
    networks: [backend]

Common pitfall: The internal: true flag blocks outbound internet from that network. Your app won't be able to pull external APIs if it's only on the internal network. Design accordingly.

Exam Tips for Certified Kubernetes Application Developer (CKAD)

The CKAD does not test Docker Compose directly, but the concepts translate 1-to-1 into Kubernetes primitives that are tested heavily.

Key concept mappings:

  • Compose service → Kubernetes Deployment or Pod
  • environment / env_file → ConfigMap and Secret with envFrom
  • depends_on + healthcheck → initContainers and livenessProbe / readinessProbe
  • named volume → PersistentVolumeClaim
  • bind mount → hostPath volume (use sparingly in production)
  • networks → Kubernetes NetworkPolicy
  • ports → Service of type NodePort or LoadBalancer

Common exam scenarios:

  1. Deploy a multi-container pod with a sidecar (logging, proxy) — understand how containers in the same pod share network and volumes
  2. Inject configuration via ConfigMap as environment variables or mounted files
  3. Add a readinessProbe to delay traffic until the app is healthy
  4. Use an initContainer to wait for a database before the main container starts

Quick reference commands for exam day:

# Generate a pod manifest quickly — then edit it
kubectl run myapp --image=nginx:alpine --dry-run=client -o yaml > pod.yaml

# Create a ConfigMap from a literal value
kubectl create configmap app-config --from-literal=DB_HOST=db --dry-run=client -o yaml

# Create a Secret from a literal value
kubectl create secret generic db-secret --from-literal=DB_PASSWORD=SecurePass123! --dry-run=client -o yaml

# Check pod logs for a specific container in a multi-container pod
kubectl logs pod/myapp -c sidecar-container

# Execute into a specific container
kubectl exec -it pod/myapp -c app -- /bin/sh

On the CKAD exam, use --dry-run=client -o yaml to generate base manifests instead of writing them from scratch. This saves significant time.

Summary

Key takeaways from this tutorial:

  • Docker Compose uses a single docker-compose.yml to define, network, and orchestrate multi-container stacks
  • Always use healthchecks with condition: service_healthy rather than bare depends_on for startup ordering
  • Store secrets in .env files and never commit them — reference them with ${VAR} interpolation
  • Use named volumes for persistence and internal networks to isolate your database tier
  • Every Compose concept has a direct Kubernetes equivalent — learning Compose first accelerates Kubernetes comprehension

Next steps:

  1. Convert this Compose stack into Kubernetes manifests using kompose convert
  2. Add resource limits to your Compose services (deploy.resources.limits) to practice the same constraints required in CKAD pod specs
  3. Explore Docker Compose watch mode (docker compose watch) for inner-loop development workflows

Ready to go deeper? Check out the CloudNinjas CKAD Certification Study Guide at cloudninjas.ca/certifications/ckad for full exam prep, practice labs, and mock exams aligned to the current curriculum.