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