본문으로 건너뛰기
Previous
Next
Docker Compose Tutorial for Beginners

Docker Compose Tutorial for Beginners

Docker Compose Tutorial for Beginners

이 글의 핵심

Docker Compose tutorial: services, networks, volumes, depends_on, and env files—run a Node app with MongoDB and Nginx from one docker-compose.yml. Full working examples, common pitfalls, and production tips.

What is Docker Compose?

Running a modern web application usually means running several processes: an API server, a database, a cache, maybe a reverse proxy. You could start each with a long docker run command — but that gets unwieldy fast, requires remembering port mappings and volume paths, and is hard to share with teammates.

Docker Compose solves this by letting you describe your entire stack in a single docker-compose.yml file. One command (docker compose up) starts everything; another (docker compose down) tears it all down cleanly.

your-project/
├── docker-compose.yml    ← the entire stack in one file
├── Dockerfile            ← how to build your app image
├── .env                  ← secrets and env vars (git-ignored)
├── app.js
└── package.json

This tutorial walks through the mental model, a complete working example, and the commands you’ll use every day.


Core Concepts

Understanding four concepts covers 90% of Compose:

ConceptWhat it is
ServiceOne container definition: which image to use (or build), ports to expose, environment variables, volumes, restart policy
NetworkA private virtual network Compose creates for your project. Services communicate by service name, not by IP
VolumePersistent storage that survives container restarts. Database data goes here
depends_onDeclares start order. Does NOT wait for readiness — use health checks for that

Why service names instead of localhost?

Inside a container, localhost means that container’s own loopback. The database is in a separate container. Compose creates a DNS name for each service so they can reach each other:

# From inside the 'app' container:
mongodb://mongo:27017/mydb      ← 'mongo' resolves to the database container
redis://cache:6379              ← 'cache' resolves to the Redis container
http://nginx:80                 ← 'nginx' resolves to the proxy container

The Dockerfile

Compose often builds your API image from a Dockerfile rather than pulling a pre-built one. A minimal Node.js Dockerfile:

# Dockerfile
FROM node:20-alpine

WORKDIR /app

# Copy dependency manifests first (cache layer)
COPY package*.json ./
RUN npm ci --only=production

# Copy source
COPY . .

EXPOSE 3000
CMD ["node", "app.js"]

Always add a .dockerignore — without it, every docker compose up --build copies node_modules and .git into the image, slowing builds dramatically:

# .dockerignore
node_modules
.git
.env
*.log
dist
coverage

A Complete docker-compose.yml

Here is a three-service stack: Node.js API, MongoDB database, and Nginx reverse proxy.

# docker-compose.yml
services:
  app:
    build: .                        # Build from ./Dockerfile
    ports:
      - "3000:3000"                 # host:container
    environment:
      NODE_ENV: production
      PORT: 3000
      MONGODB_URI: mongodb://mongo:27017/mydb
    env_file:
      - .env                        # Load secrets from .env
    depends_on:
      - mongo
    restart: unless-stopped
    volumes:
      - ./logs:/app/logs            # Bind mount for log files

  mongo:
    image: mongo:7
    ports:
      - "27017:27017"               # Expose for local tools (e.g. Compass)
    volumes:
      - mongo-data:/data/db         # Named volume — data survives restarts
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro   # :ro = read-only
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  mongo-data:      # Named volume declaration — Compose manages the storage

The .env file (never commit this):

# .env
MONGO_USER=admin
MONGO_PASSWORD=supersecretpassword
JWT_SECRET=another-secret

The Nginx Configuration

With Nginx proxying to the app container, your Node.js server is never exposed directly:

# nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream app {
        server app:3000;    # 'app' is the service name
    }

    server {
        listen 80;
        server_name _;

        location / {
            proxy_pass http://app;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_cache_bypass $http_upgrade;
        }
    }
}

The Node.js App

The app connects to MongoDB using the MONGODB_URI environment variable Compose injects:

// app.js
const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// Connect using the service name from docker-compose.yml
mongoose.connect(process.env.MONGODB_URI)
    .then(() => console.log('Connected to MongoDB'))
    .catch(err => console.error('MongoDB connection error:', err));

const Item = mongoose.model('Item', new mongoose.Schema({
    name: String,
    createdAt: { type: Date, default: Date.now }
}));

app.get('/health', (req, res) => {
    res.json({ status: 'ok', uptime: process.uptime() });
});

app.get('/items', async (req, res) => {
    const items = await Item.find().sort({ createdAt: -1 }).limit(20);
    res.json(items);
});

app.post('/items', async (req, res) => {
    const item = await Item.create({ name: req.body.name });
    res.status(201).json(item);
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Listening on port ${PORT}`));

Daily Commands

# Start all services in the background
docker compose up -d

# Start and rebuild images (after Dockerfile changes)
docker compose up -d --build

# View logs from all services
docker compose logs -f

# View logs from one service
docker compose logs -f app

# List running services
docker compose ps

# Run a one-off command inside a running container
docker compose exec app node -e "console.log('hello')"
docker compose exec mongo mongosh

# Stop containers (volumes preserved)
docker compose down

# Stop containers AND remove named volumes (database data deleted!)
docker compose down -v

# Restart one service
docker compose restart app

# Scale a service (if no host port conflict)
docker compose up -d --scale app=3

Networks in Depth

By default, Compose creates one network named <project>_default where <project> is the directory name. Every service joins it automatically.

You can define multiple networks to isolate services:

services:
  app:
    networks:
      - frontend
      - backend

  nginx:
    networks:
      - frontend      # nginx can reach app, but NOT mongo directly

  mongo:
    networks:
      - backend       # only app can reach mongo

networks:
  frontend:
  backend:

This mirrors real-world security: the database is only accessible from the application tier.


Volumes in Depth

Two types of volumes:

Named Volumes (preferred for databases)

volumes:
  mongo-data:     # declares the volume

services:
  mongo:
    volumes:
      - mongo-data:/data/db   # mounts it

Docker manages the storage location. The data persists across docker compose down / docker compose up. Only docker compose down -v removes it.

Bind Mounts (useful for development)

services:
  app:
    volumes:
      - .:/app          # mirrors your local source into the container
      - /app/node_modules  # exclude node_modules from the mirror

Bind mounts let you edit code locally and see changes immediately (when using a tool like nodemon). They are not recommended for production because they couple the container to the host filesystem layout.


Health Checks

depends_on only controls start order — it does not wait for MongoDB to be ready to accept connections. Without health checks, the app container often starts before Mongo is ready and crashes:

services:
  mongo:
    image: mongo:7
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s     # grace period before checks count

  app:
    depends_on:
      mongo:
        condition: service_healthy    # wait for the health check to pass

For PostgreSQL, the test is typically pg_isready:

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
  interval: 10s
  timeout: 5s
  retries: 5

Override Files for Environments

Instead of one file for everything, split configuration across override files:

docker-compose.yml          ← base config (shared by all environments)
docker-compose.dev.yml      ← development overrides (bind mounts, nodemon)
docker-compose.prod.yml     ← production overrides (replicas, resource limits)
# docker-compose.dev.yml
services:
  app:
    command: npx nodemon app.js    # hot reload in development
    environment:
      NODE_ENV: development
    volumes:
      - .:/app                     # bind mount for live editing
      - /app/node_modules
# Development
docker compose -f docker-compose.yml -f docker-compose.dev.yml up

# Production (just the base file, or with prod overrides)
docker compose up -d

Common Pitfalls

Container can’t connect to the database

MongoNetworkError: failed to connect to server [localhost:27017]

Cause: using localhost in the connection string instead of the service name. Fix: use mongodb://mongo:27017/mydbmongo is the service name, not localhost.

Port already in use

Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:27017

Cause: another process (or another Compose project) is using port 27017. Fix: either stop the other process or change the host port mapping ("27018:27017").

App crashes on startup before DB is ready

Cause: depends_on does not wait for the database to accept connections. Fix: add a health check to the database service and use condition: service_healthy in depends_on.

Changes not reflected after rebuild

Cause: running docker compose up -d without --build uses the cached image. Fix: docker compose up -d --build after any Dockerfile or dependency changes.

Database data lost on docker compose down

Cause: using docker compose down -v removes named volumes. Fix: use plain docker compose down (no -v) to preserve data. Reserve -v for intentional resets.


Resource Limits

In production, set memory and CPU limits to prevent runaway containers from starving neighbors:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'       # max 50% of one CPU core
          memory: 512M
        reservations:
          memory: 256M      # guaranteed minimum

Note: deploy.resources is read by docker compose since Compose v2. For Swarm mode, it’s also used for scheduling.


Key Takeaways

  • One docker-compose.yml defines your entire stack — services, networks, and volumes in one place
  • Service names are DNS names — connect from app to mongo:27017, never localhost:27017
  • Named volumes persist database data across restarts; docker compose down -v deletes them
  • depends_on is not enough for readiness — add health checks and condition: service_healthy
  • Use .env for secrets — never hard-code credentials in docker-compose.yml
  • Override files keep dev and prod config separate without duplication
  • docker compose up -d --build after Dockerfile changes; plain up -d uses the cache
  • Network isolation via multiple named networks: expose only what each tier needs to see

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Docker Compose tutorial: services, networks, volumes, depends_on, and env files—run a Node app with MongoDB and Nginx fr… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Docker, Docker Compose, Tutorial, Node.js, MongoDB, DevOps 등으로 검색하시면 이 글이 도움이 됩니다.