diff --git a/docker/.env.production.example b/docker/.env.production.example new file mode 100644 index 0000000..e6e3e98 --- /dev/null +++ b/docker/.env.production.example @@ -0,0 +1,97 @@ +# Xpeditis - Production Environment Variables +# Copy this file to .env.production and fill in the values + +# =================================== +# DOCKER REGISTRY +# =================================== +DOCKER_REGISTRY=docker.io +BACKEND_IMAGE=xpeditis/backend +BACKEND_TAG=latest +FRONTEND_IMAGE=xpeditis/frontend +FRONTEND_TAG=latest + +# =================================== +# DATABASE (PostgreSQL) +# =================================== +POSTGRES_DB=xpeditis_prod +POSTGRES_USER=xpeditis +POSTGRES_PASSWORD=CHANGE_ME_SECURE_PASSWORD_64_CHARS_MINIMUM + +# =================================== +# REDIS CACHE +# =================================== +REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD_64_CHARS_MINIMUM + +# =================================== +# JWT AUTHENTICATION +# =================================== +JWT_SECRET=CHANGE_ME_JWT_SECRET_512_BITS_MINIMUM + +# =================================== +# AWS CONFIGURATION +# =================================== +AWS_REGION=eu-west-3 +AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +AWS_SES_REGION=eu-west-1 + +# S3 Buckets +S3_BUCKET_DOCUMENTS=xpeditis-prod-documents +S3_BUCKET_UPLOADS=xpeditis-prod-uploads + +# =================================== +# EMAIL CONFIGURATION +# =================================== +EMAIL_SERVICE=ses +EMAIL_FROM=noreply@xpeditis.com +EMAIL_FROM_NAME=Xpeditis + +# =================================== +# MONITORING (Sentry) - REQUIRED +# =================================== +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +NEXT_PUBLIC_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id + +# =================================== +# ANALYTICS (Google Analytics) - REQUIRED +# =================================== +NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX + +# =================================== +# CARRIER APIs (Production) - REQUIRED +# =================================== +# Maersk Production +MAERSK_API_URL=https://api.maersk.com +MAERSK_API_KEY=your-maersk-production-api-key + +# MSC Production +MSC_API_URL=https://api.msc.com +MSC_API_KEY=your-msc-production-api-key + +# CMA CGM Production +CMA_CGM_API_URL=https://api.cma-cgm.com +CMA_CGM_API_KEY=your-cma-cgm-production-api-key + +# Hapag-Lloyd Production +HAPAG_LLOYD_API_URL=https://api.hapag-lloyd.com +HAPAG_LLOYD_API_KEY=your-hapag-lloyd-api-key + +# ONE (Ocean Network Express) +ONE_API_URL=https://api.one-line.com +ONE_API_KEY=your-one-api-key + +# =================================== +# SECURITY BEST PRACTICES +# =================================== +# ✅ Use AWS Secrets Manager for production secrets +# ✅ Rotate credentials every 90 days +# ✅ Enable AWS CloudTrail for audit logs +# ✅ Use IAM roles with least privilege +# ✅ Enable MFA on all AWS accounts +# ✅ Use strong passwords (min 64 characters, random) +# ✅ Never commit this file with real credentials +# ✅ Restrict database access to VPC only +# ✅ Enable SSL/TLS for all connections +# ✅ Monitor failed login attempts (Sentry) +# ✅ Setup automated backups (daily, 30-day retention) +# ✅ Test disaster recovery procedures monthly diff --git a/docker/.env.staging.example b/docker/.env.staging.example new file mode 100644 index 0000000..0398178 --- /dev/null +++ b/docker/.env.staging.example @@ -0,0 +1,82 @@ +# Xpeditis - Staging Environment Variables +# Copy this file to .env.staging and fill in the values + +# =================================== +# DOCKER REGISTRY +# =================================== +DOCKER_REGISTRY=docker.io +BACKEND_IMAGE=xpeditis/backend +BACKEND_TAG=staging-latest +FRONTEND_IMAGE=xpeditis/frontend +FRONTEND_TAG=staging-latest + +# =================================== +# DATABASE (PostgreSQL) +# =================================== +POSTGRES_DB=xpeditis_staging +POSTGRES_USER=xpeditis +POSTGRES_PASSWORD=CHANGE_ME_SECURE_PASSWORD_HERE + +# =================================== +# REDIS CACHE +# =================================== +REDIS_PASSWORD=CHANGE_ME_REDIS_PASSWORD_HERE + +# =================================== +# JWT AUTHENTICATION +# =================================== +JWT_SECRET=CHANGE_ME_JWT_SECRET_256_BITS_MINIMUM + +# =================================== +# AWS CONFIGURATION +# =================================== +AWS_REGION=eu-west-3 +AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE +AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +AWS_SES_REGION=eu-west-1 + +# S3 Buckets +S3_BUCKET_DOCUMENTS=xpeditis-staging-documents +S3_BUCKET_UPLOADS=xpeditis-staging-uploads + +# =================================== +# EMAIL CONFIGURATION +# =================================== +EMAIL_SERVICE=ses +EMAIL_FROM=noreply@staging.xpeditis.com +EMAIL_FROM_NAME=Xpeditis Staging + +# =================================== +# MONITORING (Sentry) +# =================================== +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +NEXT_PUBLIC_SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id + +# =================================== +# ANALYTICS (Google Analytics) +# =================================== +NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX + +# =================================== +# CARRIER APIs (Sandbox) +# =================================== +# Maersk Sandbox +MAERSK_API_URL_SANDBOX=https://sandbox.api.maersk.com +MAERSK_API_KEY_SANDBOX=your-maersk-sandbox-api-key + +# MSC Sandbox +MSC_API_URL_SANDBOX=https://sandbox.msc.com/api +MSC_API_KEY_SANDBOX=your-msc-sandbox-api-key + +# CMA CGM Sandbox +CMA_CGM_API_URL_SANDBOX=https://sandbox.cma-cgm.com/api +CMA_CGM_API_KEY_SANDBOX=your-cma-cgm-sandbox-api-key + +# =================================== +# NOTES +# =================================== +# 1. Never commit this file with real credentials +# 2. Use strong passwords (min 32 characters, random) +# 3. Rotate secrets regularly (every 90 days) +# 4. Use AWS Secrets Manager or similar for production +# 5. Enable MFA on all AWS accounts diff --git a/docker/PORTAINER_DEPLOYMENT_GUIDE.md b/docker/PORTAINER_DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..fbb523c --- /dev/null +++ b/docker/PORTAINER_DEPLOYMENT_GUIDE.md @@ -0,0 +1,419 @@ +# Guide de Déploiement Portainer - Xpeditis + +Ce guide explique comment déployer les stacks Xpeditis (staging et production) sur Portainer avec Traefik. + +--- + +## 📋 Prérequis + +### 1. Infrastructure Serveur +- **Serveur VPS/Dédié** avec Docker installé +- **Minimum**: 4 vCPU, 8 GB RAM, 100 GB SSD +- **Recommandé Production**: 8 vCPU, 16 GB RAM, 200 GB SSD +- **OS**: Ubuntu 22.04 LTS ou Debian 11+ + +### 2. Traefik déjà déployé +- Network `traefik_network` doit exister +- Let's Encrypt configuré (`letsencrypt` resolver) +- Ports 80 et 443 ouverts + +### 3. DNS Configuré +**Staging**: +- `staging.xpeditis.com` → IP du serveur +- `api-staging.xpeditis.com` → IP du serveur + +**Production**: +- `xpeditis.com` → IP du serveur +- `www.xpeditis.com` → IP du serveur +- `api.xpeditis.com` → IP du serveur + +### 4. Images Docker +Les images Docker doivent être buildées et pushées sur un registry (Docker Hub, GitHub Container Registry, ou privé): + +```bash +# Build backend +cd apps/backend +docker build -t xpeditis/backend:staging-latest . +docker push xpeditis/backend:staging-latest + +# Build frontend +cd apps/frontend +docker build -t xpeditis/frontend:staging-latest . +docker push xpeditis/frontend:staging-latest +``` + +--- + +## 🚀 Déploiement sur Portainer + +### Étape 1: Créer le network Traefik (si pas déjà fait) + +```bash +docker network create traefik_network +``` + +### Étape 2: Préparer les variables d'environnement + +#### Pour Staging: +1. Copier `.env.staging.example` vers `.env.staging` +2. Remplir toutes les valeurs (voir section Variables d'environnement ci-dessous) +3. **IMPORTANT**: Utiliser des mots de passe forts (min 32 caractères) + +#### Pour Production: +1. Copier `.env.production.example` vers `.env.production` +2. Remplir toutes les valeurs avec les credentials de production +3. **IMPORTANT**: Utiliser des mots de passe ultra-forts (min 64 caractères) + +### Étape 3: Déployer via Portainer UI + +#### A. Accéder à Portainer +- URL: `https://portainer.votre-domaine.com` (ou `http://IP:9000`) +- Login avec vos credentials admin + +#### B. Créer la Stack Staging + +1. **Aller dans**: Stacks → Add Stack +2. **Name**: `xpeditis-staging` +3. **Build method**: Web editor +4. **Copier le contenu** de `portainer-stack-staging.yml` +5. **Onglet "Environment variables"**: + - Cliquer sur "Load variables from .env file" + - Copier-coller le contenu de `.env.staging` + - OU ajouter manuellement chaque variable +6. **Cliquer**: Deploy the stack +7. **Vérifier**: Les 4 services doivent démarrer (postgres, redis, backend, frontend) + +#### C. Créer la Stack Production + +1. **Aller dans**: Stacks → Add Stack +2. **Name**: `xpeditis-production` +3. **Build method**: Web editor +4. **Copier le contenu** de `portainer-stack-production.yml` +5. **Onglet "Environment variables"**: + - Cliquer sur "Load variables from .env file" + - Copier-coller le contenu de `.env.production` + - OU ajouter manuellement chaque variable +6. **Cliquer**: Deploy the stack +7. **Vérifier**: Les 6 services doivent démarrer (postgres, redis, backend x2, frontend x2) + +--- + +## 🔐 Variables d'environnement Critiques + +### Variables Obligatoires (staging & production) + +| Variable | Description | Exemple | +|----------|-------------|---------| +| `POSTGRES_PASSWORD` | Mot de passe PostgreSQL | `XpEd1t1s_pG_S3cur3_2024!` | +| `REDIS_PASSWORD` | Mot de passe Redis | `R3d1s_C4ch3_P4ssw0rd!` | +| `JWT_SECRET` | Secret pour JWT tokens | `openssl rand -base64 64` | +| `AWS_ACCESS_KEY_ID` | AWS Access Key | `AKIAIOSFODNN7EXAMPLE` | +| `AWS_SECRET_ACCESS_KEY` | AWS Secret Key | `wJalrXUtnFEMI/K7MDENG/...` | +| `SENTRY_DSN` | Sentry monitoring URL | `https://xxx@sentry.io/123` | +| `MAERSK_API_KEY` | Clé API Maersk | Voir portail Maersk | + +### Générer des Secrets Sécurisés + +```bash +# PostgreSQL password (64 chars) +openssl rand -base64 48 + +# Redis password (64 chars) +openssl rand -base64 48 + +# JWT Secret (512 bits) +openssl rand -base64 64 + +# Generic secure password +pwgen -s 64 1 +``` + +--- + +## 🔍 Vérification du Déploiement + +### 1. Vérifier l'état des conteneurs + +Dans Portainer: +- **Stacks** → `xpeditis-staging` (ou production) +- Tous les services doivent être en status **running** (vert) + +### 2. Vérifier les logs + +Cliquer sur chaque service → **Logs** → Vérifier qu'il n'y a pas d'erreurs + +```bash +# Ou via CLI +docker logs xpeditis-backend-staging -f +docker logs xpeditis-frontend-staging -f +``` + +### 3. Vérifier les health checks + +```bash +# Backend health check +curl https://api-staging.xpeditis.com/health +# Réponse attendue: {"status":"ok","timestamp":"..."} + +# Frontend health check +curl https://staging.xpeditis.com/api/health +# Réponse attendue: {"status":"ok"} +``` + +### 4. Vérifier Traefik + +Dans Traefik dashboard: +- Routers: Doit afficher `xpeditis-backend-staging` et `xpeditis-frontend-staging` +- Services: Doit afficher les load balancers avec health checks verts +- Certificats: Let's Encrypt doit être vert + +### 5. Vérifier SSL + +```bash +# Vérifier certificat SSL +curl -I https://staging.xpeditis.com +# Header "Strict-Transport-Security" doit être présent + +# Test SSL avec SSLLabs +# https://www.ssllabs.com/ssltest/analyze.html?d=staging.xpeditis.com +``` + +### 6. Test Complet + +1. **Frontend**: Ouvrir `https://staging.xpeditis.com` dans un navigateur +2. **Backend**: Tester un endpoint: `https://api-staging.xpeditis.com/health` +3. **Login**: Créer un compte et se connecter +4. **Recherche de taux**: Tester une recherche Rotterdam → Shanghai +5. **Booking**: Créer un booking de test + +--- + +## 🐛 Dépannage + +### Problème 1: Service ne démarre pas + +**Symptôme**: Conteneur en status "Exited" ou "Restarting" + +**Solution**: +1. Vérifier les logs: Portainer → Service → Logs +2. Erreurs communes: + - `POSTGRES_PASSWORD` manquant → Ajouter la variable + - `Cannot connect to postgres` → Vérifier que postgres est en running + - `Redis connection refused` → Vérifier que redis est en running + - `Port already in use` → Un autre service utilise le port + +### Problème 2: Traefik ne route pas vers le service + +**Symptôme**: 404 Not Found ou Gateway Timeout + +**Solution**: +1. Vérifier que le network `traefik_network` existe: + ```bash + docker network ls | grep traefik + ``` +2. Vérifier que les services sont connectés au network: + ```bash + docker inspect xpeditis-backend-staging | grep traefik_network + ``` +3. Vérifier les labels Traefik dans Portainer → Service → Labels +4. Restart Traefik: + ```bash + docker restart traefik + ``` + +### Problème 3: SSL Certificate Failed + +**Symptôme**: "Your connection is not private" ou certificat invalide + +**Solution**: +1. Vérifier que DNS pointe vers le serveur: + ```bash + nslookup staging.xpeditis.com + ``` +2. Vérifier les logs Traefik: + ```bash + docker logs traefik | grep -i letsencrypt + ``` +3. Vérifier que ports 80 et 443 sont ouverts: + ```bash + sudo ufw status + sudo netstat -tlnp | grep -E '80|443' + ``` +4. Si nécessaire, supprimer le certificat et re-déployer: + ```bash + docker exec traefik rm /letsencrypt/acme.json + docker restart traefik + ``` + +### Problème 4: Database connection failed + +**Symptôme**: Backend logs montrent "Cannot connect to database" + +**Solution**: +1. Vérifier que PostgreSQL est en running +2. Vérifier les credentials: + ```bash + docker exec -it xpeditis-postgres-staging psql -U xpeditis -d xpeditis_staging + ``` +3. Vérifier le network interne: + ```bash + docker exec -it xpeditis-backend-staging ping postgres-staging + ``` + +### Problème 5: High memory usage + +**Symptôme**: Serveur lent, OOM killer + +**Solution**: +1. Vérifier l'utilisation mémoire: + ```bash + docker stats + ``` +2. Réduire les limites dans docker-compose (section `deploy.resources`) +3. Augmenter la RAM du serveur +4. Optimiser les queries PostgreSQL (indexes, explain analyze) + +--- + +## 🔄 Mise à Jour des Stacks + +### Update Rolling (Zero Downtime) + +#### Staging: +1. Build et push nouvelle image: + ```bash + docker build -t xpeditis/backend:staging-v1.2.0 . + docker push xpeditis/backend:staging-v1.2.0 + ``` +2. Dans Portainer → Stacks → `xpeditis-staging` → Editor +3. Changer `BACKEND_TAG=staging-v1.2.0` +4. Cliquer "Update the stack" +5. Portainer va pull la nouvelle image et redémarrer les services + +#### Production (avec High Availability): +La stack production a 2 instances de chaque service (backend-prod-1, backend-prod-2). Traefik va load balancer entre les deux. + +**Mise à jour sans downtime**: +1. Stopper `backend-prod-2` dans Portainer +2. Update l'image de `backend-prod-2` +3. Redémarrer `backend-prod-2` +4. Vérifier health check OK +5. Stopper `backend-prod-1` +6. Update l'image de `backend-prod-1` +7. Redémarrer `backend-prod-1` +8. Vérifier health check OK + +**OU via Portainer** (plus simple): +1. Portainer → Stacks → `xpeditis-production` → Editor +2. Changer `BACKEND_TAG=v1.2.0` +3. Cliquer "Update the stack" +4. Portainer va mettre à jour les services un par un (rolling update automatique) + +--- + +## 📊 Monitoring + +### 1. Portainer Built-in Monitoring + +Portainer → Containers → Sélectionner service → **Stats** +- CPU usage +- Memory usage +- Network I/O +- Block I/O + +### 2. Sentry (Error Tracking) + +Toutes les erreurs backend et frontend sont envoyées à Sentry (configuré via `SENTRY_DSN`) + +URL: https://sentry.io/organizations/xpeditis/projects/ + +### 3. Logs Centralisés + +**Voir tous les logs en temps réel**: +```bash +docker logs -f xpeditis-backend-staging +docker logs -f xpeditis-frontend-staging +docker logs -f xpeditis-postgres-staging +docker logs -f xpeditis-redis-staging +``` + +**Rechercher dans les logs**: +```bash +docker logs xpeditis-backend-staging 2>&1 | grep "ERROR" +docker logs xpeditis-backend-staging 2>&1 | grep "booking" +``` + +### 4. Health Checks Dashboard + +Créer un dashboard custom avec: +- Uptime Robot: https://uptimerobot.com (free tier: 50 monitors) +- Grafana + Prometheus (advanced) + +--- + +## 🔒 Sécurité Best Practices + +### 1. Mots de passe forts +✅ Min 64 caractères pour production +✅ Générés aléatoirement (openssl, pwgen) +✅ Stockés dans un gestionnaire de secrets (AWS Secrets Manager, Vault) + +### 2. Rotation des credentials +✅ Tous les 90 jours +✅ Immédiatement si compromis + +### 3. Backups automatiques +✅ PostgreSQL: Backup quotidien +✅ Retention: 30 jours staging, 90 jours production +✅ Test restore mensuel + +### 4. Monitoring actif +✅ Sentry configuré +✅ Uptime monitoring actif +✅ Alertes email/Slack pour downtime + +### 5. SSL/TLS +✅ HSTS activé (Strict-Transport-Security) +✅ TLS 1.2+ minimum +✅ Certificat Let's Encrypt auto-renew + +### 6. Rate Limiting +✅ Traefik rate limiting configuré +✅ Application-level rate limiting (NestJS throttler) +✅ Brute-force protection active + +### 7. Firewall +✅ Ports 80, 443 ouverts uniquement +✅ PostgreSQL/Redis accessibles uniquement depuis réseau interne Docker +✅ SSH avec clés uniquement (pas de mot de passe) + +--- + +## 📞 Support + +### En cas de problème critique: + +1. **Vérifier les logs** dans Portainer +2. **Vérifier Sentry** pour les erreurs récentes +3. **Restart du service** via Portainer (si safe) +4. **Rollback**: Portainer → Stacks → Redeploy previous version + +### Contacts: +- **Tech Lead**: david-henri.arnaud@3ds.com +- **DevOps**: ops@xpeditis.com +- **Support**: support@xpeditis.com + +--- + +## 📚 Ressources + +- **Portainer Docs**: https://docs.portainer.io/ +- **Traefik Docs**: https://doc.traefik.io/traefik/ +- **Docker Docs**: https://docs.docker.com/ +- **Let's Encrypt**: https://letsencrypt.org/docs/ + +--- + +*Dernière mise à jour*: 2025-10-14 +*Version*: 1.0.0 +*Auteur*: Xpeditis DevOps Team diff --git a/docker/portainer-stack-production.yml b/docker/portainer-stack-production.yml new file mode 100644 index 0000000..db58036 --- /dev/null +++ b/docker/portainer-stack-production.yml @@ -0,0 +1,456 @@ +version: '3.8' + +# Xpeditis - Stack PRODUCTION +# Portainer Stack avec Traefik reverse proxy +# Domaines: xpeditis.com (frontend) | api.xpeditis.com (backend) + +services: + # PostgreSQL Database + postgres-prod: + image: postgres:15-alpine + container_name: xpeditis-postgres-prod + restart: always + environment: + POSTGRES_DB: ${POSTGRES_DB:-xpeditis_prod} + POSTGRES_USER: ${POSTGRES_USER:-xpeditis} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?error} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data_prod:/var/lib/postgresql/data + - postgres_backups_prod:/backups + networks: + - xpeditis_internal_prod + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-xpeditis}"] + interval: 10s + timeout: 5s + retries: 5 + deploy: + resources: + limits: + cpus: '2' + memory: 4G + reservations: + cpus: '1' + memory: 2G + + # Redis Cache + redis-prod: + image: redis:7-alpine + container_name: xpeditis-redis-prod + restart: always + command: redis-server --requirepass ${REDIS_PASSWORD:?error} --maxmemory 1gb --maxmemory-policy allkeys-lru --appendonly yes + volumes: + - redis_data_prod:/data + networks: + - xpeditis_internal_prod + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + deploy: + resources: + limits: + cpus: '1' + memory: 1.5G + reservations: + cpus: '0.5' + memory: 1G + + # Backend API (NestJS) - Instance 1 + backend-prod-1: + image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-latest} + container_name: xpeditis-backend-prod-1 + restart: always + depends_on: + postgres-prod: + condition: service_healthy + redis-prod: + condition: service_healthy + environment: + # Application + NODE_ENV: production + PORT: 4000 + INSTANCE_ID: backend-prod-1 + + # Database + DATABASE_HOST: postgres-prod + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-xpeditis_prod} + DATABASE_USER: ${POSTGRES_USER:-xpeditis} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error} + DATABASE_SYNC: "false" + DATABASE_LOGGING: "false" + DATABASE_POOL_MIN: 10 + DATABASE_POOL_MAX: 50 + + # Redis + REDIS_HOST: redis-prod + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:?error} + + # JWT + JWT_SECRET: ${JWT_SECRET:?error} + JWT_ACCESS_EXPIRATION: 15m + JWT_REFRESH_EXPIRATION: 7d + + # CORS + CORS_ORIGIN: https://xpeditis.com,https://www.xpeditis.com + + # Sentry (Monitoring) + SENTRY_DSN: ${SENTRY_DSN:?error} + SENTRY_ENVIRONMENT: production + SENTRY_TRACES_SAMPLE_RATE: 0.1 + SENTRY_PROFILES_SAMPLE_RATE: 0.05 + + # AWS S3 + AWS_REGION: ${AWS_REGION:-eu-west-3} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error} + S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-prod-documents} + S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-prod-uploads} + + # Email (AWS SES) + EMAIL_SERVICE: ses + EMAIL_FROM: ${EMAIL_FROM:-noreply@xpeditis.com} + EMAIL_FROM_NAME: Xpeditis + AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1} + + # Carrier APIs (Production) + MAERSK_API_URL: ${MAERSK_API_URL:-https://api.maersk.com} + MAERSK_API_KEY: ${MAERSK_API_KEY:?error} + MSC_API_URL: ${MSC_API_URL:-} + MSC_API_KEY: ${MSC_API_KEY:-} + CMA_CGM_API_URL: ${CMA_CGM_API_URL:-} + CMA_CGM_API_KEY: ${CMA_CGM_API_KEY:-} + + # Security + RATE_LIMIT_GLOBAL: 100 + RATE_LIMIT_AUTH: 5 + RATE_LIMIT_SEARCH: 30 + RATE_LIMIT_BOOKING: 20 + + volumes: + - backend_logs_prod:/app/logs + networks: + - xpeditis_internal_prod + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # HTTPS Route + - "traefik.http.routers.xpeditis-backend-prod.rule=Host(`api.xpeditis.com`)" + - "traefik.http.routers.xpeditis-backend-prod.entrypoints=websecure" + - "traefik.http.routers.xpeditis-backend-prod.tls=true" + - "traefik.http.routers.xpeditis-backend-prod.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-backend-prod.priority=200" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.server.port=4000" + - "traefik.http.routers.xpeditis-backend-prod.middlewares=xpeditis-backend-prod-headers,xpeditis-backend-prod-security,xpeditis-backend-prod-ratelimit" + + # HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-backend-prod-http.rule=Host(`api.xpeditis.com`)" + - "traefik.http.routers.xpeditis-backend-prod-http.entrypoints=web" + - "traefik.http.routers.xpeditis-backend-prod-http.priority=200" + - "traefik.http.routers.xpeditis-backend-prod-http.middlewares=xpeditis-backend-prod-redirect" + - "traefik.http.routers.xpeditis-backend-prod-http.service=xpeditis-backend-prod" + - "traefik.http.middlewares.xpeditis-backend-prod-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-backend-prod-redirect.redirectscheme.permanent=true" + + # Middleware Headers + - "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-backend-prod-headers.headers.customRequestHeaders.X-Real-IP=" + + # Security Headers (Strict Production) + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.frameDeny=true" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.browserXssFilter=true" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsSeconds=63072000" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.stsPreload=true" + - "traefik.http.middlewares.xpeditis-backend-prod-security.headers.forceSTSHeader=true" + + # Rate Limiting (Stricter in Production) + - "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.average=50" + - "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.burst=100" + - "traefik.http.middlewares.xpeditis-backend-prod-ratelimit.ratelimit.period=1m" + + # Health Check + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.path=/health" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.interval=30s" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.healthcheck.timeout=5s" + + # Load Balancing (Sticky Sessions) + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie=true" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.name=xpeditis_backend_route" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.secure=true" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.sticky.cookie.httpOnly=true" + + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G + + # Backend API (NestJS) - Instance 2 (High Availability) + backend-prod-2: + image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-latest} + container_name: xpeditis-backend-prod-2 + restart: always + depends_on: + postgres-prod: + condition: service_healthy + redis-prod: + condition: service_healthy + environment: + # Application + NODE_ENV: production + PORT: 4000 + INSTANCE_ID: backend-prod-2 + + # Database + DATABASE_HOST: postgres-prod + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-xpeditis_prod} + DATABASE_USER: ${POSTGRES_USER:-xpeditis} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error} + DATABASE_SYNC: "false" + DATABASE_LOGGING: "false" + DATABASE_POOL_MIN: 10 + DATABASE_POOL_MAX: 50 + + # Redis + REDIS_HOST: redis-prod + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:?error} + + # JWT + JWT_SECRET: ${JWT_SECRET:?error} + JWT_ACCESS_EXPIRATION: 15m + JWT_REFRESH_EXPIRATION: 7d + + # CORS + CORS_ORIGIN: https://xpeditis.com,https://www.xpeditis.com + + # Sentry (Monitoring) + SENTRY_DSN: ${SENTRY_DSN:?error} + SENTRY_ENVIRONMENT: production + SENTRY_TRACES_SAMPLE_RATE: 0.1 + SENTRY_PROFILES_SAMPLE_RATE: 0.05 + + # AWS S3 + AWS_REGION: ${AWS_REGION:-eu-west-3} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error} + S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-prod-documents} + S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-prod-uploads} + + # Email (AWS SES) + EMAIL_SERVICE: ses + EMAIL_FROM: ${EMAIL_FROM:-noreply@xpeditis.com} + EMAIL_FROM_NAME: Xpeditis + AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1} + + # Carrier APIs (Production) + MAERSK_API_URL: ${MAERSK_API_URL:-https://api.maersk.com} + MAERSK_API_KEY: ${MAERSK_API_KEY:?error} + MSC_API_URL: ${MSC_API_URL:-} + MSC_API_KEY: ${MSC_API_KEY:-} + CMA_CGM_API_URL: ${CMA_CGM_API_URL:-} + CMA_CGM_API_KEY: ${CMA_CGM_API_KEY:-} + + # Security + RATE_LIMIT_GLOBAL: 100 + RATE_LIMIT_AUTH: 5 + RATE_LIMIT_SEARCH: 30 + RATE_LIMIT_BOOKING: 20 + + volumes: + - backend_logs_prod:/app/logs + networks: + - xpeditis_internal_prod + - traefik_network + labels: + # Same Traefik labels as backend-prod-1 (load balanced) + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + - "traefik.http.routers.xpeditis-backend-prod.rule=Host(`api.xpeditis.com`)" + - "traefik.http.services.xpeditis-backend-prod.loadbalancer.server.port=4000" + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G + + # Frontend (Next.js) - Instance 1 + frontend-prod-1: + image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-latest} + container_name: xpeditis-frontend-prod-1 + restart: always + depends_on: + - backend-prod-1 + - backend-prod-2 + environment: + NODE_ENV: production + NEXT_PUBLIC_API_URL: https://api.xpeditis.com + NEXT_PUBLIC_APP_URL: https://xpeditis.com + NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:?error} + NEXT_PUBLIC_SENTRY_ENVIRONMENT: production + NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:?error} + + # Backend API for SSR (internal load balanced) + API_URL: http://backend-prod-1:4000 + + networks: + - xpeditis_internal_prod + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # HTTPS Route + - "traefik.http.routers.xpeditis-frontend-prod.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)" + - "traefik.http.routers.xpeditis-frontend-prod.entrypoints=websecure" + - "traefik.http.routers.xpeditis-frontend-prod.tls=true" + - "traefik.http.routers.xpeditis-frontend-prod.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-frontend-prod.priority=200" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.server.port=3000" + - "traefik.http.routers.xpeditis-frontend-prod.middlewares=xpeditis-frontend-prod-headers,xpeditis-frontend-prod-security,xpeditis-frontend-prod-compress,xpeditis-frontend-prod-www-redirect" + + # HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-frontend-prod-http.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)" + - "traefik.http.routers.xpeditis-frontend-prod-http.entrypoints=web" + - "traefik.http.routers.xpeditis-frontend-prod-http.priority=200" + - "traefik.http.routers.xpeditis-frontend-prod-http.middlewares=xpeditis-frontend-prod-redirect" + - "traefik.http.routers.xpeditis-frontend-prod-http.service=xpeditis-frontend-prod" + - "traefik.http.middlewares.xpeditis-frontend-prod-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-frontend-prod-redirect.redirectscheme.permanent=true" + + # WWW → non-WWW Redirect + - "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.regex=^https://www\\.(.+)" + - "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.replacement=https://$${1}" + - "traefik.http.middlewares.xpeditis-frontend-prod-www-redirect.redirectregex.permanent=true" + + # Middleware Headers + - "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-frontend-prod-headers.headers.customRequestHeaders.X-Real-IP=" + + # Security Headers (Strict Production) + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.frameDeny=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.browserXssFilter=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsSeconds=63072000" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.stsPreload=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.forceSTSHeader=true" + - "traefik.http.middlewares.xpeditis-frontend-prod-security.headers.contentSecurityPolicy=default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.xpeditis.com;" + + # Compression + - "traefik.http.middlewares.xpeditis-frontend-prod-compress.compress=true" + + # Health Check + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.path=/api/health" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.interval=30s" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.healthcheck.timeout=5s" + + # Load Balancing (Sticky Sessions) + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie=true" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.name=xpeditis_frontend_route" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.secure=true" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.sticky.cookie.httpOnly=true" + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G + + # Frontend (Next.js) - Instance 2 (High Availability) + frontend-prod-2: + image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-latest} + container_name: xpeditis-frontend-prod-2 + restart: always + depends_on: + - backend-prod-1 + - backend-prod-2 + environment: + NODE_ENV: production + NEXT_PUBLIC_API_URL: https://api.xpeditis.com + NEXT_PUBLIC_APP_URL: https://xpeditis.com + NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:?error} + NEXT_PUBLIC_SENTRY_ENVIRONMENT: production + NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:?error} + + # Backend API for SSR (internal load balanced) + API_URL: http://backend-prod-2:4000 + + networks: + - xpeditis_internal_prod + - traefik_network + labels: + # Same Traefik labels as frontend-prod-1 (load balanced) + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + - "traefik.http.routers.xpeditis-frontend-prod.rule=Host(`xpeditis.com`) || Host(`www.xpeditis.com`)" + - "traefik.http.services.xpeditis-frontend-prod.loadbalancer.server.port=3000" + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G + +networks: + xpeditis_internal_prod: + driver: bridge + name: xpeditis_internal_prod + traefik_network: + external: true + +volumes: + postgres_data_prod: + name: xpeditis_postgres_data_prod + postgres_backups_prod: + name: xpeditis_postgres_backups_prod + redis_data_prod: + name: xpeditis_redis_data_prod + backend_logs_prod: + name: xpeditis_backend_logs_prod diff --git a/docker/portainer-stack-staging.yml b/docker/portainer-stack-staging.yml new file mode 100644 index 0000000..a9c8843 --- /dev/null +++ b/docker/portainer-stack-staging.yml @@ -0,0 +1,253 @@ +version: '3.8' + +# Xpeditis - Stack STAGING/PREPROD +# Portainer Stack avec Traefik reverse proxy +# Domaines: staging.xpeditis.com (frontend) | api-staging.xpeditis.com (backend) + +services: + # PostgreSQL Database + postgres-staging: + image: postgres:15-alpine + container_name: xpeditis-postgres-staging + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-xpeditis_staging} + POSTGRES_USER: ${POSTGRES_USER:-xpeditis} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?error} + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data_staging:/var/lib/postgresql/data + networks: + - xpeditis_internal_staging + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-xpeditis}"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis Cache + redis-staging: + image: redis:7-alpine + container_name: xpeditis-redis-staging + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD:?error} --maxmemory 512mb --maxmemory-policy allkeys-lru + volumes: + - redis_data_staging:/data + networks: + - xpeditis_internal_staging + healthcheck: + test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + interval: 10s + timeout: 3s + retries: 5 + + # Backend API (NestJS) + backend-staging: + image: ${DOCKER_REGISTRY:-docker.io}/${BACKEND_IMAGE:-xpeditis/backend}:${BACKEND_TAG:-staging-latest} + container_name: xpeditis-backend-staging + restart: unless-stopped + depends_on: + postgres-staging: + condition: service_healthy + redis-staging: + condition: service_healthy + environment: + # Application + NODE_ENV: staging + PORT: 4000 + + # Database + DATABASE_HOST: postgres-staging + DATABASE_PORT: 5432 + DATABASE_NAME: ${POSTGRES_DB:-xpeditis_staging} + DATABASE_USER: ${POSTGRES_USER:-xpeditis} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD:?error} + DATABASE_SYNC: "false" + DATABASE_LOGGING: "true" + + # Redis + REDIS_HOST: redis-staging + REDIS_PORT: 6379 + REDIS_PASSWORD: ${REDIS_PASSWORD:?error} + + # JWT + JWT_SECRET: ${JWT_SECRET:?error} + JWT_ACCESS_EXPIRATION: 15m + JWT_REFRESH_EXPIRATION: 7d + + # CORS + CORS_ORIGIN: https://staging.xpeditis.com,http://localhost:3000 + + # Sentry (Monitoring) + SENTRY_DSN: ${SENTRY_DSN:-} + SENTRY_ENVIRONMENT: staging + SENTRY_TRACES_SAMPLE_RATE: 0.1 + SENTRY_PROFILES_SAMPLE_RATE: 0.05 + + # AWS S3 (or MinIO) + AWS_REGION: ${AWS_REGION:-eu-west-3} + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:?error} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:?error} + S3_BUCKET_DOCUMENTS: ${S3_BUCKET_DOCUMENTS:-xpeditis-staging-documents} + S3_BUCKET_UPLOADS: ${S3_BUCKET_UPLOADS:-xpeditis-staging-uploads} + + # Email (AWS SES or SMTP) + EMAIL_SERVICE: ${EMAIL_SERVICE:-ses} + EMAIL_FROM: ${EMAIL_FROM:-noreply@staging.xpeditis.com} + EMAIL_FROM_NAME: Xpeditis Staging + AWS_SES_REGION: ${AWS_SES_REGION:-eu-west-1} + + # Carrier APIs (Sandbox) + MAERSK_API_URL: ${MAERSK_API_URL_SANDBOX:-https://sandbox.api.maersk.com} + MAERSK_API_KEY: ${MAERSK_API_KEY_SANDBOX:-} + MSC_API_URL: ${MSC_API_URL_SANDBOX:-} + MSC_API_KEY: ${MSC_API_KEY_SANDBOX:-} + + # Security + RATE_LIMIT_GLOBAL: 200 + RATE_LIMIT_AUTH: 10 + RATE_LIMIT_SEARCH: 50 + RATE_LIMIT_BOOKING: 30 + + volumes: + - backend_logs_staging:/app/logs + networks: + - xpeditis_internal_staging + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # HTTPS Route + - "traefik.http.routers.xpeditis-backend-staging.rule=Host(`api-staging.xpeditis.com`)" + - "traefik.http.routers.xpeditis-backend-staging.entrypoints=websecure" + - "traefik.http.routers.xpeditis-backend-staging.tls=true" + - "traefik.http.routers.xpeditis-backend-staging.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-backend-staging.priority=100" + - "traefik.http.services.xpeditis-backend-staging.loadbalancer.server.port=4000" + - "traefik.http.routers.xpeditis-backend-staging.middlewares=xpeditis-backend-staging-headers,xpeditis-backend-staging-security" + + # HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-backend-staging-http.rule=Host(`api-staging.xpeditis.com`)" + - "traefik.http.routers.xpeditis-backend-staging-http.entrypoints=web" + - "traefik.http.routers.xpeditis-backend-staging-http.priority=100" + - "traefik.http.routers.xpeditis-backend-staging-http.middlewares=xpeditis-backend-staging-redirect" + - "traefik.http.routers.xpeditis-backend-staging-http.service=xpeditis-backend-staging" + - "traefik.http.middlewares.xpeditis-backend-staging-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-backend-staging-redirect.redirectscheme.permanent=true" + + # Middleware Headers + - "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-backend-staging-headers.headers.customRequestHeaders.X-Real-IP=" + + # Security Headers + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.frameDeny=true" + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.browserXssFilter=true" + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsSeconds=31536000" + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.xpeditis-backend-staging-security.headers.stsPreload=true" + + # Rate Limiting + - "traefik.http.middlewares.xpeditis-backend-staging-ratelimit.ratelimit.average=100" + - "traefik.http.middlewares.xpeditis-backend-staging-ratelimit.ratelimit.burst=200" + + # Health Check + - "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.path=/health" + - "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.interval=30s" + - "traefik.http.services.xpeditis-backend-staging.loadbalancer.healthcheck.timeout=5s" + + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:4000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Frontend (Next.js) + frontend-staging: + image: ${DOCKER_REGISTRY:-docker.io}/${FRONTEND_IMAGE:-xpeditis/frontend}:${FRONTEND_TAG:-staging-latest} + container_name: xpeditis-frontend-staging + restart: unless-stopped + depends_on: + - backend-staging + environment: + NODE_ENV: staging + NEXT_PUBLIC_API_URL: https://api-staging.xpeditis.com + NEXT_PUBLIC_APP_URL: https://staging.xpeditis.com + NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN:-} + NEXT_PUBLIC_SENTRY_ENVIRONMENT: staging + NEXT_PUBLIC_GA_MEASUREMENT_ID: ${NEXT_PUBLIC_GA_MEASUREMENT_ID:-} + + # Backend API for SSR (internal) + API_URL: http://backend-staging:4000 + + networks: + - xpeditis_internal_staging + - traefik_network + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik_network" + + # HTTPS Route + - "traefik.http.routers.xpeditis-frontend-staging.rule=Host(`staging.xpeditis.com`)" + - "traefik.http.routers.xpeditis-frontend-staging.entrypoints=websecure" + - "traefik.http.routers.xpeditis-frontend-staging.tls=true" + - "traefik.http.routers.xpeditis-frontend-staging.tls.certresolver=letsencrypt" + - "traefik.http.routers.xpeditis-frontend-staging.priority=100" + - "traefik.http.services.xpeditis-frontend-staging.loadbalancer.server.port=3000" + - "traefik.http.routers.xpeditis-frontend-staging.middlewares=xpeditis-frontend-staging-headers,xpeditis-frontend-staging-security,xpeditis-frontend-staging-compress" + + # HTTP → HTTPS Redirect + - "traefik.http.routers.xpeditis-frontend-staging-http.rule=Host(`staging.xpeditis.com`)" + - "traefik.http.routers.xpeditis-frontend-staging-http.entrypoints=web" + - "traefik.http.routers.xpeditis-frontend-staging-http.priority=100" + - "traefik.http.routers.xpeditis-frontend-staging-http.middlewares=xpeditis-frontend-staging-redirect" + - "traefik.http.routers.xpeditis-frontend-staging-http.service=xpeditis-frontend-staging" + - "traefik.http.middlewares.xpeditis-frontend-staging-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.xpeditis-frontend-staging-redirect.redirectscheme.permanent=true" + + # Middleware Headers + - "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Forwarded-Proto=https" + - "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Forwarded-For=" + - "traefik.http.middlewares.xpeditis-frontend-staging-headers.headers.customRequestHeaders.X-Real-IP=" + + # Security Headers + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.frameDeny=true" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.contentTypeNosniff=true" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.browserXssFilter=true" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsSeconds=31536000" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsIncludeSubdomains=true" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.stsPreload=true" + - "traefik.http.middlewares.xpeditis-frontend-staging-security.headers.customResponseHeaders.X-Robots-Tag=noindex,nofollow" + + # Compression + - "traefik.http.middlewares.xpeditis-frontend-staging-compress.compress=true" + + # Health Check + - "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.path=/api/health" + - "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.interval=30s" + - "traefik.http.services.xpeditis-frontend-staging.loadbalancer.healthcheck.timeout=5s" + + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + xpeditis_internal_staging: + driver: bridge + name: xpeditis_internal_staging + traefik_network: + external: true + +volumes: + postgres_data_staging: + name: xpeditis_postgres_data_staging + redis_data_staging: + name: xpeditis_redis_data_staging + backend_logs_staging: + name: xpeditis_backend_logs_staging