Docker Compose: Node API, PostgreSQL, Redis in One Stack | Production Template

Docker Compose: Node API, PostgreSQL, Redis in One Stack | Production Template

이 글의 핵심

Compose Node API, PostgreSQL, and Redis with environment variables, health checks, persistent volumes, and restart policies close to production.

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.