Intro

dockercompose Allows managing containers, networks, port-mappings, environment variables, run flags etc. Very useful for multi container systems. It defines every service, network, and volume in a yaml file named docker-compose.yaml.

Compose builds a custom network automatically for all the services with DNS resolution.

Docker Compose is separate from the Docker daemon but check if it's already installed since it's part of the toolbox

When’s It Needed?

Simply whenever your containerized app relies on multiple interconnected services that also run in their own containers. Example is a web server that needs a database, and maybe some memory-caching service like Redis.

Other Cases

  • Development Environments: really simplifies local setup by starting the entire project stack with one command
  • Automated Testing: It is ideal for creating and destroying isolated environments quickly for CI/CD pipelines or end-to-end testing.
  • Version-Controlled Infrastructure: Since infrastructure’s defined in YAML file, changes can be tracked by VCS like and shared

Basic Commands

  • docker compose up to spin it all to life
  • docker compose down to shut it

How It Works

As mentioned, Docker Compose relies on docker-compose.yaml to determine what to do in order to run the containers comprising the app.
It works in all environments, including CI/CD pipelines, but is more suited fro dev and testing.

Docker-Compose File

The YAML file has several parts and parameters. I’ll go through them by order and importance.

  • version this defines the Docker Compose version and is at the very top of the file.
  • services: a nested paramer that defines the different services that each run in their own container
  • volumes: define persistent data storage volumes (shared) for your containers to maintain data

Here’s an example file from the Docker Curriculum:

version: "3"
services:
  es:
    image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
    container_name: es
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
    volumes:
      - esdata1:/usr/share/elasticsearch/data
  web:
    image: yourusername/foodtrucks-web
    command: python3 app.py
    depends_on:
      - es
    ports:
      - 5000:5000
    volumes:
      - ./flask-app:/opt/flask-app
volumes:
  esdata1:
    driver: local

Notice how the services are named.
Each service must have an image parameter to determine its container’s image…
Whereas the depends_on parameter for the web service, tells docker to start the es container before web.
Check out the online reference for all the parameters.

Environment variable from .env

If your variables are in .env you can add the following to a service’s config:

services:
	web:
	#...all other config fields
	environment:
      - DB_HOST=${DB_HOST}
      - DB_USER=${DB_USER}
      # Or inject the whole file:
    env_file:
      - .env

Real Secrets

For secrets like DB passwords and TLS certificates, use the secrets field:

services:
  db:
    image: postgres
    secrets:
      - db_password
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
 
secrets:
  db_password:
    file: ./dev_password.txt

[!.tip] Many official Docker images support a _FILE suffix
Telling the application to read the secret from a mounted file rather than an environment variable. This is much more secure.

Architecture during Dev

As I integrated Docker Compose into my tool-set, I quickly realized I don’t know how to structure everything.
Sometimes I have multiple containerized applications, each with their own Dockerfile and Git repository. How does Docker Compose consolidate that?

Simply place the docker-compose.yaml in your project’s root directory /, wrapping all your different apps (repos)

In Prod

In production, Compose finds the Images via the image field which points to a registry. See previous exampels.

Parent Mono-repo

A Mono-repo containing the YAML file in the root and the apps in their own sub-directories.
Inside that root docker-compose.yaml, tell Docker Compose exactly where to find local Dockerfiles using a relative path via the build:context field

Sibling Repos

Each app still lives in its own sub-directory with its own Git repo and Dockerfile. However, one of them also has the Compose file.
So for the context field, it’s . for the one containing the compose file, and the relative path for the other.

Health Checks

Like mentioned before some services rely on others and the depends_on parameter tells DC this. However, it’s not enough. A more robust way to check things are healthy is to conduct a health check.

This is a block within a service that tells DC how to do so.
Take a look at this example:

services:
  web:
    build: .
    ports:
      - "${APP_PORT}:5000"
    environment:
      - REDIS_HOST=${REDIS_HOST}
      - REDIS_PORT=${REDIS_PORT}
    depends_on:
      redis:
        condition: service_healthy
 
  redis:
    image: redis:alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
  • The test parameter lists the commands to run
  • The start_peroid is the delay before DC checks, gives Redis time to spin up

To verify, simply run it with docker compose up, then should see:

[+] Running 2/2
✔ Container compose-demo-redis-1  Healthy   

Project idea

Retrofit existing (AudioReaper is Flask and Redis) or deploy a new app with a docker-compose.yml that spins up the app and its dependencies in one command.

What to learn

  • docker-compose.yml structure: services, networks, volumes, depends_on, healthchecks
  • Environment variable injection via .env files
  • Compose for local dev vs. production considerations

Resources