</>StackKit
</>StackKit

Developer tutorials & guides

Docker Compose: Run Multi-Container Applications with One Command

Learn Docker Compose from scratch. Define, run, and manage multi-container apps — databases, backends, and frontends — with a single docker compose up.

N

Nitheesh DR

Founder & Full-Stack Engineer

9 min read796 words
#docker#docker-compose#devops#containers#postgresql

What Is Docker Compose?

Docker Compose is a tool for defining and running multi-container applications. Instead of running multiple docker run commands with long flags you can never remember, you write a single docker-compose.yml file that describes your entire stack, then start everything with:

docker compose up

That's it. One command spins up your app, database, cache, and anything else — fully networked, with volumes mounted.


Why Docker Compose?

Running a typical web app requires at least:

  • Your application server
  • A database (PostgreSQL, MySQL, MongoDB)
  • Maybe a cache (Redis)
  • Maybe a reverse proxy (Nginx)

Without Compose, you'd run four separate docker run commands, manually create a network, and hope you remembered all the environment variables. With Compose, all of that is declared once in a file and repeatable.


Your First docker-compose.yml

version: "3.9"

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
    depends_on:
      - db

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

volumes:
  postgres_data:

What this does:

  • app — builds from your local Dockerfile, maps port 3000, waits for db to start
  • db — runs PostgreSQL 16, sets credentials via env vars, persists data in a named volume
  • Both services share a private network automatically

Key Compose Concepts

Services

Each container is a service. Services are the main thing you configure in Compose.

Networks

By default, Compose creates a network for your project and all services join it. Services can reach each other by service name:

// In your app, connect to the database like this:
const db = new Client({ host: "db", port: 5432 });
// "db" resolves to the postgres container's IP

Volumes

Volumes persist data between container restarts. Without a volume, stopping the database container loses all your data.

volumes:
  - postgres_data:/var/lib/postgresql/data  # named volume (persisted)
  - ./src:/app/src                           # bind mount (local files)

Environment Variables

Pass config via environment or reference an .env file:

services:
  app:
    env_file:
      - .env
# .env
DATABASE_URL=postgresql://postgres:password@db:5432/myapp
SECRET_KEY=supersecret

Full Stack Example: Node + PostgreSQL + Redis

version: "3.9"

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:secret@db:5432/appdb
      - REDIS_URL=redis://cache:6379
    volumes:
      - .:/app
      - /app/node_modules
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    command: npm run dev

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

  cache:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Essential Docker Compose Commands

# Start all services (detached = in background)
docker compose up -d

# View running services
docker compose ps

# View logs
docker compose logs -f          # all services
docker compose logs -f api      # specific service

# Run a command inside a running container
docker compose exec api sh
docker compose exec db psql -U postgres

# Stop all services (keeps volumes)
docker compose down

# Stop and remove volumes (WARNING: deletes data)
docker compose down -v

# Rebuild images (after Dockerfile changes)
docker compose build
docker compose up -d --build

# Restart a single service
docker compose restart api

# Scale a service
docker compose up -d --scale api=3

Development vs Production Compose

Use multiple Compose files to override settings per environment:

docker-compose.yml (base):

services:
  api:
    image: myapp/api:latest
    environment:
      - NODE_ENV=production

docker-compose.override.yml (development — auto-loaded):

services:
  api:
    build: .
    volumes:
      - .:/app
    environment:
      - NODE_ENV=development
    command: npm run dev

docker compose up automatically merges both files in development.

For production: docker compose -f docker-compose.yml up -d


Health Checks

Ensure services are actually ready (not just started) before dependent services connect:

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

api:
  depends_on:
    db:
      condition: service_healthy  # wait until pg_isready passes

Common Gotchas

Port already in use: Another process is using the port. Either stop it or change the host port mapping ("3001:3000").

Volume permission errors: On Linux, files created by containers may be owned by root. Fix with user: "${UID}:${GID}" in the service config.

Can't connect to database: The app starts before Postgres is ready. Use health checks with depends_on: condition: service_healthy.

Changes not reflected: Rebuilt image not used. Run docker compose up -d --build to force a rebuild.


Conclusion

Docker Compose transforms a complex multi-container setup into a single declarative file and a single command. For local development, it eliminates "works on my machine" problems by making your environment reproducible. For small to medium production deployments, it's a practical alternative to Kubernetes that's far simpler to operate.

Tagged

#docker#docker-compose#devops#containers#postgresql
N

Written by

Nitheesh DR

Founder & Full-Stack Engineer

Nitheesh is a full-stack software engineer based in Tamil Nadu, India, with hands-on experience building production SaaS applications using Next.js, TypeScript, React, Node.js, and cloud infrastructure. He founded StackKit to share the practical knowledge he uses every day — not just theory, but the real-world techniques that help developers ship better software faster.