01 — Architecture de production sur Hetzner
Vue d'ensemble
┌─────────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└───────────────────────────────┬─────────────────────────────────────────┘
│
┌───────────▼───────────┐
│ Cloudflare │
│ WAF + CDN + DNS │
│ TLS termination │
└───────────┬───────────┘
│ HTTPS (443)
┌───────────▼───────────┐
│ Hetzner Load │
│ Balancer (LB11) │
│ €7.49/mois │
└─────┬─────────┬───────┘
│ │
┌──────────────▼──┐ ┌───▼──────────────┐
│ Worker Node 1 │ │ Worker Node 2 │
│ CX42 (8c/16G) │ │ CX42 (8c/16G) │
│ €21.49/mois │ │ €21.49/mois │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ NestJS Pod │ │ │ │ NestJS Pod │ │
│ │ (backend) │ │ │ │ (backend) │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Next.js Pod │ │ │ │ Next.js Pod │ │
│ │ (frontend) │ │ │ │ (frontend) │ │
│ └─────────────┘ │ │ └─────────────┘ │
└────────┬────────┘ └────────┬─────────┘
│ Réseau privé Hetzner (10.0.0.0/16)
┌────────▼────────────────────▼─────────┐
│ Control Plane Node │
│ CX22 (2c/4G) €5.11/mois │
│ k3s server (etcd) │
└────────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌───────▼───────┐ ┌───────────▼──────────┐ ┌───────▼───────┐
│ PostgreSQL │ │ Redis │ │ Hetzner │
│ Neon.tech │ │ Upstash (serverless) │ │ Object │
│ ou self-host │ │ ou self-hosted │ │ Storage │
│ $19/mois │ │ $0-10/mois │ │ S3-compat. │
│ │ │ │ │ €4.99/mois │
└───────────────┘ └──────────────────────┘ └───────────────┘
Composants et rôles
Couche réseau
| Composant |
Rôle |
Port |
| Cloudflare |
DNS, WAF, CDN, protection DDoS, cache assets |
443 (HTTPS) |
| Hetzner Load Balancer |
Distribution trafic entre workers, sticky sessions WebSocket |
80, 443 |
| Réseau privé Hetzner |
Communication inter-nœuds (10.0.0.0/16), base de données |
Interne |
Couche Kubernetes (k3s)
| Composant |
Rôle |
Ressource |
| Control Plane (CX22) |
etcd, kube-apiserver, scheduler, controller-manager |
2 vCPU / 4 GB |
| Worker Nodes (CX42) |
Exécution des pods NestJS + Next.js |
8 vCPU / 16 GB chacun |
| Traefik Ingress |
Routage HTTP/HTTPS, sticky sessions Socket.IO |
Built-in k3s |
| cert-manager |
TLS automatique via Let's Encrypt |
In-cluster |
| Hetzner Cloud Controller |
Provisionne LB + volumes depuis Kubernetes |
In-cluster |
| Hetzner CSI Driver |
PersistentVolumes sur Hetzner Volumes |
In-cluster |
Couche application
| Pod |
Image |
Replicas |
Ports |
| xpeditis-backend |
ghcr.io/<org>/xpeditis-backend:latest |
2–15 |
4000 |
| xpeditis-frontend |
ghcr.io/<org>/xpeditis-frontend:latest |
1–8 |
3000 |
Couche données
| Service |
Option MVP |
Option Production |
Protocole |
| PostgreSQL 15 |
Neon.tech Pro ($19/mois) |
Self-hosted sur CX32 |
5432 |
| Redis 7 |
Upstash free ($0-10/mois) |
Self-hosted StatefulSet |
6379 |
| Stockage fichiers |
Hetzner Object Storage (€4.99/mois) |
Idem (scale automatique) |
HTTPS/S3 API |
Flux réseau détaillé
Requête API standard (rate search)
Client Browser
│ HTTPS
▼
Cloudflare (cache miss → forward)
│ HTTPS, header CF-Connecting-IP
▼
Hetzner Load Balancer :443
│ HTTP (TLS terminé par Cloudflare ou cert-manager)
▼
Traefik Ingress (api.xpeditis.com)
│ HTTP :80 interne
▼
NestJS Pod (port 4000)
├── Redis (cache rate:FSN:HAM:20ft) → HIT → retour direct
└── MISS → 5× appels APIs carriers (Maersk/MSC/etc.)
└── Réponse → Store Redis TTL 15min
└── Réponse client
Connexion WebSocket (notifications temps réel)
Client Browser
│ wss:// upgrade
▼
Cloudflare (WebSocket proxy activé)
│
▼
Hetzner LB (sticky session cookie activé)
│ Même backend pod à chaque reconnexion
▼
Traefik (annotation sticky cookie)
│
▼
NestJS Pod /notifications namespace (Socket.IO)
├── Auth: JWT validation on connect
├── Join room: user:{userId}
└── Redis pub/sub → broadcast cross-pods
Upload de document (carrier portal)
Carrier Browser
│
▼
NestJS POST /api/v1/csv-bookings/{id}/documents
│ Validation: type (PDF/XLS/IMG), taille max 10 MB
▼
S3StorageAdapter.upload()
│ AWS SDK v3, forcePathStyle: true
▼
Hetzner Object Storage
│ Endpoint: https://fsn1.your-objectstorage.com
└── Stocké: xpeditis-docs/{orgId}/{bookingId}/{filename}
Ports et protocoles
Ports externes (ouverts sur Hetzner Firewall)
| Port |
Protocole |
Source |
Destination |
Usage |
| 22 |
TCP |
Votre IP uniquement |
Tous nœuds |
SSH administration |
| 80 |
TCP |
0.0.0.0/0 |
LB |
Redirection HTTP → HTTPS |
| 443 |
TCP |
0.0.0.0/0 |
LB |
HTTPS + WebSocket |
| 6443 |
TCP |
Votre IP + workers |
Control plane |
Kubernetes API |
Ports internes (réseau privé 10.0.0.0/16 uniquement)
| Port |
Protocole |
Usage |
| 5432 |
TCP |
PostgreSQL (si self-hosted) |
| 6379 |
TCP |
Redis (si self-hosted) |
| 4000 |
TCP |
NestJS API (pod → pod) |
| 3000 |
TCP |
Next.js (pod → pod) |
| 10250 |
TCP |
kubelet API |
| 2379-2380 |
TCP |
etcd (control plane) |
Namespaces Kubernetes
cluster
├── xpeditis-prod # Application principale
│ ├── Deployments: backend, frontend
│ ├── Services: backend-svc, frontend-svc
│ ├── ConfigMaps: backend-config, frontend-config
│ ├── Secrets: backend-secrets, frontend-secrets
│ ├── HPA: backend-hpa, frontend-hpa
│ └── Ingress: xpeditis-ingress
│
├── cert-manager # Gestion certificats TLS
│ └── ClusterIssuer: letsencrypt-prod, letsencrypt-staging
│
├── monitoring # Observabilité
│ ├── Prometheus
│ ├── Grafana
│ └── Loki
│
└── kube-system # Système k3s
├── Traefik (Ingress Controller)
├── Hetzner Cloud Controller Manager
└── Hetzner CSI Driver
Pourquoi k3s plutôt que k8s complet
| Critère |
k3s (choisi) |
k8s complet |
| RAM control plane |
512 MB |
2-4 GB |
| CPU control plane |
1 vCPU |
2-4 vCPU → serveur plus cher |
| Temps install |
5 min (hetzner-k3s) |
30-60 min |
| Maintenance |
System Upgrade Controller inclus |
Manuelle |
| Compatibilité |
100% compatible kubectl/helm |
— |
| Traefik |
Inclus par défaut |
Installation séparée |
| Coût |
CX22 (€5.11/mois) comme control plane |
Minimum CX42 (€21.49) |
| Production |
Oui (utilisé par des milliers de startups) |
Oui |
Stratégie de scaling
Horizontal Pod Autoscaler (HPA)
Métriques surveillées :
- CPU > 70% → Scale up
- CPU < 30% (5 min) → Scale down
- Mémoire > 80% → Scale up (custom metric)
Backend : min 2 → max 15 pods
Frontend : min 1 → max 8 pods
Cluster Autoscaler
Worker nodes : min 2 → max 8
Déclenché par : pods en état "Pending" (pas assez de ressources)
Délai scale-down : 10 min d'utilisation < 50%
Décisions d'architecture
Pourquoi Hetzner Object Storage plutôt que MinIO self-hosted
Le code utilise déjà AWS SDK v3 avec forcePathStyle: true et un endpoint configurable. Hetzner Object Storage est 100% compatible S3 → zéro modification de code, juste les variables d'environnement :
# Avant (MinIO local)
AWS_S3_ENDPOINT=http://localhost:9000
# Après (Hetzner Object Storage)
AWS_S3_ENDPOINT=https://fsn1.your-objectstorage.com
AWS_ACCESS_KEY_ID=<hetzner_access_key>
AWS_SECRET_ACCESS_KEY=<hetzner_secret_key>
AWS_REGION=eu-central-1
AWS_S3_BUCKET=xpeditis-prod
Pourquoi Neon.tech pour PostgreSQL (MVP)
- PostgreSQL 15 managé, compatible TypeORM
- Extensions
uuid-ossp et pg_trgm disponibles (requis par Xpeditis)
- Backups automatiques inclus
- Connection pooling built-in (via PgBouncer)
- Pas de gestion de HA à faire manuellement
- Free tier pour le dev, $19/mois pour la prod
- Migration vers self-hosted possible à tout moment
Pourquoi Cloudflare devant Hetzner LB
- CDN mondial (cache des assets Next.js)
- Protection DDoS free
- WAF avec règles OWASP
- DNS avec failover automatique
- Certificats TLS optionnels (on peut laisser cert-manager gérer le TLS)
- Cache des PDFs générés → économise les appels S3