Skip to content
SumGuy's Ramblings
Go back

Docker Compose: Orchestrating Multi-Container Applications

In the world of software development, containerization has become an indispensable paradigm. Docker, the leading containerization platform, offers a way to package applications and their dependencies into portable, lightweight units. While Docker excels at managing individual containers, real-world applications often demand cooperation between multiple components. That’s where Docker Compose steps in: a powerful tool to define and orchestrate complex, multi-container applications.

What is Docker Compose?

In essence, Docker Compose is a tool for defining and operating applications comprised of multiple Docker containers. It utilizes a YAML configuration file, typically named docker compose.yml, as a blueprint for your application. This file specifies the different services (which generally map to containers), their configuration, and how they interconnect. With a single command, docker compose up, you can bring an entire application architecture to life.

Why Use Docker Compose?

Key Concepts in Docker Compose

A Practical Example

Let’s illustrate the use of Docker Compose with a basic web application example consisting of a Python Flask backend, a Redis cache, and a MongoDB database.

services:
  web:
    build: ./web  # Build Docker image from a Dockerfile
    ports: 
      - "5000:5000"  # Expose port 5000
    depends_on:  # Establish dependencies
      - redis
      - mongo 
  redis:
    image: "redis:alpine" 
  mongo:
    image: "mongo:latest" 
    volumes:
      - mongo-data:/data/db # Map a volume for persistent data 
volumes:
  mongo-data: # Define a named volume

In this example:

Essential Docker Compose Commands

Beyond the Basics

Docker Compose offers a rich set of features for more sophisticated use cases:

Build Customization with Dockerfile ARGs

Let’s say you want to build images with environment-specific configuration:

FROM python:3.9-alpine

ARG APP_ENV="development"

WORKDIR /app

COPY requirements.txt ./
RUN pip install -r requirements.txt

COPY . ./

CMD ["python", "app.py", "--env",  $APP_ENV]
services:
  web:
    build:
      context: ./web
      args:
        APP_ENV: production

Multi-Stage Builds with Build Context

Often, you may want separate stages in your Dockerfile for development and production:

# Development Stage
FROM node:16-alpine as development
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

# Production Stage
FROM node:16-alpine 
WORKDIR /app
COPY --from=development /app/build ./build
CMD ["node", "build/index.js"]
services:
  web:
    build:
      context: ./
      target: production  # Target the final production stage

Secrets Management (Careful!)

While less ideal than true secrets management tools, sometimes you need parameters during build:

FROM python:3.9-alpine
ARG SECRET_KEY
services:
  web:
    build:
      context: ./web
      args:
        SECRET_KEY: super_secret_value 
    environment: # Less secure: consider proper secrets management
      - SECRET_KEY

Important Note: Embedding secrets directly in the docker-compose.yml is generally discouraged due to security risks. For production scenarios, use dedicated secrets management tools like Docker secrets or environment variables in conjunction with external secret stores (e.g., HashiCorp Vault).

Reducing Repetition using Yaml anchors and aliases

services:
  web:
    image: my-web-app:latest
    ports:
      - "8080:80"
    volumes:
      - ./app-data:/var/www/html
  database:
    image: postgres:12
    ports:
      - "5432:5432" 
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
services:
  web:
    image: my-web-app:latest
    ports:
      - "8080:80"
    volumes: 
      - &shared_volume ./app-data:/var/www/html 
  database:
    image: postgres:12
    ports:
      - "5432:5432" 
    volumes: 
      - *shared_volume

Explanation

Example: DRY (Don’t Repeat Yourself) with Service Templates

services:
  worker1:
    &worker_base
    build: ./worker 
    environment:
      - TASK_QUEUE=queue1 
  worker2:
    <<: *worker_base # Merge in the base configuration 
    environment:
      - TASK_QUEUE=queue2

Explanation

Benefits of Anchors and Aliases

Caveat

Be mindful when using anchors and aliases with complex structures. Overusing them can sometimes make your Compose file harder to understand. Strike a balance between conciseness and clarity.

Extending Services

Instead of completely overwriting configurations, Docker Compose allows extension for greater flexibility:

services:
  base_service:
     image: nginx:alpine
     ports:
       - "80:80"

  web:
    extends:
      file: base-compose.yml  # Could be a separate file
      service: base_service
    volumes:
      - ./web-static:/usr/share/nginx/html

2. Profiles

Profiles selectively activate services, perfect for different environments:

services:
  web:
    # ...
  monitoring:
    # ...
    profiles: [production] # Only active with '--profile production'

3. Deploying to Docker Swarm

Docker Compose integrates with Docker Swarm for cluster orchestration:

services:
  frontend:
    # ...
    deploy:  
      replicas: 5 
      update_config:
        parallelism: 2
        failure_action: rollback

4. Docker Secrets

While environment variables are helpful, secrets offer stronger security (requires Compose version 3.1+):

services:
  web:
    # ...
    secrets: 
      - DB_PASSWORD 

secrets:  
  DB_PASSWORD:
    external: true  # Secret defined externally

5. Advanced Networking

Docker Compose provides fine-grained network controls:

services:
  # ...
  networks:
    frontend:
    backend:
      driver: overlay
      ipam:
        config:
          - subnet: 172.20.0.0/16

networks:
  frontend:
  backend:
Comprehensive docker-compose.yml with many of these principles
services:
  web:
    &web_base # Base anchor
    build: 
      context: ./web
      args:
        BUILD_ENV: ${BUILD_ENV:-development} # Environment variable fallback
    image: my-web-app:latest 
    ports:
      - "8080:80"
    depends_on:
      - database 
    profiles: [development, production] # Profile based on usage

  database:
    image: postgres:12
    volumes: 
      - database-data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD} # From environment variable 
    healthcheck:  
      test: pg_isready -U postgres 
      interval: 10s

  redis:
    image: redis:alpine
    networks:
      - backend

  worker:
    <<: *web_base # Extend base service
    build: ./worker
    environment:
      - TASK_QUEUE=default
      - LOG_LEVEL=DEBUG 
    depends_on:
      - redis

  monitoring:
    image: prometheus/prometheus 
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    profiles: [production] 

volumes:
  database-data:

networks:
  frontend:
  backend:
    driver: overlay

secrets: 
  DB_PASSWORD:
    external: true

Explanation:

Anchors & Merging:

Environment Variables:

Dependencies, Profiles & Health Checks:

Secrets:

Networking:

Build and Deployment Considerations

How to Use:

Important Notes:


Share this post on:

Previous Post
SumGuy’s Guide to Linux Log Analysis
Next Post
Linux Home Lab Security: Planning for the Unexpected