xpeditis2.0/docker/portainer-stack-production.yml
David 08787c89c8
Some checks failed
Dev CI / Unit Tests (${{ matrix.app }}) (backend) (push) Blocked by required conditions
Dev CI / Unit Tests (${{ matrix.app }}) (frontend) (push) Blocked by required conditions
Dev CI / Notify Failure (push) Blocked by required conditions
Dev CI / Quality (${{ matrix.app }}) (backend) (push) Has been cancelled
Dev CI / Quality (${{ matrix.app }}) (frontend) (push) Has been cancelled
chore: sync full codebase from cicd branch
Aligns dev with the complete application codebase (cicd branch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 12:56:16 +02:00

457 lines
18 KiB
YAML

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