# Production stack (app + Postgres + Garage). Local dev Postgres/Garage: docker-compose.dev.yml # # Coolify: deploy docker-compose.coolify.yml as ONE Docker Compose resource (preferred), # or this file. S3_ENDPOINT=http://garage:3900 works only inside a single stack. # Application + separate Garage → docs/COOLIFY.md. # # After git pull, rebuild before starting — a plain `docker compose up -d` reuses # the existing local image and will NOT include new code. Use: # ./scripts/docker-deploy.sh # docker compose up -d --build services: app: build: context: . args: NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000} BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} # Fixed default tag (beenvoice:local) is reused until you --build. docker-deploy.sh # sets BEENVOICE_IMAGE=beenvoice: so each deploy gets a fresh tag. image: ${BEENVOICE_IMAGE:-beenvoice:local} environment: NODE_ENV: production AUTH_SECRET: ${AUTH_SECRET:?Set AUTH_SECRET in .env} DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres} DB_DISABLE_SSL: "true" BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost: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:3900 S3_BUCKET: ${S3_BUCKET:-beenvoice-receipts} S3_ACCESS_KEY: ${S3_ACCESS_KEY:-GK3515373e4c851ebaad366558} S3_SECRET_KEY: ${S3_SECRET_KEY:-7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34} S3_REGION: ${S3_REGION:-garage} ports: - "${WEB_PORT:-${PORT:-3000}}:3000" depends_on: db: condition: service_healthy garage: 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} volumes: - beenvoice_pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] interval: 5s timeout: 5s retries: 10 restart: unless-stopped # S3-compatible receipt storage (~50–100 MB RAM vs MinIO). API :3900. garage: image: dxflrs/garage:v2.3.0 environment: GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY:-GK3515373e4c851ebaad366558} GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY:-7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34} 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"] ports: - "${GARAGE_API_PORT:-3900}:3900" 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 = "[::]:3901" rpc_public_addr = "garage:3901" rpc_secret = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" [s3_api] s3_region = "garage" api_bind_addr = "[::]:3900" root_domain = ".s3.garage" [s3_web] bind_addr = "[::]:3902" root_domain = ".web.garage" index = "index.html" [admin] api_bind_addr = "[::]:3903" admin_token = "beenvoice_garage_admin_token_change_me_in_production" metrics_token = "beenvoice_garage_metrics_token_change_me_in_production"