DevOps

Docker Compose: Multi-Container Apps from Scratch

Tech Setup1 min read
TS

Tech Setup

Published August 2, 2026 · Editorial policy

Docker Compose: Multi-Container Apps from Scratch

What is Docker Compose?

Docker Compose defines and runs multi-container applications. With one YAML file, you start databases, caches, web servers, and app servers — all networked together.

Installation

Docker Desktop includes Compose. Verify:

docker compose version
# Docker Compose version v2.x.x

Basic Example: Web + Database

docker-compose.yml

services:
  web:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    depends_on:
      - db
    volumes:
      - .:/app
      - /app/node_modules
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Start Everything

docker compose up -d

Essential Commands

CommandPurpose
docker compose up -dStart in background
docker compose downStop and remove containers
docker compose psList running containers
docker compose logs webView logs for service
docker compose logs -fFollow all logs
docker compose exec web shShell into container
docker compose buildRebuild images
docker compose restart webRestart specific service
docker compose stopStop without removing
docker compose pullUpdate base images

Common Patterns

Environment Variables

Option 1: env_file

services:
  web:
    env_file:
      - .env

Option 2: inline

services:
  web:
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
    env_file:
      - .env

Networks

services:
  web:
    networks:
      - frontend
  db:
    networks:
      - frontend
      - backend
  cache:
    networks:
      - backend

networks:
  frontend:
  backend:

Services on the same network can communicate by service name.

Health Checks

services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  web:
    build: .
    depends_on:
      db:
        condition: service_healthy

Volumes

services:
  db:
    volumes:
      # Named volume (persistent)
      - postgres_data:/var/lib/postgresql/data
      # Bind mount (local directory)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

volumes:
  postgres_data:

Ports

services:
  web:
    ports:
      # host:container
      - "3000:3000"
      # Bind to localhost only
      - "127.0.0.1:3000:3000"
      # Random host port
      - "3000"

Full-Stack Example

Node.js + PostgreSQL + Redis + Nginx

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      NODE_ENV: production
      DATABASE_URL: postgresql://postgres:secret@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    networks:
      - backend

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - backend

  cache:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    networks:
      - backend

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - backend

volumes:
  pgdata:

networks:
  backend:

Development vs Production

Override File

# docker-compose.yml (base)
services:
  app:
    build: .
    environment:
      NODE_ENV: development

# docker-compose.prod.yml (production overrides)
services:
  app:
    image: myregistry/app:latest
    environment:
      NODE_ENV: production
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

Commands

# Development
docker compose up -d

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

Debugging

View Logs

# All services
docker compose logs -f

# Specific service
docker compose logs -f app

# Last 100 lines
docker compose logs --tail 100 app

Inspect Network

docker network inspect tech-setup_default

Execute Commands

# Shell into running container
docker compose exec app sh

# Run one-off command
docker compose exec app node scripts/migrate.js

# Database shell
docker compose exec db psql -U postgres -d appdb

Cleanup

# Remove stopped containers
docker compose down

# Remove containers + volumes
docker compose down -v

# Remove containers + images
docker compose down --rmi all

# Prune everything
docker system prune -af

Performance Tips

  1. Use .dockerignore to exclude node_modules, .git
  2. Layer caching: copy package.json before source code
  3. Named volumes for databases (not bind mounts)
  4. Multi-stage builds for smaller production images
  5. Use --no-cache only when needed (docker compose build --no-cache)