← Back to DevBytes

Docker Compose with Postgres and Redis: Production Guide

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:

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:

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:

# 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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles