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.
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.
The front door pattern: Nginx terminating TLS with automated Let's Encrypt renewal via Certbot, hardened by CrowdSec WAF/IPS with the nginx bouncer, kept fresh by Watchtower. One compose file in front of every other stack on this site.
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:
nginx:
image: nginx:${NGINX_VERSION:-1.27-alpine}
container_name: ssl-nginx
restart: unless-stopped
ports:
- ${HTTP_PORT:-80}:80
- ${HTTPS_PORT:-443}:443
volumes:
- ./config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./config/conf.d:/etc/nginx/conf.d:ro
- certbot-www:/var/www/certbot
- certbot-certs:/etc/letsencrypt
- ${LOG_DIR:-./logs}:/var/log/nginx
depends_on:
certbot:
condition: service_completed_successfully
networks:
- proxy
healthcheck:
test:
- CMD
- nginx
- -t
interval: 30s
timeout: 5s
retries: 3
crowdsec:
image: crowdsecurity/crowdsec:${CROWDSEC_VERSION:-v1.6.8}
container_name: ssl-crowdsec
restart: unless-stopped
environment:
- COLLECTIONS=crowdsecurity/nginx
- GID=${PGID:-1000}
- UID=${PUID:-1000}
volumes:
- crowdsec-db:/var/lib/crowdsec/data
- crowdsec-config:/etc/crowdsec
- ${LOG_DIR:-./logs}:/var/log/nginx:ro
networks:
- proxy
Excerpt — nginx, crowdsec 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:
HTTP_PORT=80
HTTPS_PORT=443
NGINX_VERSION=1.27-alpine
LOG_DIR=./logs
PROXY_NETWORK=ssl-reverse-proxy
CERTBOT_VERSION=latest
CROWDSEC_API_KEY (use openssl rand -base64 32)./logs//etc/letsencrypt/ (SSL certs).env for production deploymentsEvery inbound connection lands on Nginx first: it terminates TLS, serves whatever is static, and proxies the rest to the upstream stack on the shared proxy network. Before the request is proxied anywhere, the CrowdSec bouncer checks the client IP against the local decision list - IPs flagged by the community crowd or by local scenarios get a 403 and never touch an upstream. That ordering is the whole security model: detection happens on logs Nginx already writes, enforcement happens at the edge where it is cheapest.
Certificate traffic is the one exception to the proxy path. Let's Encrypt
validation requests to /.well-known/acme-challenge/ are served directly from
the shared Certbot volume, so renewals work even while most routes are proxied.
Three failures account for most edge incidents in practice. Silent cert
expiry: the Certbot container exits successfully but renewed nothing - check
echo | openssl s_client -connect your.domain:443 2>/dev/null | openssl x509 -noenddate, and confirm the webroot path in the vhost matches the Certbot
volume mount. CrowdSec false positives: your own office IP trips a
scenario after a misconfigured cron - cscli decisions list shows who is
banned and why, and adding the IP to the whitelist ends the surprise. 502s
after an upstream redeploy: the new container joined a different compose
network than Nginx proxies into - upstream stacks must join the shared proxy
network by name, not recreate it.
Every image tag in the compose file resolves through .env, and the
.env.example pins concrete versions for a reason: an edge stack should
upgrade when you decide, not when an upstream publishes. The upgrade cadence
that works in practice is monthly - read the Nginx and CrowdSec changelogs,
bump the pins, docker compose up -d, and watch the health checks. Certbot
is the one component worth updating immediately when a renewal API changes.
Keep a rollback line in your notes: the previous pin values, one copy-paste
away.
Why Nginx and CrowdSec instead of Caddy or Traefik? Caddy and Traefik automate certificates beautifully, but neither ships an intrusion-prevention layer. CrowdSec reads the Nginx access logs, detects scan and brute-force behaviour, and the nginx bouncer blocks offending IPs at the edge before they reach your applications. Certificate automation still comes from Certbot, so you lose nothing.
How do certificates get renewed without downtime?
Certbot runs in webroot mode: Nginx serves the .well-known/acme-challenge
directory from a shared volume, Certbot drops the validation file there, and
the compose file gates Nginx on Certbot completing first (depends_on with
service_completed_successfully). Renewals just re-run the Certbot container
and reload Nginx.
Can this front multiple domains and stacks?
Yes, that is the point of the pattern. Each upstream stack gets one
virtual-host file in config/conf.d, proxying to the shared Docker proxy
network. Certificates are issued per server block, and CrowdSec watches all of
them from one log stream.
What does Watchtower do, and should I keep it? Watchtower pulls newer images and restarts containers on a schedule, keeping the edge components patched without manual work. If you prefer controlled upgrades - and for a public edge you probably should - remove the Watchtower profile and update on your own cadence.
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? Edge security like this is part of my infrastructure consulting — details on the consulting page.