Docker How To Use

Docker How To Use

Reading time1 min
#Docker#DevOps#Containers#DockerCompose#Microservices#Orchestration

Mastering Docker Compose: Streamlined, Reliable Multi-Container Management

Scaling a development environment to match modern microservices architecture rarely involves a single container. Managing three, five, or a dozen interdependent services via docker run and shell scripts quickly devolves into chaos—especially when you need to replicate it somewhere else or automate it in CI.

Docker Compose solves this by allowing you to declare service graphs, persistent volumes, and networking policies in a single docker-compose.yml. No custom scripts for startup sequencing. No inconsistencies across environments.


Problem: Juggling Containers and Hidden Dependencies

A typical use case: Node.js API, MongoDB, and perhaps a Redis cache. Developers often try launching each service as needed:

docker run -d --name mongo -p 27017:27017 mongo:5.0
docker run -d --name api --link mongo -v $(pwd):/app -p 3000:3000 node:14 npm start

Issues appear fast:

  • Link flags (--link) are deprecated—networking becomes brittle.
  • Build steps for the API are inconsistently applied.
  • Restart order isn’t guaranteed.
  • Data persists only until the next docker rm.

When this workflow gets tangled, deployments and onboarding collapse under their own weight.


The Compose File: Defining the State

Consider this minimal Compose configuration for a basic two-service stack:

version: '3.9'

services:
  api:
    image: node:14
    working_dir: /app
    volumes:
      - ./src:/app
    command: sh -c "npm ci && npm run start"
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
    depends_on:
      - mongo

  mongo:
    image: mongo:5.0
    restart: unless-stopped
    ports:
      - "27017:27017"
    volumes:
      - mongo-data:/data/db

volumes:
  mongo-data:

Key points:

  • depends_on lists service order, but note: it doesn’t guarantee the service is ready, only started. Use custom healthchecks for robust dependency management.
  • Mounting ./src allows local code changes to sync within the API container; discouraged in production for security/performance.
  • Persistent MongoDB data with mongo-data—critical if you don’t want to lose state between cycles.

Execution Lifecycle

Standard workflow:

  1. Startup

    docker-compose up --build
    

    Includes image build step for local Dockerfiles if present.

  2. Detached mode

    docker-compose up -d
    
  3. Check state

    docker-compose ps
    
  4. Streaming logs

    docker-compose logs -f api
    
  5. Graceful teardown

    docker-compose down -v
    

    With -v, volumes (including your database) are purged—non-recoverable. Don’t use this in production.

Common gotcha: Compose networks are reused between runs unless explicitly removed; stale networks can cause confusing DNS resolution issues.


Advanced Patterns and Troubleshooting

Healthchecks for Real Dependency Control

Out of the box, depends_on doesn’t wait for services to be ready. For mission-critical startup order:

services:
  mongo:
    image: mongo:5.0
    healthcheck:
      test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    ...
    depends_on:
      mongo:
        condition: service_healthy

This avoids race conditions where the API fails because MongoDB isn't accepting connections yet.


Overlay Files for Environment Variation

Compose supports stacking config files:

  • Base: docker-compose.yml
  • Override for production: docker-compose.prod.yml

Prod-specific tweaks (e.g., scaling services, disabling debug ports) belong in the override file. Example run:

docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Using Environment Variables

Self-documenting .env files reduce hard-coding and ease portability:

API_PORT=3000
MONGO_PORT=27017

Reference directly in docker-compose.yml:

ports:
  - "${API_PORT}:3000"

Note: Environment resolution happens at run-time, not parse-time, which can cause hard-to-spot configuration bugs.


Custom Networks

For multi-tier apps, restricting service visibility is essential.

networks:
  frontend:
  backend:

services:
  api:
    networks: [frontend, backend]
  mongo:
    networks: [backend]

API bridges both networks. MongoDB, isolated, accessible only by API.


Practical Example: Production-Ready WordPress–MySQL Stack

version: '3.9'

services:
  wordpress:
    image: wordpress:6.2.1-php8.2-apache
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: siteuser
      WORDPRESS_DB_PASSWORD: examplepassword123
      WORDPRESS_DB_NAME: wpdb
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: supersecretroot
      MYSQL_DATABASE: wpdb 
      MYSQL_USER: siteuser 
      MYSQL_PASSWORD: examplepassword123 
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5
    restart: unless-stopped

volumes:
  db_data:

Accessing http://localhost:8000 after startup yields a fully initialized CMS, MySQL volume persistent on host disk. Downside: updating the MySQL image can corrupt data if schema changes occur—snapshot volumes before major upgrades.

Typical error on container startup if env vars are missing:

db_1         | ERROR 1045 (28000): Access denied for user 'siteuser'@'%' (using password: YES)

Compose in CI/CD and Beyond

Direct integration with CI tools (GitHub Actions, GitLab CI) allows ephemeral test environments with a single command, e.g.:

# .github/workflows/test.yml (excerpt)
- name: Start test stack
  run: docker-compose -f docker-compose.test.yml up -d

By referencing version-pinned images, you gain repeatability in tests, avoiding “works on my machine” surprises.


Trade-Offs and Alternatives

Compose is not Kubernetes. For high-availability, automatic self-healing, and advanced rolling updates, orchestrators like Kubernetes or Docker Swarm are more suitable. Still, Docker Compose remains ideal for localized development, testing, and certain monolithic legacy deployments.


Non-Obvious Tip

Compose files can include build contexts targeting multiple architectures:

api:
  build:
    context: .
    platforms:
      - linux/amd64
      - linux/arm64

Useful for projects meant to run on both Intel and ARM hosts—a common scenario as teams use M-series Macs.


Conclusion

Declarative Compose files increase confidence when rebuilding, sharing, or scaling development environments. Investing time in robust Compose configurations pays off with reproducibility, easier collaboration, and faster onboarding—saving yourself from shell script debt.

In practice, revisit your stack definitions regularly. Unexpected upgrade headaches or restart failures typically arise from drift between Compose files and actual container state.

Caveat: There’s no one-size-fits-all Compose setup. Sometimes you’ll need custom entrypoints, special logging, or manual network tweaks. Expect to iterate.


Further reading: Consider integrating docker-compose into Makefiles or CI pipelines for seamless multi-environment workflows. Questions about unusual service dependencies or advanced scaling? Open an issue or suggest a topic for a focused deep-dive.