본문으로 건너뛰기
Previous
Next
Docker Compose: Node API, PostgreSQL, Redis in One Stack

Docker Compose: Node API, PostgreSQL, Redis in One Stack

Docker Compose: Node API, PostgreSQL, Redis in One Stack

이 글의 핵심

Production-style Docker Compose for Node.js: define API, PostgreSQL, and Redis in one stack with docker-compose.yml, env, health checks, volumes, and restart policies.

Introduction

Docker Compose declares multiple containers as one project so docker compose up can align local and staging. A Node.js API behind PostgreSQL and Redis is a very common stack; defining it with health checks, volumes, and restart policies makes failures easier to recover from and automation simpler to wire. Production still needs secret management, resource limits, log drivers, and network isolation. This post centers on a runnable compose template plus an operations checklist on top.

After reading this post

  • Patterns for docker-compose.yml that start API · Postgres · Redis in dependency order
  • Environment separation, health checks, and named volumes for data and cache
  • How to avoid common pitfalls (hostname, port binding, migration timing)

Table of contents

  1. Concepts: Compose and production stacks
  2. Hands-on: docker-compose.yml template
  3. Advanced: resources, secrets, profiles
  4. Performance: volumes and connection pools
  5. Real-world cases
  6. Troubleshooting
  7. Wrap-up

Concepts: Compose and production stacks

Basics

  • Service: unit of execution (e.g. api, db, redis)—image, command, ports, environment.
  • Network: on the default bridge, service names resolve as DNS (api connects to postgres at postgres:5432).
  • Volume: persists data across container recreation (e.g. Postgres data directory).
  • Healthcheck: with depends_on condition: service_healthy, the app can start after dependencies are ready (Compose v2+).

Why it matters

Locally you can bind ports on localhost, but if DB version and Redis config differ per developer, you get “cannot reproduce” bugs. Compose pins versions, same network, same env in code.

Hands-on: docker-compose.yml template

Example project root. Never commit real secrets in .env—use CI/CD secrets or server env files.

.env.example (commit to the repo)

# .env.example — copy to .env
NODE_ENV=production
POSTGRES_USER=app
POSTGRES_PASSWORD=change_me
POSTGRES_DB=appdb
DATABASE_URL=postgresql://app:change_me@postgres:5432/appdb
REDIS_URL=redis://:redis_secret@redis:6379/0

docker-compose.yml

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: myorg/api:${IMAGE_TAG:-latest}
    restart: unless-stopped
    ports:
      - "${API_PORT:-3000}:3000"
    environment:
      NODE_ENV: ${NODE_ENV:-production}
      DATABASE_URL: ${DATABASE_URL}
      REDIS_URL: ${REDIS_URL}
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 40s
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: >
      redis-server
      --appendonly yes
      --requirepass ${REDIS_PASSWORD}
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10
volumes:
  pgdata:
  redisdata:

Notes

  • depends_on + condition: service_healthy: API starts after DB and Redis are ready.
  • Named volumes pgdata, redisdata: data survives container removal (docker compose down -v deletes them—be careful).
  • Redis AOF (appendonly yes) improves durability a bit (add RDB snapshots if required).

API /health endpoint (Node.js example)

// src/health.js — Express example
import express from 'express';
export function healthRouter() {
  const r = express.Router();
  r.get('/health', (_req, res) => {
    res.status(200).json({ status: 'ok', ts: Date.now() });
  });
  return r;
}

Migration timing

Often run migrations once after DB connect in the app startup script.

{
  "scripts": {
    "migrate": "node scripts/migrate.js",
    "start": "node dist/index.js"
  }
}

Prefer sequential execution in code over npm run migrate && npm run start to reduce shell coupling.

Advanced: resources, secrets, profiles

Resource limits (production-oriented)

    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M

deploy is Swarm-oriented; on plain Compose you may use other keys or move limits to Kubernetes/Nomad. On a single host, check Compose docs for cgroup compatibility.

Profiles for dev-only services

# docker-compose.dev.yml
services:
  adminer:
    image: adminer
    profiles: [dev]
    ports:
      - "8080:8080"
docker compose --profile dev up

Secrets (Swarm or external vault)

On a single server, file-based env with chmod 600 is practical. On Kubernetes, use Secret resources.

Performance: volumes and connection pools

TopicRecommendationNotes
Postgres volumenamed volumeHost bind has OS-specific perf/permission issues
Connection poolcap max in the appcontainers × pool size ≤ DB max_connections
Redismaxmemory + policye.g. allkeys-lru for cache-only
LogsJSON driver or rotationavoid disk fill from container logs
Trade-off: restart: always is convenient but can loop forever if the service is broken—pair with health checks and alerts.

Real-world cases

  • Staging = same compose as prod: reuse the file with STAGING_* variables only.
  • CI compose tests: docker compose up -d, smoke tests, docker compose down—see GitHub Actions post.
  • Nginx in front: TLS and reverse proxy in Nginx setup post.

Troubleshooting

SymptomCauseFix
API cannot reach DBusing localhostDB host must be the postgres service name
password authentication failed.env mismatchsync DATABASE_URL with Postgres env
Health check never passesmissing /health or wrong portcheck path, port, wget/curl in image
Redis NOAUTHpassword mismatchalign REDIS_URL and requirepass
Disk fulllogs / AOF growthlog rotation, Redis maxmemory
Debug: docker compose logs -f api postgres redis; docker compose exec postgres psql -U app -d appdb for DB-only checks.

Wrap-up

  • Compose defines API · Postgres · Redis together for reproducibility and faster onboarding.
  • Health checks, volumes, restart policies are close to mandatory near production.
  • Next: Nginx reverse proxy in front of Node.js for public endpoints and TLS.

자주 묻는 질문 (FAQ)

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

A. Production-style Docker Compose for Node.js: define API, PostgreSQL, and Redis in one stack with docker-compose.yml, env… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


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

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


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

Node.js, Docker, Docker Compose, PostgreSQL, Redis, Container, Production 등으로 검색하시면 이 글이 도움이 됩니다.