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
A complete Kubernetes lab-to-production pattern: k3s in Docker with the embedded etcd datastore, Rancher for management, MetalLB for Layer-2 LoadBalancer IPs, Longhorn replicated storage, and Traefik
Self-hosted S3-compatible object storage for backups: MinIO behind an Nginx proxy, with an rclone-based backup agent and versioned buckets. Your offsite backups without a cloud bill.
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:
minio:
image: minio/minio:${MINIO_VERSION:-RELEASE.2025-09-07T16-13-09Z}
container_name: minio-server
restart: unless-stopped
command: server /data --console-address ":9001"
ports:
- ${MINIO_API_PORT:-9000}:9000
- ${MINIO_CONSOLE_PORT:-9001}:9001
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me}
MINIO_DOMAIN: ${MINIO_DOMAIN:-minio.example.com}
MINIO_BROWSER: ${MINIO_BROWSER:-on}
MINIO_PROMETHEUS_AUTH_TYPE: public
volumes:
- minio-data:/data
- ./config/minio:/etc/minio:ro
healthcheck:
test:
- CMD
- curl
- -f
- http://localhost:9000/minio/health/live
interval: 15s
timeout: 5s
retries: 5
start_period: 10s
networks:
- storage
backup-agent:
image: minio/mc:${MC_VERSION:-RELEASE.2025-08-13T08-35-41Z}
container_name: minio-backup
restart: unless-stopped
entrypoint:
- /bin/sh
- -c
command:
- |
# Configure alias for local MinIO
mc alias set local http://minio:9000 \
${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}
# Configure alias for remote backup (if configured)
if [ -n "${BACKUP_ENDPOINT}" ]; then
mc alias set remote ${BACKUP_ENDPOINT} \
${BACKUP_ACCESS_KEY} ${BACKUP_SECRET_KEY}
fi
# Run backup script on schedule
while true; do
echo "[$$] Running backup at $$(date)"
sh /config/backup.sh 2>&1 || echo "Backup failed"
sleep ${BACKUP_INTERVAL:-86400}
done
volumes:
- ./config:/config:ro
depends_on:
minio:
condition: service_healthy
networks:
- storage
Excerpt — minio, backup-agent 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:
MINIO_VERSION=latest
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=change-me
MINIO_DOMAIN=minio.example.com
MINIO_ROOT_PASSWORD (24+ chars)BACKUP_RETENTION_DAYS for automatic cleanup/minio/v2/metrics/cluster)MINIO_VERSION to a specific release for productionmc admin heal on degraded drivesThe classic rule - three copies, two media, one offsite - maps cleanly onto this stack. Copy one is the live data where it is produced. Copy two is the MinIO bucket on your own hardware, written by the rclone agent on schedule. Copy three is the offsite push: because the agent speaks S3, the identical job definition gains a second target - any external S3 - and the same retention rules apply to both. Two media is satisfied naturally (local disk and object storage), and the offsite copy is what turns a house fire or ransomware event from a total loss into an inconvenient restore. The weak point of every 3-2-1 setup is never the copying; it is the retention policy nobody wrote down and the restore nobody rehearsed.
Versioning changes what delete means. With it enabled, every overwrite keeps the old object as a noncurrent version, and every delete writes a delete marker instead of removing data - restoring is then just removing the marker or copying an older version back. The cost is storage growth, which lifecycle rules contain: keep noncurrent versions for a fixed window (90 days is a sane default for backup buckets) and expire delete markers once no versions remain. The rclone agent applies these rules per bucket, so retention becomes configuration in git rather than a New Year's resolution. One habit worth keeping: check bucket size growth monthly, because unbounded noncurrent versions are the quietest disk-filler in a self-hosted setup.
Why self-host S3 storage instead of using Backblaze B2 or S3? Three reasons: data sovereignty (the copies never leave your hardware), no egress fees on restores, and LAN-speed recovery. The trade-off is that you own durability - which is exactly why the pairing with an offsite copy or versioned buckets matters.
Why MinIO instead of just backing up to a filesystem? The S3 API is the de-facto interface of the backup world - rclone, restic, and Veeam all speak it natively. Versioned buckets, lifecycle rules, and per-bucket policies come from the protocol instead of hand-rolled scripts, and switching to a cloud S3 later is an endpoint change.
How does bucket versioning protect against ransomware?
With versioning enabled, an overwrite or delete adds a new version marker
instead of destroying the old object. Encrypted-by-attacker files become just
the latest version; the intact predecessors remain restorable unless the
attacker gets the root credentials - which is why the root password belongs in
.env, not in the backup tool's config.
What does the rclone agent actually do? It runs on a schedule, syncs or copies from your sources into versioned MinIO buckets, and applies retention to old versions. Because it speaks S3, the identical job also pushes to any external S3 - the same config covers local and offsite copies.
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? Backup architecture like this is part of my infrastructure consulting — details on the consulting page.