n8n in queue mode with Redis, PostgreSQL persistence, and proper process supervision — the boring, reliable setup that survives restarts and workload spikes. The automation layer behind several of my
When one node is not enough: SeaweedFS provides distributed, replicated S3 storage with a master/filer/volume architecture — ideal as a large-scale backup target on commodity hardware.
The two services almost every application needs, tuned instead of defaulted: production-tuned PostgreSQL (shared buffers, WAL compression), Redis with AOF + RDB persistence, and a scheduled backup job with retention.
A deployment guide with working examples — not a downloadable product. Everything below is the actual configuration I use, abridged to the parts that matter.
services:
postgres:
image: postgres:${POSTGRES_VERSION:-16-alpine}
container_name: db-postgres
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
- ./config/postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
- ./config/postgres/init:/docker-entrypoint-initdb.d:ro
- backup_data:/backups
environment:
- POSTGRES_USER=${POSTGRES_USER:-app}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-app}
- PGDATA=/var/lib/postgresql/data/pgdata
ports:
- ${POSTGRES_PORT:-5432}:5432
command:
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
healthcheck:
test:
- CMD-SHELL
- pg_isready -U ${POSTGRES_USER:-app}
interval: 15s
timeout: 5s
retries: 5
networks:
- db_network
redis:
image: redis:${REDIS_VERSION:-7-alpine}
container_name: db-redis
restart: unless-stopped
command:
- redis-server
- --appendonly
- "yes"
- --appendfsync
- everysec
- --requirepass
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
- --maxmemory
- ${REDIS_MAX_MEMORY:-512mb}
- --maxmemory-policy
- allkeys-lru
- --save
- 900 1
- --save
- 300 10
- --save
- 60 10000
volumes:
- redis_data:/data
ports:
- ${REDIS_PORT:-6379}:6379
healthcheck:
test:
- CMD
- redis-cli
- --raw
- incr
- ping
interval: 15s
timeout: 5s
retries: 5
networks:
- db_network
Excerpt — postgres, redis from the compose file. The remaining services (proxies, init jobs, exporters) follow the same pattern and mount their configuration from a config/ directory.
Copy .env.example to .env and at minimum set:
POSTGRES_VERSION=16-alpine
POSTGRES_USER=app
POSTGRES_PASSWORD=change_me
POSTGRES_DB=app
POSTGRES_PORT=5432
PGDATA=/var/lib/postgresql/data/pgdata
.env before the first start — never ship the example valuesThree settings do most of the work, and all three are statements about your
machine, not magic numbers. shared_buffers is PostgreSQL's own cache - the
classical starting point is a quarter of system RAM, enough to hold your hot
working set without starving the operating system. effective_cache_size
does not allocate anything; it tells the planner how much cache the OS page
cache likely provides, which changes index-scan versus sequential-scan
decisions. Setting it to roughly half to three-quarters of RAM is honest
reporting, not tuning. WAL compression trades a little CPU for significantly
less write volume on the disk - the right trade on almost any modern host.
The config file ships mounted read-only, so changing tuning is a git commit
and a container restart, and the restart is deliberate: durable configuration
should never depend on remembering a docker exec.
The backup job is half the feature; the other half is rehearsing the restore
until it is boring. The drill: spin up a throwaway Postgres container, apply
the newest dump, run the application against it, and time the whole thing.
The timing matters more than the success - restore duration is your real
RTO, and discovering that it is four hours during an incident is too late.
Put the drill on a calendar for each retention cycle. Redis restores are the
same discipline in miniature: stop the instance, swap in the dump.rdb,
start, and confirm the keys are there. A backup you have never restored is
not a backup; it is a hope with a schedule.
Which PostgreSQL settings matter most for production?
The defaults are tuned for laptops: shared_buffers, effective_cache_size,
and WAL compression are the first three worth setting deliberately against
your actual RAM. The stack ships a tuned postgresql.conf mounted read-only, so
tuning is versioned config, not a forgotten docker exec.
Why enable both AOF and RDB in Redis? They cover each other's gaps: RDB is a compact point-in-time snapshot that restores fast but loses everything since the last snapshot, AOF logs every write for minimal loss but grows larger and replays slower. Together you get fast recovery and a tight loss window.
How do backups and retention work here? A scheduled job dumps the database into a backup volume with a retention policy, separate from the live data directories. The critical part is not the dump but the restore: rehearse the restore path before you need it - a backup you have never restored is a hypothesis.
Should Postgres and Redis ports be exposed to the host? Keep them on the internal Docker network by default and expose only what an external consumer genuinely needs, ideally through the reverse proxy with TLS. Databases on 0.0.0.0 are the single most common self-inflicted breach in homelab setups.
A guide, not a product. This page is deployment documentation with working examples — there is no zip, no download, no support contract. You adapt the patterns to your own environment, and you own the result.
Want this running production-grade in your infrastructure instead? Foundation work like this is my standard infrastructure consulting scope — details on the consulting page.