Docker Compose with Postgres and Redis: Production-Ready Stack
What It Is
Docker Compose is a tool for defining and running multi-container Docker applications using a single declarative YAML file. When combined with PostgreSQL (a robust relational database) and Redis (an in-memory data store used for caching, session management, and message brokering), you get a powerful, reproducible infrastructure stack that can be spun up with a single command.
In a typical modern web application, Postgres handles persistent relational data—user accounts, orders, inventory—while Redis accelerates performance through caching, handles background job queues, or manages real-time pub/sub events. Docker Compose ties them together into a cohesive local development environment or a production deployment blueprint.
Why It Matters for Production
Many teams treat Docker Compose as a development-only tool, but with the right configuration it becomes a legitimate production orchestrator for smaller to medium-scale deployments, especially on single-host setups or when paired with Docker Swarm. Here's why it matters:
- Reproducibility: Every service, network, and volume is defined as code. No more "works on my machine" issues—your staging and production environments mirror each other exactly.
- Atomic deployments: A single
docker compose up -dbrings up the entire stack, anddocker compose downtears it down cleanly. - Isolation: Each service runs in its own container with its own filesystem, process space, and network namespace, reducing conflicts.
- Simplified secrets and config management: Environment variables, mounted config files, and Docker secrets can be managed directly in the compose file.
- Built-in health checks: Compose supports native Docker health checks, allowing dependent services to wait for Postgres and Redis to be truly ready before starting.
- Zero-downtime potential: With proper rolling update strategies and reverse proxies, you can achieve near-zero-downtime deployments even on a single host.
How to Use It: A Complete Production Setup
Below is a full, production-oriented docker-compose.yml that brings together an application service, PostgreSQL, and Redis with sensible production defaults. We'll walk through each section in detail.
1. Project Structure
Before writing the compose file, organize your project directory:
project/
├── docker-compose.yml
├── .env
├── postgres/
│ ├── init/
│ │ └── 01-create-tables.sql
│ └── data/ # mounted as a volume (or Docker-managed)
├── redis/
│ └── redis.conf
├── app/
│ ├── Dockerfile
│ └── ... application code
└── scripts/
└── backup.sh
2. The Complete docker-compose.yml
version: "3.9"
services:
# ---- Application Service ----
app:
build:
context: ./app
dockerfile: Dockerfile
image: myapp:${APP_VERSION:-latest}
container_name: myapp_production
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable
REDIS_URL: redis://redis:6379/0
APP_ENV: production
LOG_LEVEL: info
ports:
- "8080:8080"
volumes:
- app_logs:/var/log/myapp
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
deploy:
resources:
limits:
cpus: '2'
memory: 512M
reservations:
cpus: '1'
memory: 256M
# ---- PostgreSQL ----
postgres:
image: postgres:16-alpine
container_name: postgres_production
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_INITDB_ARGS: "--data-checksums"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./postgres/init:/docker-entrypoint-initdb.d:ro
- postgres_backups:/backups
ports:
- "127.0.0.1:5432:5432" # only bind to localhost for security
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
deploy:
resources:
limits:
cpus: '4'
memory: 2G
reservations:
cpus: '2'
memory: 1G
command:
- "postgres"
- "-c"
- "shared_buffers=512MB"
- "-c"
- "effective_cache_size=1536MB"
- "-c"
- "maintenance_work_mem=128MB"
- "-c"
- "wal_level=replica"
- "-c"
- "max_wal_senders=3"
- "-c"
- "wal_keep_size=128MB"
- "-c"
- "max_connections=200"
- "-c"
- "log_statement=ddl"
- "-c"
- "log_checkpoints=on"
# ---- Redis ----
redis:
image: redis:7-alpine
container_name: redis_production
restart: unless-stopped
command:
- "redis-server"
- "/usr/local/etc/redis/redis.conf"
- "--requirepass"
- "${REDIS_PASSWORD}"
- "--appendonly"
- "yes"
- "--maxmemory"
- "512mb"
- "--maxmemory-policy"
- "allkeys-lru"
volumes:
- redis_data:/data
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
ports:
- "127.0.0.1:6379:6379"
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
deploy:
resources:
limits:
cpus: '2'
memory: 768M
reservations:
cpus: '1'
memory: 256M
sysctls:
- net.core.somaxconn=512
- vm.overcommit_memory=1
volumes:
postgres_data:
driver: local
postgres_backups:
driver: local
redis_data:
driver: local
app_logs:
driver: local
networks:
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
3. The .env File (Sensitive Variables)
Never hard-code secrets in the compose file. Use a .env file alongside your compose file, and add it to .gitignore:
# .env — keep this out of version control!
POSTGRES_USER=app_user
POSTGRES_PASSWORD=s3cur3_pg_p@ssw0rd_here
POSTGRES_DB=app_database
REDIS_PASSWORD=s3cur3_redis_p@ssw0rd_here
APP_VERSION=1.2.3
4. Redis Configuration File (redis.conf)
For production, Redis should be configured with persistence, security, and memory limits baked into a config file:
# redis.conf — production Redis configuration
# Network
bind 0.0.0.0
protected-mode yes
tcp-backlog 511
timeout 300
tcp-keepalive 300
# General
daemonize no
supervised auto
loglevel notice
logfile /var/log/redis/redis.log
# Persistence (AOF + RDB for durability)
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
appendonly yes
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# Security — password is also passed via command line
# requirepass is set via CLI for flexibility
# Memory Management
maxmemory 512mb
maxmemory-policy allkeys-lru
# Slow Log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Client limits
maxclients 10000
# Replication (if you later add replicas)
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
min-replicas-to-write 1
min-replicas-max-lag 10
5. PostgreSQL Initialization Script
Place SQL files in ./postgres/init/ to auto-run on first container startup:
-- 01-create-tables.sql
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token TEXT UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
6. Starting the Stack
With everything in place, bring the production stack up:
# Start all services in detached mode
docker compose up -d
# Watch logs across all services
docker compose logs -f
# Check health status of each container
docker compose ps
# View resource usage
docker stats myapp_production postgres_production redis_production
7. Backup and Restore Procedures
A production guide is incomplete without backup strategies. Here are practical commands:
# --- PostgreSQL Backup ---
# Full database dump
docker compose exec postgres pg_dump -U ${POSTGRES_USER} -d ${POSTGRES_DB} \
--format=custom --compress=9 > ./backups/pg_dump_$(date +%Y%m%d_%H%M%S).dump
# Plain SQL dump (for easier inspection)
docker compose exec postgres pg_dump -U ${POSTGRES_USER} -d ${POSTGRES_DB} \
--clean --if-exists > ./backups/pg_dump_$(date +%Y%m%d_%H%M%S).sql
# Restore from custom dump
docker compose exec -T postgres pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB} \
--clean --if-exists --no-owner < ./backups/pg_dump_20250101_120000.dump
# --- Redis Backup ---
# Redis automatically saves RDB snapshots and AOF logs in /data
# Simply copy the volume contents
docker compose exec redis redis-cli -a ${REDIS_PASSWORD} BGSAVE
# The dump.rdb and appendonly.aof files are now up-to-date in /data
# Back up the entire redis_data volume:
docker run --rm -v myproject_redis_data:/data -v $(pwd)/backups:/backups alpine \
cp -r /data /backups/redis_backup_$(date +%Y%m%d_%H%M%S)
Best Practices for Production
1. Bind Database Ports to Localhost Only
Notice in the compose file we use "127.0.0.1:5432:5432" instead of just "5432:5432". This prevents direct external access to Postgres and Redis. Only your application containers communicate over the internal backend network. If you need external database access, use an SSH tunnel or a VPN—never expose these ports directly to the internet.
2. Use Docker Secrets (Swarm Mode) or Runtime Secret Injection
For single-host Compose, environment variables in .env are acceptable if the file is properly restricted (chmod 600). For multi-node Swarm deployments, migrate to Docker Secrets:
# Example of Docker secret usage (Swarm mode)
secrets:
postgres_password:
external: true
services:
postgres:
secrets:
- postgres_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
3. Implement Proper Health Checks with Dependencies
The depends_on with condition: service_healthy ensures your app container doesn't start until Postgres and Redis are truly accepting connections, not just when their process starts. This eliminates race-condition failures during cold starts.
4. Set Resource Limits Explicitly
Without CPU and memory limits, a runaway query or a Redis memory leak can consume all host resources, starving other services. The deploy.resources block enforces hard boundaries. For Postgres, allocate generous memory but cap it; for Redis, set maxmemory alongside the container limit so they align.
5. Use Named Volumes with Local Driver for Data Durability
Named volumes (like postgres_data) persist beyond container lifecycles. The local driver stores data in /var/lib/docker/volumes/, which survives docker compose down (without -v flag). Always use volumes for database and Redis data—never rely on container ephemeral storage.
6. Configure PostgreSQL for Production Workloads
The command override in our compose file tunes Postgres with critical parameters:
- shared_buffers: ~25% of container memory for cache
- effective_cache_size: ~75% of container memory so the planner knows what's available
- wal_level=replica: enables point-in-time recovery and replication
- log_statement=ddl: logs schema changes for audit trails without overwhelming logs with every SELECT
- data-checksums: enabled via
POSTGRES_INITDB_ARGSto detect storage corruption early
7. Redis Persistence: AOF + RDB
For production, enable both RDB snapshots (periodic full dumps) and AOF (append-only file logging every write). This gives you the best of both worlds: fast recovery from RDB and minimal data loss from AOF. The appendfsync everysec setting balances durability with performance—fsync once per second means at most 1 second of data loss on crash, with minimal I/O overhead.
8. Use Alpine Images for Smaller Attack Surface
We use postgres:16-alpine and redis:7-alpine. Alpine-based images are significantly smaller (typically 30-50% smaller), have fewer installed packages (reducing CVE exposure), and consume less memory. The trade-off is occasional musl libc compatibility issues, but for databases this is rarely a problem in practice.
9. Network Isolation
Define a dedicated backend network with a static subnet. This prevents your database containers from accidentally communicating with other Docker networks on the host. If you later add a reverse proxy (like Traefik or Nginx), place it on a separate frontend network and only attach the app service to both:
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # prevents external traffic entirely
services:
app:
networks:
- frontend
- backend
postgres:
networks:
- backend
redis:
networks:
- backend
Setting internal: true on the backend network prevents any container on that network from reaching the internet—a useful security hardening measure.
10. Regular Backup Automation
Automate backups with a cron job on the host or a dedicated backup container. Here's a minimal host-level cron script:
#!/bin/bash
# /etc/cron.daily/pg_backup — run nightly via cron
BACKUP_DIR="/var/backups/postgres"
RETENTION_DAYS=30
CONTAINER="postgres_production"
USER="${POSTGRES_USER:-app_user}"
DB="${POSTGRES_DB:-app_database}"
mkdir -p "$BACKUP_DIR"
FILENAME="pg_dump_$(date +%Y%m%d_%H%M%S).dump"
docker compose exec -T "$CONTAINER" pg_dump -U "$USER" -d "$DB" \
--format=custom --compress=9 > "$BACKUP_DIR/$FILENAME"
# Remove backups older than retention period
find "$BACKUP_DIR" -name "pg_dump_*.dump" -mtime +$RETENTION_DAYS -delete
echo "Backup completed: $FILENAME"
11. Monitoring and Logging
For production visibility, integrate with external monitoring:
- Postgres: Use the
pg_stat_statementsextension (enable via init script) to track slow queries. Export metrics viapostgres_exporterfor Prometheus. - Redis: Use
redis_exporteror runredis-cli INFOperiodically to track memory fragmentation, hit ratios, and connected clients. - Logs: Use Docker's
json-filelogging driver with log rotation, or ship logs to a central system using thefluentdorlokilogging driver.
# In docker-compose.yml, add logging configuration per service:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
12. Graceful Shutdown and Zero-Downtime Updates
For rolling updates of your application container while keeping Postgres and Redis running:
# Rebuild and restart only the app service
docker compose up -d --no-deps --build app
# This creates a new container, waits for it to be healthy,
# then removes the old one — with no interruption to database services.
If you run multiple replicas of the app behind a load balancer, you can achieve true zero-downtime deployments. For single-host setups, consider using docker compose with an HAProxy or Nginx reverse proxy container that handles traffic shifting.
Conclusion
Docker Compose with Postgres and Redis, when configured thoughtfully, provides a robust production platform that balances simplicity with reliability. The key takeaways are: never expose databases to the public internet, always use health checks and proper dependency ordering, set explicit resource limits, persist data with named volumes, configure both database systems for production workloads (not their default "development-friendly" settings), and automate your backup strategy from day one. With the complete compose file and configurations provided above, you have a solid foundation that can be extended with replication, monitoring, and orchestrated rolling updates as your application grows. The stack is reproducible, version-controlled, and ready for both single-host production and migration to Docker Swarm or Kubernetes when the time comes.