version: '3.8' services: backend: image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest} networks: - photo-sharing - traefik-public environment: - NODE_ENV=production - PORT=3000 - JWT_SECRET_FILE=/run/secrets/jwt_secret - ADMIN_URL=${ADMIN_URL} - FRONTEND_URL=${FRONTEND_URL} - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_SECURE=${SMTP_SECURE} - SMTP_USER_FILE=/run/secrets/smtp_user - SMTP_PASS_FILE=/run/secrets/smtp_pass - EMAIL_FROM=${EMAIL_FROM} - UMAMI_URL=${UMAMI_URL} - UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID} - DB_HOST=db - DB_PORT=5432 - DB_NAME=${DB_NAME:-photo_sharing} - DB_USER_FILE=/run/secrets/db_user - DB_PASSWORD_FILE=/run/secrets/db_password secrets: - jwt_secret - smtp_user - smtp_pass - db_user - db_password volumes: - photo-storage:/app/storage - app-data:/app/data - app-logs:/app/logs deploy: replicas: 3 update_config: parallelism: 1 delay: 10s failure_action: rollback max_failure_ratio: 0.3 restart_policy: condition: on-failure delay: 5s max_attempts: 3 resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.25' memory: 128M labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.constraint-label=traefik-public" - "traefik.http.routers.backend.rule=Host(`${BACKEND_HOST}`) && PathPrefix(`/api`)" - "traefik.http.routers.backend.entrypoints=https" - "traefik.http.routers.backend.tls=true" - "traefik.http.routers.backend.tls.certresolver=letsencrypt" - "traefik.http.services.backend.loadbalancer.server.port=3000" - "traefik.http.services.backend.loadbalancer.healthcheck.path=/api/health" - "traefik.http.services.backend.loadbalancer.healthcheck.interval=10s" healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s frontend: image: ${REGISTRY_URL}/photo-sharing-frontend:${VERSION:-latest} networks: - photo-sharing - traefik-public deploy: replicas: 2 update_config: parallelism: 1 delay: 10s failure_action: rollback restart_policy: condition: on-failure resources: limits: cpus: '0.5' memory: 256M reservations: cpus: '0.1' memory: 64M labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.constraint-label=traefik-public" - "traefik.http.routers.frontend.rule=Host(`${FRONTEND_HOST}`)" - "traefik.http.routers.frontend.entrypoints=https" - "traefik.http.routers.frontend.tls=true" - "traefik.http.routers.frontend.tls.certresolver=letsencrypt" - "traefik.http.services.frontend.loadbalancer.server.port=80" - "traefik.http.middlewares.frontend-compress.compress=true" - "traefik.http.routers.frontend.middlewares=frontend-compress" db: image: postgres:14-alpine networks: - photo-sharing environment: - POSTGRES_USER_FILE=/run/secrets/db_user - POSTGRES_PASSWORD_FILE=/run/secrets/db_password - POSTGRES_DB=${DB_NAME:-photo_sharing} secrets: - db_user - db_password volumes: - postgres-data:/var/lib/postgresql/data deploy: placement: constraints: - node.labels.db == true restart_policy: condition: on-failure resources: limits: cpus: '2' memory: 1G reservations: cpus: '0.5' memory: 256M healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 # Background workers as separate services for better control email-worker: image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest} command: ["node", "src/services/emailService.js"] networks: - photo-sharing environment: - NODE_ENV=production - JWT_SECRET_FILE=/run/secrets/jwt_secret - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_SECURE=${SMTP_SECURE} - SMTP_USER_FILE=/run/secrets/smtp_user - SMTP_PASS_FILE=/run/secrets/smtp_pass - EMAIL_FROM=${EMAIL_FROM} secrets: - jwt_secret - smtp_user - smtp_pass volumes: - app-data:/app/data - app-logs:/app/logs deploy: replicas: 1 restart_policy: condition: on-failure delay: 5s resources: limits: cpus: '0.5' memory: 256M expiration-checker: image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest} command: ["node", "src/services/expirationChecker.js"] networks: - photo-sharing environment: - NODE_ENV=production volumes: - photo-storage:/app/storage - app-data:/app/data - app-logs:/app/logs deploy: replicas: 1 restart_policy: condition: on-failure delay: 5s resources: limits: cpus: '0.5' memory: 256M archive-worker: image: ${REGISTRY_URL}/photo-sharing-backend:${VERSION:-latest} command: ["node", "src/services/archiveService.js"] networks: - photo-sharing environment: - NODE_ENV=production volumes: - photo-storage:/app/storage - app-data:/app/data - app-logs:/app/logs deploy: replicas: 1 restart_policy: condition: on-failure delay: 5s resources: limits: cpus: '1' memory: 512M # Umami Analytics umami: image: ghcr.io/umami-software/umami:postgresql-latest networks: - photo-sharing - traefik-public environment: DATABASE_URL: postgresql://umami:${UMAMI_DB_PASSWORD}@db:5432/umami DATABASE_TYPE: postgresql HASH_SALT: ${UMAMI_HASH_SALT} depends_on: - db deploy: replicas: 1 restart_policy: condition: on-failure labels: - "traefik.enable=true" - "traefik.docker.network=traefik-public" - "traefik.constraint-label=traefik-public" - "traefik.http.routers.umami.rule=Host(`${UMAMI_HOST}`)" - "traefik.http.routers.umami.entrypoints=https" - "traefik.http.routers.umami.tls=true" - "traefik.http.routers.umami.tls.certresolver=letsencrypt" - "traefik.http.services.umami.loadbalancer.server.port=3000" networks: photo-sharing: driver: overlay attachable: true traefik-public: external: true volumes: postgres-data: driver: local photo-storage: driver: local app-data: driver: local app-logs: driver: local secrets: jwt_secret: external: true smtp_user: external: true smtp_pass: external: true db_user: external: true db_password: external: true