Back to blog

Docker Compose Basics: Run Multi-Container Apps with One Command

dockerdocker-composedevopscontainerstutorial
Docker Compose Basics: Run Multi-Container Apps with One Command

In the previous post, you learned how to write a Dockerfile and build your own images. But real apps don't run in isolation — they need a database, maybe a cache, maybe a background worker. Running each one with a separate docker run command gets unwieldy fast.

That's where Docker Compose comes in.

What is Docker Compose?

Docker Compose is a tool for defining and running multi-container Docker applications. Instead of typing long docker run commands for each service, you declare everything in a single docker-compose.yml file — and spin up the entire stack with one command:

docker compose up

That's it. App, database, cache — all running together.

Docker Compose is especially well-suited for development and testing environments. Commit the docker-compose.yml to your repo and every teammate can get the full stack running without installing PostgreSQL, Redis, or anything else on their machine.


The docker-compose.yml Structure

Here's what a typical Compose file looks like:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
    depends_on:
      - db
    volumes:
      - .:/app
 
  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - db_data:/var/lib/postgresql/data
 
volumes:
  db_data:

Two services: app (your Node.js application) and db (PostgreSQL). One named volume to persist database data. Let's break down each field.


The Key Fields

services

The top-level block that lists all the containers you want to run. Each key under services is the container's name — this name also becomes its hostname on the internal Docker network.

image

Use a pre-built image from Docker Hub (or local registry) instead of building from source.

image: postgres:16
image: redis:alpine
image: nginx:latest

build

Tells Docker to build an image from a Dockerfile rather than pulling one. Points to the directory containing the Dockerfile.

build: .               # Use Dockerfile in current directory
build: ./backend       # Use Dockerfile in the backend/ directory

ports

Maps container ports to your host machine. Format: "host:container".

ports:
  - "3000:3000"   # localhost:3000 → container port 3000
  - "5432:5432"

environment

Sets environment variables inside the container. Supports both list and map formats:

# List format
environment:
  - NODE_ENV=production
  - PORT=3000
 
# Map format
environment:
  POSTGRES_USER: admin
  POSTGRES_PASSWORD: secret

volumes

Two types of volume mounts:

volumes:
  - .:/app                              # Bind mount — sync local code into container
  - db_data:/var/lib/postgresql/data    # Named volume — persist data across container restarts

Bind mounts (.:/app) link a host directory directly into the container. Change a file locally — the container sees it immediately. No rebuild needed. Perfect for development.

Named volumes (db_data:...) are managed by Docker and persist even when the container is removed. Essential for database data you don't want to lose.

depends_on

Controls startup order — this service starts after the listed services.

depends_on:
  - db
  - redis

Note: depends_on only waits for the container to start, not for the service inside to be ready. For databases, the app might start before PostgreSQL finishes initializing. For production-grade setups, use health checks or a retry loop in your app.


Essential Commands

# Start the entire stack in the background
docker compose up -d
 
# View logs for all services
docker compose logs -f
 
# View logs for a specific service
docker compose logs -f app
 
# Stop all containers
docker compose down
 
# Stop and remove everything: containers, volumes, images
docker compose down -v --rmi all
 
# Rebuild images then start (use after changing Dockerfile)
docker compose up -d --build
 
# Run a command inside a running container
docker compose exec app sh
 
# Check status of all services
docker compose ps

Real Example: Node.js + PostgreSQL

Here's a complete setup for a Node.js app connected to PostgreSQL:

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://admin:secret@db:5432/mydb
    depends_on:
      - db
    volumes:
      - .:/app
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - db_data:/var/lib/postgresql/data
 
volumes:
  db_data:
docker compose up -d
# App running at http://localhost:3000
# PostgreSQL running at localhost:5432

One important detail: services in the same docker-compose.yml can reach each other using the service name as hostname. Notice the DATABASE_URL uses @db:5432 — not @localhost:5432. The app container connects to the db service by its name, because Docker Compose puts all services on a shared internal network automatically.


Summary

✅ Docker Compose manages multi-container apps from a single docker-compose.yml
docker compose up -d starts the entire stack; docker compose down tears it down
✅ Use build: to build from a Dockerfile, image: to pull a pre-built image
✅ Bind mounts (.:/app) sync local code into containers — no rebuild on code changes
✅ Named volumes persist data across container restarts — essential for databases
✅ Services connect to each other using the service name as hostname, not localhost


Series: Docker & Kubernetes Learning Roadmap
Previous: Writing a Dockerfile: Build Your Own Docker Images
Next: Docker Networking & Volumes


Next up: a deeper look at how Docker networking and volumes work under the hood.

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.