version: "3.8" x-app-environment: &app-environment APP_ENV: production DATABASE_URL_FILE: /run/secrets/database_url S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} S3_PUBLIC_ENDPOINT: ${R2_PUBLIC_ENDPOINT:?set R2_PUBLIC_ENDPOINT} S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET} AWS_ACCESS_KEY_ID_FILE: /run/secrets/r2_access_key_id AWS_SECRET_ACCESS_KEY_FILE: /run/secrets/r2_secret_access_key AWS_DEFAULT_REGION: auto OPERATOR_USER: ${OPERATOR_USER:?set OPERATOR_USER} OPERATOR_PASSWORD_FILE: /run/secrets/operator_password PAYMENT_ADAPTER: ${PAYMENT_ADAPTER:?set PAYMENT_ADAPTER} FREIGHT_ADAPTER: ${FREIGHT_ADAPTER:?set FREIGHT_ADAPTER} TINY_ADAPTER: ${TINY_ADAPTER:?set TINY_ADAPTER} WHATSAPP_ADAPTER: ${WHATSAPP_ADAPTER:?set WHATSAPP_ADAPTER} STORAGE_ADAPTER: s3-r2 PAYMENT_TOKEN_FILE: /run/secrets/payment_token PAYMENT_WEBHOOK_SECRET_FILE: /run/secrets/payment_webhook_secret TINY_TOKEN_FILE: /run/secrets/tiny_token WHATSAPP_TOKEN_FILE: /run/secrets/whatsapp_token PUBLIC_ORIGIN: ${PUBLIC_ORIGIN:?set PUBLIC_ORIGIN} PUBLIC_HOST: ${PUBLIC_HOST:?set PUBLIC_HOST} ALLOWED_HOSTS: ${PUBLIC_HOST:?set PUBLIC_HOST},${KANBAN_HOST:?set KANBAN_HOST} ALLOWED_ORIGINS: ${PUBLIC_ORIGIN:?set PUBLIC_ORIGIN},https://${KANBAN_HOST:?set KANBAN_HOST} COOKIE_SECURE: "true" MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-5368709120} UPLOAD_PART_BYTES: ${UPLOAD_PART_BYTES:-8388608} STORAGE_QUOTA_BYTES: ${STORAGE_QUOTA_BYTES:?set STORAGE_QUOTA_BYTES} OWNER_UPLOAD_QUOTA_BYTES: ${OWNER_UPLOAD_QUOTA_BYTES:?set OWNER_UPLOAD_QUOTA_BYTES} MAX_PENDING_UPLOADS: ${MAX_PENDING_UPLOADS:-10} SCAN_MAX_BYTES: ${SCAN_MAX_BYTES:-134217728} x-app-secrets: &app-secrets - database_url - r2_access_key_id - r2_secret_access_key - operator_password - payment_token - payment_webhook_secret - tiny_token - whatsapp_token x-rolling: &rolling update_config: parallelism: 1 delay: 10s order: start-first failure_action: rollback monitor: 45s rollback_config: parallelism: 1 delay: 5s order: start-first failure_action: pause monitor: 45s restart_policy: condition: on-failure delay: 5s max_attempts: 5 window: 60s services: db: image: ${POSTGRES_IMAGE:?set POSTGRES_IMAGE} environment: POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER} POSTGRES_PASSWORD_FILE: /run/secrets/db_admin_password secrets: [db_admin_password] volumes: - postgres-data:/var/lib/postgresql/data networks: [backend] healthcheck: test: [CMD-SHELL, 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'] interval: 10s timeout: 5s retries: 12 start_period: 20s stop_grace_period: 60s deploy: replicas: 1 placement: constraints: [node.labels.dtf_database == true] update_config: parallelism: 1 order: stop-first failure_action: rollback monitor: 60s rollback_config: parallelism: 1 order: stop-first failure_action: pause monitor: 60s restart_policy: condition: on-failure delay: 10s max_attempts: 5 window: 120s resources: limits: {cpus: "2.0", memory: 4G} reservations: {cpus: "0.5", memory: 1G} db-init: image: ${API_IMAGE:?set API_IMAGE}:${IMAGE_TAG:-latest} command: python -m local.bootstrap environment: APP_ENV: production DATABASE_ADMIN_URL_FILE: /run/secrets/database_admin_url APP_DB_USER: ${APP_DB_USER:?set APP_DB_USER} APP_DB_PASSWORD_FILE: /run/secrets/app_db_password secrets: [database_admin_url, app_db_password] networks: [backend] deploy: replicas: 1 restart_policy: {condition: none} placement: constraints: [node.platform.os == linux] resources: limits: {cpus: "0.5", memory: 512M} scanner: image: ${CLAMAV_IMAGE:?set CLAMAV_IMAGE} user: "100:101" entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf] configs: - source: clamd_config target: /etc/clamav/clamd.conf mode: 0444 networks: [backend] read_only: true cap_drop: [ALL] security_opt: [no-new-privileges:true] tmpfs: - /tmp:uid=100,gid=101,mode=0750 - /run/clamav:uid=100,gid=101,mode=0750 - /var/log/clamav:uid=100,gid=101,mode=0750 healthcheck: test: [CMD, clamdscan, --config-file=/etc/clamav/clamd.conf, --ping, "3"] interval: 15s timeout: 5s retries: 20 start_period: 90s deploy: replicas: 1 restart_policy: {condition: on-failure, delay: 10s} resources: limits: {cpus: "2.0", memory: 3G} reservations: {cpus: "0.5", memory: 1G} api: image: ${API_IMAGE:?set API_IMAGE}:${IMAGE_TAG:-latest} environment: *app-environment secrets: *app-secrets networks: [backend, egress] read_only: true tmpfs: [/tmp] init: true cap_drop: [ALL] security_opt: [no-new-privileges:true] healthcheck: test: - CMD-SHELL - >- python -c "import os,urllib.request; r=urllib.request.Request('http://localhost:8000/health',headers={'Host':os.environ['PUBLIC_HOST']}); urllib.request.urlopen(r,timeout=3)" interval: 10s timeout: 5s retries: 12 start_period: 30s stop_grace_period: 30s deploy: <<: *rolling replicas: 2 resources: limits: {cpus: "1.0", memory: 1G} reservations: {cpus: "0.25", memory: 256M} worker: image: ${API_IMAGE:?set API_IMAGE}:${IMAGE_TAG:-latest} command: python -m local.worker environment: <<: *app-environment CLAMD_HOST: scanner secrets: *app-secrets networks: [backend, egress] read_only: true tmpfs: [/tmp] init: true cap_drop: [ALL] security_opt: [no-new-privileges:true] healthcheck: test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8002/health',timeout=3)"] interval: 15s timeout: 5s retries: 12 start_period: 90s stop_grace_period: 60s deploy: <<: *rolling replicas: 1 update_config: parallelism: 1 order: stop-first failure_action: rollback monitor: 60s rollback_config: parallelism: 1 order: stop-first failure_action: pause monitor: 60s resources: limits: {cpus: "1.5", memory: 2G} reservations: {cpus: "0.25", memory: 512M} site: image: ${WEB_IMAGE:?set WEB_IMAGE}:${IMAGE_TAG:-latest} environment: WEB_INDEX: index.html PUBLIC_HOST: ${PUBLIC_HOST:?set PUBLIC_HOST} S3_PUBLIC_ENDPOINT: ${R2_PUBLIC_ENDPOINT:?set R2_PUBLIC_ENDPOINT} networks: [backend] ports: - target: 8080 published: ${SITE_PORT:-8080} protocol: tcp mode: ingress read_only: true tmpfs: - /tmp:uid=101,gid=101,mode=0750 - /var/cache/nginx:uid=101,gid=101,mode=0750 - /var/run:uid=101,gid=101,mode=0750 - /etc/nginx/conf.d:uid=101,gid=101,mode=0750 cap_drop: [ALL] security_opt: [no-new-privileges:true] healthcheck: test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health'] interval: 10s timeout: 5s retries: 12 start_period: 15s deploy: <<: *rolling replicas: 2 resources: limits: {cpus: "0.5", memory: 256M} reservations: {cpus: "0.1", memory: 64M} kanban: image: ${WEB_IMAGE:?set WEB_IMAGE}:${IMAGE_TAG:-latest} environment: WEB_INDEX: kanban.html PUBLIC_HOST: ${KANBAN_HOST:?set KANBAN_HOST} S3_PUBLIC_ENDPOINT: ${R2_PUBLIC_ENDPOINT:?set R2_PUBLIC_ENDPOINT} networks: [backend] ports: - target: 8080 published: ${KANBAN_PORT:-8081} protocol: tcp mode: ingress read_only: true tmpfs: - /tmp:uid=101,gid=101,mode=0750 - /var/cache/nginx:uid=101,gid=101,mode=0750 - /var/run:uid=101,gid=101,mode=0750 - /etc/nginx/conf.d:uid=101,gid=101,mode=0750 cap_drop: [ALL] security_opt: [no-new-privileges:true] healthcheck: test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health'] interval: 10s timeout: 5s retries: 12 start_period: 15s deploy: <<: *rolling replicas: 1 resources: limits: {cpus: "0.5", memory: 256M} reservations: {cpus: "0.1", memory: 64M} configs: clamd_config: file: ../local/clamd.conf secrets: database_url: {external: true, name: "${DATABASE_URL_SECRET:?set DATABASE_URL_SECRET}"} database_admin_url: {external: true, name: "${DATABASE_ADMIN_URL_SECRET:?set DATABASE_ADMIN_URL_SECRET}"} db_admin_password: {external: true, name: "${DB_ADMIN_PASSWORD_SECRET:?set DB_ADMIN_PASSWORD_SECRET}"} app_db_password: {external: true, name: "${APP_DB_PASSWORD_SECRET:?set APP_DB_PASSWORD_SECRET}"} r2_access_key_id: {external: true, name: "${R2_ACCESS_KEY_ID_SECRET:?set R2_ACCESS_KEY_ID_SECRET}"} r2_secret_access_key: {external: true, name: "${R2_SECRET_ACCESS_KEY_SECRET:?set R2_SECRET_ACCESS_KEY_SECRET}"} operator_password: {external: true, name: "${OPERATOR_PASSWORD_SECRET:?set OPERATOR_PASSWORD_SECRET}"} payment_token: {external: true, name: "${PAYMENT_TOKEN_SECRET:?set PAYMENT_TOKEN_SECRET}"} payment_webhook_secret: {external: true, name: "${PAYMENT_WEBHOOK_SECRET:?set PAYMENT_WEBHOOK_SECRET}"} tiny_token: {external: true, name: "${TINY_TOKEN_SECRET:?set TINY_TOKEN_SECRET}"} whatsapp_token: {external: true, name: "${WHATSAPP_TOKEN_SECRET:?set WHATSAPP_TOKEN_SECRET}"} volumes: postgres-data: external: true name: ${POSTGRES_VOLUME:?set POSTGRES_VOLUME} networks: backend: driver: overlay internal: true egress: driver: overlay