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