name: who_need_help x-app-environment: &app-environment APP_ROLE: web DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL in .env} DATABASE_SOCKET_DIR: ${DATABASE_SOCKET_DIR:-} SECRET_KEY_BASE: ${SECRET_KEY_BASE:?Set SECRET_KEY_BASE in .env} HANDOVER_SECRET: ${HANDOVER_SECRET:?Set HANDOVER_SECRET in .env} RELEASE_COOKIE: ${RELEASE_COOKIE:?Set RELEASE_COOKIE in .env} METRICS_TOKEN: ${METRICS_TOKEN:?Set an independent METRICS_TOKEN in .env} # Keep BEAM from deriving a massive port table from a host/container nofile # limit. OTP's normal default is 65,536; deployments can override it after # measuring their concurrent file/socket requirements. ERL_ZFLAGS: "+Q ${ERLANG_PORT_LIMIT:-65536}" CLUSTER_INTERFACE: ${CLUSTER_INTERFACE:-eth0} DNS_CLUSTER_QUERY: ${DNS_CLUSTER_QUERY:-cluster-web} PHX_HOST: ${PHX_HOST:?Set PHX_HOST in .env} PHX_SCHEME: ${PHX_SCHEME:?Set PHX_SCHEME in .env} PHX_URL_PORT: ${PHX_URL_PORT:?Set PHX_URL_PORT in .env} PORT: "4000" EMAIL_DELIVERY_PROVIDER: ${EMAIL_DELIVERY_PROVIDER:-smtp} SMTP_RELAY: ${SMTP_RELAY:-mailpit} SMTP_PORT: ${SMTP_PORT:-1025} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_AUTH: ${SMTP_AUTH:-never} SMTP_TLS: ${SMTP_TLS:-never} SMTP_SSL: ${SMTP_SSL:-false} UNISENDER_GO_API_KEY: ${UNISENDER_GO_API_KEY:-} UNISENDER_GO_BASE_URL: ${UNISENDER_GO_BASE_URL:-https://goapi.unisender.ru/ru/transactional/api/v1} EMAIL_HTTP_CONNECT_TIMEOUT_MS: ${EMAIL_HTTP_CONNECT_TIMEOUT_MS:-} EMAIL_HTTP_RECEIVE_TIMEOUT_MS: ${EMAIL_HTTP_RECEIVE_TIMEOUT_MS:-} EMAIL_FROM_NAME: ${EMAIL_FROM_NAME:?Set EMAIL_FROM_NAME in .env} EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:?Set EMAIL_FROM_ADDRESS in .env} SUPPORT_INBOX_ADDRESS: ${SUPPORT_INBOX_ADDRESS:-} CODEX_SESSION_ID: ${CODEX_SESSION_ID:-not-configured} RATE_LIMIT_POLICIES_JSON: ${RATE_LIMIT_POLICIES_JSON:-{}} MAP_TILE_URL: ${MAP_TILE_URL:-https://tile.openstreetmap.org/{z}/{x}/{y}.png} GITHUB_OAUTH_CLIENT_ID: ${GITHUB_OAUTH_CLIENT_ID:-} GITHUB_OAUTH_CLIENT_SECRET: ${GITHUB_OAUTH_CLIENT_SECRET:-} GITHUB_OAUTH_BASE_URL: ${GITHUB_OAUTH_BASE_URL:-} GITHUB_OAUTH_AUTHORIZE_URL: ${GITHUB_OAUTH_AUTHORIZE_URL:-} GITHUB_OAUTH_TOKEN_URL: ${GITHUB_OAUTH_TOKEN_URL:-} GITHUB_OAUTH_USER_URL: ${GITHUB_OAUTH_USER_URL:-} GITHUB_OAUTH_HTTP_CONNECT_TIMEOUT_MS: ${GITHUB_OAUTH_HTTP_CONNECT_TIMEOUT_MS:-} GITHUB_OAUTH_HTTP_RECEIVE_TIMEOUT_MS: ${GITHUB_OAUTH_HTTP_RECEIVE_TIMEOUT_MS:-} GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-} GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-} GOOGLE_OAUTH_BASE_URL: ${GOOGLE_OAUTH_BASE_URL:-} GOOGLE_OAUTH_HTTP_CONNECT_TIMEOUT_MS: ${GOOGLE_OAUTH_HTTP_CONNECT_TIMEOUT_MS:-} GOOGLE_OAUTH_HTTP_RECEIVE_TIMEOUT_MS: ${GOOGLE_OAUTH_HTTP_RECEIVE_TIMEOUT_MS:-} PUSH_HTTP_ENDPOINT: ${PUSH_HTTP_ENDPOINT:-} PUSH_HTTP_BEARER_TOKEN: ${PUSH_HTTP_BEARER_TOKEN:-} PUSH_HTTP_MAX_ATTEMPTS: ${PUSH_HTTP_MAX_ATTEMPTS:-} PUSH_HTTP_RECEIVE_TIMEOUT_MS: ${PUSH_HTTP_RECEIVE_TIMEOUT_MS:-} PUSH_HTTP_CONNECT_TIMEOUT_MS: ${PUSH_HTTP_CONNECT_TIMEOUT_MS:-} PUSH_HTTP_RETRY_DELAY_MS: ${PUSH_HTTP_RETRY_DELAY_MS:-} OBAN_MAINTENANCE_CONCURRENCY: ${OBAN_MAINTENANCE_CONCURRENCY:-2} OBAN_PUSH_CONCURRENCY: ${OBAN_PUSH_CONCURRENCY:-1} services: docker-api-proxy: image: who-need-help:socket-proxy-local build: context: . dockerfile: Dockerfile.socket-proxy environment: CONTAINERS: "1" EVENTS: "1" INFO: "1" NETWORKS: "1" PING: "1" POST: "0" VERSION: "1" LOG_LEVEL: warning volumes: - /var/run/docker.sock:/var/run/docker.sock:ro group_add: - ${DOCKER_SOCKET_GID:?Set DOCKER_SOCKET_GID to the numeric group of /var/run/docker.sock} networks: [docker-api] read_only: true tmpfs: - /run:uid=99,gid=99,mode=0755 - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped proxy: image: traefik:v3.7.8@sha256:4299bbed850421258fc5448c2e0e6ad350981d4d335a68de11b92448aedbefe5 command: - --api.dashboard=false - --providers.docker=true - --providers.docker.endpoint=tcp://docker-api-proxy:2375 - --providers.docker.exposedbydefault=false - --providers.docker.constraints=Label(`com.docker.compose.project`,`${TRAEFIK_PROJECT_CONSTRAINT:-who_need_help}`) - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --entrypoints.web.forwardedheaders.trustedips=${TRAEFIK_TRUSTED_IPS:-127.0.0.1/32} ports: - "${HTTP_BIND_ADDRESS:-0.0.0.0}:${HTTP_PORT:-4010}:80" depends_on: - docker-api-proxy networks: [docker-api, edge, ingress] read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped db: image: who-need-help:postgis-local build: context: . dockerfile: Dockerfile.postgis environment: # Profiles are resolved after interpolation. These disabled-definition # fallbacks let external mode render without inventing live credentials; # scripts/compose.sh requires all three values before container mode. POSTGRES_DB: ${POSTGRES_DB:-disabled_external_database} POSTGRES_USER: ${POSTGRES_USER:-disabled_external_database} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-disabled-external-database} healthcheck: # The image starts a temporary Unix-socket-only PostgreSQL while it loads # PostGIS into a fresh database, then stops it and starts the final server. # Checking TCP prevents dependants from starting during that bootstrap window. test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 3s timeout: 3s retries: 20 volumes: - postgres_data:/var/lib/postgresql networks: internal: interface_name: eth0 security_opt: - no-new-privileges:true restart: unless-stopped mailpit: image: axllent/mailpit:v1.30.4@sha256:5a49a77c5bdbe7c5474450b4f46348d09949df3695257729c93a30369382d4f6 user: "65534:65534" ports: - "${MAILPIT_BIND_ADDRESS:-127.0.0.1}:${MAILPIT_PORT:-8027}:8025" networks: [edge, internal] read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped migrate: image: who-need-help:local build: context: . environment: <<: *app-environment APP_ROLE: migrate POOL_SIZE: ${MIGRATE_POOL_SIZE:-${POOL_SIZE:?Set MIGRATE_POOL_SIZE in .env}} command: ["/app/bin/migrate"] extra_hosts: - "host.docker.internal:host-gateway" depends_on: db: condition: service_healthy networks: internal: interface_name: eth0 egress: interface_name: eth1 gw_priority: 1 read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: "no" app: profiles: [compact] image: who-need-help:local environment: <<: *app-environment APP_ROLE: combined PHX_SERVER: "true" DNS_CLUSTER_QUERY: ignore POOL_SIZE: ${COMBINED_POOL_SIZE:-${WEB_POOL_SIZE:-4}} depends_on: migrate: condition: service_completed_successfully extra_hosts: - "host.docker.internal:host-gateway" ports: - "${HTTP_BIND_ADDRESS:-127.0.0.1}:${HTTP_PORT:-4010}:4000" healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:4000/healthz/ready"] interval: 10s timeout: 3s retries: 10 networks: internal: interface_name: eth0 egress: interface_name: eth1 gw_priority: 1 read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped web: image: who-need-help:local environment: <<: *app-environment APP_ROLE: web PHX_SERVER: "true" POOL_SIZE: ${WEB_POOL_SIZE:-${POOL_SIZE:?Set WEB_POOL_SIZE in .env}} depends_on: migrate: condition: service_completed_successfully extra_hosts: - "host.docker.internal:host-gateway" labels: - traefik.enable=true - traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-who_need_help_ingress} - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.rule=${TRAEFIK_ROUTER_RULE:-PathPrefix(`/`)} - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.entrypoints=web - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.service=${TRAEFIK_APP_NAME:-who-need-help} - traefik.http.routers.${TRAEFIK_APP_NAME:-who-need-help}.middlewares=${TRAEFIK_APP_NAME:-who-need-help}-retry - traefik.http.middlewares.${TRAEFIK_APP_NAME:-who-need-help}-retry.retry.attempts=${TRAEFIK_RETRY_ATTEMPTS:-3} - traefik.http.services.${TRAEFIK_APP_NAME:-who-need-help}.loadbalancer.server.port=4000 healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:4000/healthz/ready"] interval: 10s timeout: 3s retries: 10 deploy: replicas: ${WEB_REPLICAS:-2} networks: internal: interface_name: eth0 aliases: [cluster-web] ingress: interface_name: eth1 egress: interface_name: eth2 gw_priority: 1 read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped worker: image: who-need-help:local environment: <<: *app-environment APP_ROLE: worker POOL_SIZE: ${WORKER_POOL_SIZE:-${POOL_SIZE:?Set WORKER_POOL_SIZE in .env}} command: ["/app/bin/who_need_help", "start"] depends_on: migrate: condition: service_completed_successfully extra_hosts: - "host.docker.internal:host-gateway" healthcheck: test: ["CMD", "curl", "--fail", "--silent", "http://localhost:4000/healthz/ready"] interval: 10s timeout: 3s retries: 10 deploy: replicas: ${WORKER_REPLICAS:-2} networks: internal: interface_name: eth0 egress: interface_name: eth1 gw_priority: 1 read_only: true tmpfs: - /tmp cap_drop: [ALL] security_opt: - no-new-privileges:true restart: unless-stopped networks: docker-api: internal: true edge: ingress: internal: true internal: internal: true egress: volumes: postgres_data: