# beenvoice on Coolify — single Docker Compose resource (recommended). # # Deploy: Coolify → New Resource → Docker Compose → compose file: docker-compose.coolify.yml # # 1. Assign a domain to the `app` service in Coolify (SERVICE_FQDN_APP wires Traefik). # 2. Set AUTH_SECRET, POSTGRES_PASSWORD, S3_ACCESS_KEY, S3_SECRET_KEY in the resource env (see .env.example). # 3. Ports are configurable with APP_PORT, POSTGRES_PORT, and GARAGE_*_PORT. # Only APP_PORT is exposed to Coolify's proxy; db and Garage stay internal. # 4. Do NOT override S3_ENDPOINT — it follows GARAGE_API_PORT on the shared network. # 5. Rebuild after changing NEXT_PUBLIC_* (image build args use SERVICE_URL_APP). # # Migrating from Application + separate Postgres + Garage compose: # - Export Postgres data, point DATABASE_URL at this stack's `db` service, redeploy once here. # - Or keep external Postgres and remove the `db` service + volume from this file. services: app: build: context: . args: NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:${APP_PORT:-3000}}} BETTER_AUTH_URL: ${SERVICE_URL_APP:-${BETTER_AUTH_URL:-http://localhost:${APP_PORT:-3000}}} image: ${BEENVOICE_IMAGE:-beenvoice:coolify} environment: SERVICE_FQDN_APP: NODE_ENV: production PORT: ${APP_PORT:-3000} AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in Coolify env} DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} DB_DISABLE_SSL: "true" BETTER_AUTH_URL: ${SERVICE_URL_APP:-${BETTER_AUTH_URL:-http://localhost:${APP_PORT:-3000}}} NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:${APP_PORT:-3000}}} RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_DOMAIN: ${RESEND_DOMAIN:-} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-} NEXT_PUBLIC_UMAMI_SCRIPT_URL: ${NEXT_PUBLIC_UMAMI_SCRIPT_URL:-https://analytics.umami.is/script.js} NEXT_PUBLIC_AUTHENTIK_ENABLED: ${NEXT_PUBLIC_AUTHENTIK_ENABLED:-false} DISABLE_SIGNUPS: ${DISABLE_SIGNUPS:-true} CRON_SECRET: ${CRON_SECRET:-} AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-} AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-} AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-} AUTHENTIK_ORIGIN: ${AUTHENTIK_ORIGIN:-} S3_ENDPOINT: http://garage:${GARAGE_API_PORT:-3900} S3_BUCKET: ${S3_BUCKET:-beenvoice-receipts} S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_REGION: ${S3_REGION:-garage} expose: - "${APP_PORT:-3000}" depends_on: db: condition: service_healthy garage: condition: service_healthy restart: unless-stopped worker: build: context: . target: worker image: ${BEENVOICE_WORKER_IMAGE:-beenvoice-worker:coolify} environment: NODE_ENV: production DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} DB_DISABLE_SSL: "true" AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in Coolify env} WORKER_POLL_MS: ${WORKER_POLL_MS:-2000} WORKER_SCHEDULE_MS: ${WORKER_SCHEDULE_MS:-60000} RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_DOMAIN: ${RESEND_DOMAIN:-} CRON_SECRET: ${CRON_SECRET:-} APP_INTERNAL_URL: http://app:${APP_PORT:-3000} depends_on: app: condition: service_started db: condition: service_healthy restart: unless-stopped db: image: postgres:17-alpine environment: POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} POSTGRES_DB: ${POSTGRES_DB:-postgres} POSTGRES_PORT: ${POSTGRES_PORT:-5432} command: ["postgres", "-p", "${POSTGRES_PORT:-5432}"] volumes: - beenvoice_pg_data:/var/lib/postgresql/data healthcheck: test: [ "CMD-SHELL", 'pg_isready -h 127.0.0.1 -p "$${POSTGRES_PORT}" -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"', ] interval: 5s timeout: 5s retries: 10 restart: unless-stopped garage: image: dxflrs/garage:v2.3.0 environment: GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY} GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY} GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} configs: - source: garage_config target: /etc/garage.toml volumes: - beenvoice_garage_meta:/var/lib/garage/meta - beenvoice_garage_data:/var/lib/garage/data command: ["/garage", "server", "--single-node", "--default-bucket"] healthcheck: test: ["CMD", "/garage", "status"] interval: 5s timeout: 5s retries: 15 start_period: 20s restart: unless-stopped volumes: beenvoice_pg_data: beenvoice_garage_meta: beenvoice_garage_data: configs: garage_config: content: | metadata_dir = "/var/lib/garage/meta" data_dir = "/var/lib/garage/data" db_engine = "sqlite" replication_factor = 1 rpc_bind_addr = "[::]:${GARAGE_RPC_PORT:-3901}" rpc_public_addr = "garage:${GARAGE_RPC_PORT:-3901}" rpc_secret = "${GARAGE_RPC_SECRET:-000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f}" [s3_api] s3_region = "garage" api_bind_addr = "[::]:${GARAGE_API_PORT:-3900}" root_domain = ".s3.garage" [s3_web] bind_addr = "[::]:${GARAGE_WEB_PORT:-3902}" root_domain = ".web.garage" index = "index.html" [admin] api_bind_addr = "[::]:${GARAGE_ADMIN_PORT:-3903}" admin_token = "${GARAGE_ADMIN_TOKEN:-beenvoice_garage_admin_token_change_me_in_production}" metrics_token = "${GARAGE_METRICS_TOKEN:-beenvoice_garage_metrics_token_change_me_in_production}"