Docker Compose로 Node API·PostgreSQL·Redis 한 번에 띄우기 | 프로덕션 템플릿

Docker Compose로 Node API·PostgreSQL·Redis 한 번에 띄우기 | 프로덕션 템플릿

이 글의 핵심

docker-compose로 Node API, PostgreSQL, Redis를 묶고 환경 변수·헬스체크·영속 볼륨·재시작 정책까지 프로덕션에 가깝게 구성합니다.

들어가며

Docker Compose는 여러 컨테이너를 한 프로젝트로 선언해 docker compose up 한 번에 로컬·스테이징 환경을 맞출 수 있게 합니다. Node.js API 뒤에 PostgreSQLRedis를 붙이는 구성은 실무에서 매우 흔하며, 이를 헬스체크·볼륨·재시작 정책까지 포함해 정의해두면 장애 시 자동 복구와 배포 자동화가 쉬워집니다.

다만 프로덕션에서는 비밀 번호 관리, 리소스 한도, 로그 드라이버, 네트워크 격리가 추가로 필요합니다. 이 글은 실행 가능한 compose 템플릿을 중심으로, 그 위에 얹을 운영 체크리스트를 덧붙입니다.

이미지를 빌드·배포하기 전에 Node.js 테스트GitHub Actions CI/CD로 같은 스택을 검증하는 편이 안전합니다. Kubernetes로 넘어가려면 minikube 배포를, C++·멀티 스테이지 패턴은 C++ Docker 가이드·C++ GitHub Actions와 비교해 보세요. 애플리케이션에서 DB에 붙는 방법은 Node.js 데이터베이스 연동·C++ DB 연동(libpq 등)을, 엔진 선택은 PostgreSQL vs MySQL을, 캐시 패턴은 Redis 캐싱을 함께 보시면 코드 ↔ 스택이 맞물립니다. 호스트 디스크·inode 이슈는 Linux 트러블슈팅과 겹칠 수 있습니다.

비유로 말씀드리면, Docker Compose는 악단을 한 번에 맞추는 지휘자의 점수(누가 먼저 올라오고, 어떤 순서로 연주할지)에 가깝고, 동시에 재료와 단계가 적힌 레시피북(이미지·환경·볼륨을 한 파일에 고정)이기도 합니다. 요청 흐름은 대략 클라이언트 → (호스트 포트) → api 컨테이너 → (DNS 이름 postgres/redis) → DB·캐시 순으로 이해하시면 됩니다.

이 글을 읽으면

  • API·Postgres·Redis를 의존 순서에 맞게 기동하는 docker-compose.yml 패턴을 익히실 수 있습니다
  • 환경 변수 분리, 헬스체크, named volume으로 데이터·캐시를 안전하게 다루는 요령을 파악하실 수 있습니다
  • 흔한 함정(호스트명, 포트 바인딩, 마이그레이션 타이밍)을 피하는 방법을 정리해 두었습니다

목차

  1. 개념: Compose와 프로덕션 스택
  2. 실전: docker-compose.yml 템플릿
  3. 고급: 리소스·시크릿·프로파일
  4. 성능: 볼륨과 연결 풀
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

개념: Compose와 프로덕션 스택

기본 개념

  • Service: 실행 단위(예: api, db, redis). 이미지·명령·포트·환경을 묶습니다.
  • Network: 기본 브리지 네트워크에서 서비스 이름이 DNS 이름으로 해석됩니다(apipostgrespostgres:5432로 접속).
  • Volume: 컨테이너 재생성 후에도 유지할 데이터를 저장합니다(Postgres 데이터 디렉터리 등).
  • Healthcheck: depends_oncondition: service_healthy와 함께 쓰면 준비된 뒤에 앱을 띄울 수 있습니다(Compose v2+).

왜 필요한가

로컬에서는 localhost에 각각 포트를 열어도 되지만, 팀원마다 DB 버전·Redis 설정이 달라지면 “재현 불가” 버그가 납니다. Compose는 버전 고정·동일 네트워크·동일 env를 코드로 고정합니다.


실전: docker-compose.yml 템플릿

프로젝트 루트 예시입니다. 실제 비밀번호는 .env를 Git에 넣지 말고 CI/CD 시크릿이나 서버의 env 파일로 주입하세요.

.env.example (저장소에 커밋)

# .env.example — 복사 후 .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:

주석 포인트

  • build.context / dockerfile: 이미지를 저장소의 Dockerfile로 빌드합니다. image는 빌드 결과에 붙일 이름·태그입니다.
  • restart: unless-stopped: 데몬 재시작·비정상 종료 후에 컨테이너가 다시 뜨도록 합니다(수동으로 멈춘 경우는 제외).
  • ports: "${API_PORT:-3000}:3000": 호스트의 포트(기본 3000)를 컨테이너 3000으로 넘깁니다. ${VAR:-기본값}은 셸과 비슷하게 환경 변수 미설정 시 기본값을 씁니다.
  • depends_on + condition: service_healthy: DB·Redis가 헬스체크를 통과한 뒤 API가 시작됩니다(단순 depends_on만으로는 “준비 완료”를 보장하지 않습니다).
  • healthcheck(interval/timeout/retries/start_period): 프로브 주기·실패 판정·기동 유예를 정합니다. API는 wget으로 로컬 /health를 두드립니다.
  • named volume pgdata, redisdata: 데이터가 도커 볼륨 영역에 남아 컨테이너를 재생성해도 유지됩니다(docker compose down -v는 볼륨까지 지우므로 주의하십시오).
  • Redis AOF(appendonly yes): 명령을 추가 로그로 남겨 재시작 시 복구력을 높입니다(요구 수준에 따라 RDB 스냅샷 병행을 검토합니다).
  • command: > redis-server ... --requirepass: 비밀번호 인증을 켭니다. REDIS_URL의 비밀번호와 반드시 맞춰야 합니다.

API의 /health 엔드포인트 (Node.js 예시)

// src/health.js — Express 예시
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;
}

마이그레이션 실행 타이밍

앱 기동 스크립트에서 DB 연결 후 마이그레이션을 한 번 실행하는 패턴이 흔합니다.

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

엔트리에서 npm run migrate && npm run start 대신 코드에서 순차 실행하면 셸 의존을 줄일 수 있습니다.


고급: 리소스·시크릿·프로파일

리소스 제한 (프로덕션 권장)

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

Swarm 전용 deploy가 아닌 일반 compose에서는 비 Swarm 환경에선 mem_limit 등 호환 키를 쓰거나, Kubernetes/Nomad로 넘어가며 제한을 거는 경우가 많습니다. 호스트 한 대에서 돌릴 때는 docker run 대신 compose에 cgroup 관련 옵션을 맞추는지 문서를 확인하세요.

프로파일로 dev만 부가 서비스

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

시크릿 (Docker Swarm 또는 외부 비밀 저장소)

단일 서버라면 파일 기반 env + 권한 chmod 600이 현실적입니다. Kubernetes를 쓰면 Secret 리소스로 치환합니다.


성능: 볼륨과 연결 풀

항목권장비고
Postgres 볼륨named volume호스트 바인드는 OS별 성능·권한 이슈
연결 풀앱에서 max 제한컨테이너 수 × 풀 크기 ≤ DB max_connections
Redis적절한 maxmemory + 정책캐시 전용이면 allkeys-lru
로그JSON 드라이버 또는 파일 로테이션컨테이너 로그 디스크 폭주 방지

트레이드오프: restart: always는 편하지만 재시작 루프에 빠지면 서비스가 계속 죽을 수 있으므로 헬스체크·로그 알림과 함께 써야 합니다.


실무 사례

  • 스테이징 = 프로덕션과 동일 compose: 변수만 STAGING_*로 바꿔 동일 파일을 재사용합니다.
  • CI에서 compose 테스트: docker compose up -d 후 스모크 테스트·docker compose down으로 통합 검증을 자동화합니다(GitHub Actions 글 참고).
  • Nginx 앞단: TLS 종료와 리버스 프록시는 Nginx 구성 글에서 다룹니다.

트러블슈팅

증상원인해결
API가 DB에 연결 실패localhost 사용DB 호스트는 postgres 서비스명
password authentication failed.env 불일치DATABASE_URL과 Postgres env 동기화
헬스체크 무한 실패/health 없음 또는 포트 불일치경로·포트·wget/curl 설치 확인
Redis NOAUTH비밀번호 불일치REDIS_URLrequirepass 동일
디스크 꽉 참로그·AOF 증가로그 로테이션, Redis maxmemory

디버깅 팁: docker compose logs -f api postgres redis로 동시에 보고, docker compose exec postgres psql -U app -d appdb로 DB만 따로 확인합니다.


마무리

  • Compose로 API·Postgres·Redis를 한 번에 정의하시면 환경 재현성과 온보딩이 빨라집니다.
  • 헬스체크·볼륨·재시작 정책은 프로덕션 인근 환경에서 필수에 가깝습니다.
  • 다음으로 Nginx 리버스 프록시로 Node.js 서비스 앞단 구성하기에서 공개 엔드포인트와 TLS를 정리해 보세요.

프로덕션 배포 전 체크리스트

  • .env·비밀번호가 Git에 올라가지 않았는지, 스테이징·프로덕션 값이 섞이지 않았는지 확인합니다.
  • DB·Redis 백업 주기down -v 실수 시 복구 절차를 문서화합니다.
  • API max_connections 대비 연결 풀 합이 DB 한도를 넘지 않는지 점검합니다.