· Hetzner· VPS· Docker Compose· Nginx· SaaS· server· deployment· sikkerhed· backup
Hetzner VPS med Docker Compose til SaaS-platform — opsætning og sikkerhed
Opsætning af Hetzner VPS til IoT SaaS-platform: Docker Compose stack, Nginx reverse proxy, Let's Encrypt TLS, firewall-regler, backup-strategi og monitoring.
Af M-Bus Gateway
M-Bus Gateway SaaS-platformen kører på en Hetzner VPS i Falkenstein (EU) med Docker Compose. Her er den komplette opsætning fra tom server til produktionsklar platform.
Hardware-valg: Hetzner CX32
Hetzner Cloud CX32 (valgt):
vCPU: 4 (AMD EPYC)
RAM: 8 GB
SSD: 80 GB NVMe
Netværk: 20 TB/måned (mere end rigeligt)
Pris: ~15 EUR/måned (EU-hostet = GDPR-kompatibelt)
Lokation: Falkenstein (DE) — Hetzner Object Storage samme datacenter
Load estimat:
FastAPI: 2-4 workers, <500 MB RAM
TimescaleDB: 2-4 GB RAM (med cache)
Celery workers: 200-400 MB
Mosquitto: <50 MB
Redis: 100-300 MB
Nginx: <50 MB
Total: ~5-6 GB RAM → CX32 passer
Docker Compose stack
# docker-compose.yml
version: "3.9"
services:
db:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_DB: mbus
POSTGRES_USER: mbus
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
networks:
- internal
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- internal
mosquitto:
image: eclipse-mosquitto:2.0
ports:
- "8883:8883"
volumes:
- ./mosquitto/:/mosquitto/config/:ro
- ./certs/:/mosquitto/certs/:ro
- mosquitto_data:/var/lib/mosquitto
restart: unless-stopped
networks:
- internal
- external
api:
build:
context: ./server
dockerfile: Dockerfile
environment:
DATABASE_URL: postgresql+asyncpg://mbus:${POSTGRES_PASSWORD}@db/mbus
REDIS_URL: redis://redis:6379/0
MQTT_HOST: mosquitto
SECRET_KEY: ${SECRET_KEY}
BREVO_API_KEY: ${BREVO_API_KEY}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
depends_on:
- db
- redis
- mosquitto
volumes:
- ./ui/dist:/app/static:ro
- ./server/templates:/app/templates:ro
restart: unless-stopped
networks:
- internal
worker:
build: ./server
command: celery -A server.src.workers.celery_app worker
--queues default,high --concurrency 2 --loglevel=info
environment:
DATABASE_URL: postgresql+asyncpg://mbus:${POSTGRES_PASSWORD}@db/mbus
REDIS_URL: redis://redis:6379/0
depends_on:
- db
- redis
restart: unless-stopped
networks:
- internal
beat:
build: ./server
command: celery -A server.src.workers.celery_app beat --loglevel=info
environment:
REDIS_URL: redis://redis:6379/0
DATABASE_URL: postgresql+asyncpg://mbus:${POSTGRES_PASSWORD}@db/mbus
depends_on:
- redis
restart: unless-stopped
networks:
- internal
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- ./ui/dist:/var/www/html:ro
depends_on:
- api
restart: unless-stopped
networks:
- internal
- external
networks:
internal:
internal: true # Ikke tilgængeligt udefra
external:
volumes:
postgres_data:
redis_data:
mosquitto_data:
Nginx reverse proxy
# nginx/nginx.conf
server {
listen 80;
server_name mbus-gateway.dk www.mbus-gateway.dk;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name mbus-gateway.dk www.mbus-gateway.dk;
ssl_certificate /etc/letsencrypt/live/mbus-gateway.dk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mbus-gateway.dk/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
# Statisk frontend (React build):
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# API proxy:
location /api/ {
proxy_pass http://api:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Webhooks:
location /webhooks/ {
proxy_pass http://api:8000;
proxy_read_timeout 60s;
}
# Security headers:
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'";
}
Firewall: Hetzner Cloud Firewall
# Hetzner Cloud Firewall rules:
# Kun åbn det absolutt nødvendige:
Indgående:
TCP 22 → SSH (begræns til din IP!)
TCP 80 → HTTP (redirect til HTTPS)
TCP 443 → HTTPS
TCP 8883 → MQTT TLS (fra gateways)
Alt andet: BLOCK (implicit)
# Lokalt UFW som ekstra lag:
ufw default deny incoming
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8883/tcp
ufw enable
Let's Encrypt TLS
# Certbot installation:
apt install certbot python3-certbot-nginx
# Hent certifikat:
certbot --nginx -d mbus-gateway.dk -d www.mbus-gateway.dk \
--email admin@mbus-gateway.dk --agree-tos --non-interactive
# Auto-fornyelse (via certbot-timer):
systemctl enable certbot.timer
# Verificér fornyelse:
certbot renew --dry-run
Backup-strategi
#!/bin/bash
# scripts/backup.sh — køres dagligt via cron
BACKUP_DIR="/tmp/mbus-backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# PostgreSQL dump:
docker compose exec -T db pg_dump -U mbus mbus | \
gzip > "$BACKUP_DIR/postgres.sql.gz"
# Upload til Hetzner Object Storage (S3-kompatibel):
s3cmd put "$BACKUP_DIR/postgres.sql.gz" \
s3://mbus-backups/daily/postgres-$(date +%Y%m%d).sql.gz
# Ryd lokalt:
rm -rf "$BACKUP_DIR"
# Retention: Hetzner Object Storage lifecycle rule:
# → Daily backups: 30 dage
# → Weekly backups: 1 år
# → Monthly backups: 5 år (bogføringsloven)
Monitoring
# Simpel health-check script (cron hvert 5. min):
#!/bin/bash
API_URL="https://mbus-gateway.dk/api/v1/health"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_URL")
if [ "$STATUS" != "200" ]; then
curl -s -X POST "https://api.brevo.com/v3/smtp/email" \
-H "api-key: $BREVO_API_KEY" \
-d "{\"to\":[{\"email\":\"admin@mbus-gateway.dk\"}],
\"subject\":\"Platform nede! HTTP $STATUS\",
\"textContent\":\"API svarede $STATUS\"}"
fi
Konklusion
Hetzner CX32 + Docker Compose er den kostnadseffektive løsning for en IoT SaaS-platform med GDPR-compliance: EU-hostet, predictable pris, kraftfuld nok til hundredevis af tenants. Nøglen er korrekt netværkssegmentering (internal Docker network), Nginx som eneste indgangspunkt og automatisk backup til Hetzner Object Storage.
Se FastAPI multi-tenant arkitektur eller MQTT TLS opsætning.