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.ymlthat 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
- Concepts: Compose and production stacks
- Hands-on: docker-compose.yml template
- Advanced: resources, secrets, profiles
- Performance: volumes and connection pools
- Real-world cases
- Troubleshooting
- 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 (
apiconnects topostgresatpostgres:5432). - Volume: persists data across container recreation (e.g. Postgres data directory).
- Healthcheck: with
depends_oncondition: 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 -vdeletes 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
| Topic | Recommendation | Notes |
|---|---|---|
| Postgres volume | named volume | Host bind has OS-specific perf/permission issues |
| Connection pool | cap max in the app | containers × pool size ≤ DB max_connections |
| Redis | maxmemory + policy | e.g. allkeys-lru for cache-only |
| Logs | JSON driver or rotation | avoid 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
| Symptom | Cause | Fix |
|---|---|---|
| API cannot reach DB | using localhost | DB host must be the postgres service name |
password authentication failed | .env mismatch | sync DATABASE_URL with Postgres env |
| Health check never passes | missing /health or wrong port | check path, port, wget/curl in image |
Redis NOAUTH | password mismatch | align REDIS_URL and requirepass |
| Disk full | logs / AOF growth | log 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.