My complete self-hosting stack: Docker Compose + hardening scripts I use on Hetzner (sharing everything)
Source: Dev.to
Originally written for r/selfhosted on Reddit — sharing here for the dev.to community. After running my self-hosted setup for 2+ years on a single Hetzner CX32 (4 vCPU, 8GB RAM, €15/mo), I finally cleaned up my config into something reusable. Currently hosting 18+ containers with ~1.7GB RAM to spare. Sharing the full setup in case it helps someone getting started or optimizing. Services running: ├── Reverse Proxy: Caddy (auto-HTTPS, dead simple config) ├── Monitoring: Uptime Kuma + Prometheus + Grafana ├── Analytics: Matomo (self-hosted, no Google) ├── Passwords: Vaultwarden ├── Notes: Hedgedoc ├── Files: Nextcloud ├── Media: Jellyfin ├── Git: Gitea + Drone CI ├── DNS: AdGuard Home ├── Automation: n8n ├── Backup: Restic → Hetzner Storage Box └── DSGVO Scanner: Custom (more on that below)
I use a single docker-compose.yml with profiles so I can start subsets:
docker-compose.yml (simplified)
version: “3.8”
services: caddy: image: caddy:2-alpine container_name: caddy restart: unless-stopped ports: - “80:80” - “443:443” volumes: - ./caddy/Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config profiles: [“core”]
uptime-kuma: image: louislam/uptime-kuma:1 container_name: uptime-kuma restart: unless-stopped volumes: - uptime-kuma:/app/data profiles: [“monitoring”]
matomo: image: matomo:latest container_name: matomo restart: unless-stopped depends_on: - matomo-db environment: - MATOMO_DATABASE_HOST=matomo-db volumes: - matomo:/var/www/html profiles: [“analytics”]
vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped environment: - ADMIN_TOKEN=${VAULTWARDEN_ADMIN_TOKEN} - SMTP_HOST=${SMTP_HOST} volumes: - vaultwarden:/data profiles: [“security”]
volumes: caddy_data: caddy_config: uptime-kuma: matomo: vaultwarden:
Start everything: docker compose —profile core —profile monitoring —profile security up -d Or just the core: docker compose —profile core up -d This is the script I run on every fresh Hetzner box: #!/bin/bash
server-harden.sh — Run as root on fresh Debian/Ubuntu
set -euo pipefail
1. SSH hardening
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak cat > /etc/ssh/sshd_config.d/hardening.conf <<EOF PermitRootLogin prohibit-password PasswordAuthentication no PubkeyAuthentication yes X11Forwarding no MaxAuthTries 3 ClientAliveInterval 300 ClientAliveCountMax 2 EOF systemctl restart sshd
2. Firewall
apt install -y ufw ufw default deny incoming ufw default allow outgoing ufw allow 80/tcp ufw allow 443/tcp ufw allow 22/tcp ufw —force enable
3. Fail2ban
apt install -y fail2ban systemctl enable —now fail2ban
4. Automatic updates
apt install -y unattended-upgrades dpkg-reconfigure -plow unattended-upgrades
5. Docker
curl -fsSL https://get.docker.com | sh systemctl enable docker
6. Monitoring agent
apt install -y prometheus-node-exporter systemctl enable —now prometheus-node-exporter
echo ”✅ Server hardened. Reboot recommended.”
One Caddyfile handles all services with auto-HTTPS: { email admin@yourdomain.de }
grafana.yourdomain.de { reverse_proxy grafana:3000 }
uptime.yourdomain.de { reverse_proxy uptime-kuma:3001 }
bitwarden.yourdomain.de { reverse_proxy vaultwarden:80 }
matomo.yourdomain.de { reverse_proxy matomo:80 }
*.yourdomain.de { @nc host cloud.yourdomain.de handle @nc { reverse_proxy nextcloud:80 } }
#!/bin/bash
backup.sh — Runs via cron daily at 3am
export RESTIC_REPOSITORY=/mnt/storagebox/backups export RESTIC_PASSWORD_FILE=/root/.restic-password
Stop services for consistent backup
docker compose -f /opt/selfhosted/docker-compose.yml stop matomo nextcloud
Backup docker volumes + config
restic backup /var/lib/docker/volumes /opt/selfhosted
Restart
docker compose -f /opt/selfhosted/docker-compose.yml start matomo nextcloud
Prune old backups (keep 7 daily, 4 weekly, 3 monthly)
restic forget —keep-daily 7 —keep-weekly 4 —keep-monthly 3 —prune
Since I’m on a German server, I actually need to ensure my self-hosted services are DSGVO compliant too. Yes, even personal projects if they collect any user data. I built a scanner that checks for common issues: External resource loading (Google Fonts, CDNs outside EU) Cookie consent status SSL/TLS configuration Missing Impressum/Datenschutz It’s free to run at nevik.de/guard/ if anyone wants to check their setup. Honestly surprised how many self-hosted services I found with Google Fonts still loading externally — that’s a €500-2000 Abmahnung risk in Germany. After 2 years of tuning: Uptime: 99.94% (2 planned reboots) RAM usage: 5.8GB / 7.6GB Disk: 109GB / 150GB Monthly cost: ~€15 (Hetzner) + €3.50 (Storage Box for backups) Time investment: ~2 hours/month for updates Total monthly cost: €18.50 for what would cost $200+/month in SaaS subscriptions. Start with Caddy, not Nginx — Saved me hours of SSL cert management Use Docker profiles from day 1 — Makes testing individual services much easier Set up Restic immediately — I lost Nextcloud data once before I had proper backups Monitor from the start — Uptime Kuma takes 2 minutes to set up, saves hours of debugging Happy to answer questions about any part of the setup. I also put together a more complete package with all the scripts, configs, and a step-by-step guide if anyone’s interested — DM me and I’ll share the link.