diff --git a/apps/web/.env.example b/apps/web/.env.example index d11b6bb..a1fd812 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -68,6 +68,20 @@ DB_DISABLE_SSL=true POSTGRES_PORT=5432 GARAGE_API_PORT=3900 +# Coolify Compose container listener ports. APP_PORT is Coolify-only; +# POSTGRES_PORT and GARAGE_API_PORT are reused from above. The regular/dev +# compose files keep the database and Garage container ports at 5432/3900 and +# use those variables only for host-side mappings. +APP_PORT=3000 +GARAGE_RPC_PORT=3901 +GARAGE_WEB_PORT=3902 +GARAGE_ADMIN_PORT=3903 +# Garage requires a 64-character hexadecimal RPC secret. Generate with: +# openssl rand -hex 32 +# GARAGE_RPC_SECRET= +# GARAGE_ADMIN_TOKEN= +# GARAGE_METRICS_TOKEN= + # Optional: if Next dev picks another port, you do not need to change URLs for # sign-in — the auth client uses window.location.origin in the browser. @@ -138,7 +152,8 @@ NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.umami.is/script.js # • Coolify — see docs/COOLIFY.md. Summary: # - Best: one Compose resource with docker-compose.coolify.yml (app+db+garage). # - Application + separate Garage: ENOTFOUND garage → set S3_ENDPOINT to -# SERVICE_URL_GARAGE_3900 (public domain) OR http://garage-:3900 +# SERVICE_URL_GARAGE (public domain) OR +# http://garage-: # with Connect to Predefined Network on both resources. Never bare "garage". # - NEVER use localhost in production — inside the app container that is the app, not Garage. # @@ -150,8 +165,8 @@ S3_SECRET_KEY=7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34 S3_REGION=garage # S3_FORCE_PATH_STYLE=true # default on when S3_ENDPOINT is set; required for Garage/HTTPS proxy # -# docker-compose.yml sets S3_ENDPOINT=http://garage:3900 inside the app container -# automatically. S3_ACCESS_KEY / S3_SECRET_KEY must match the garage service env. +# docker-compose.yml uses http://garage:3900 internally. The Coolify compose +# derives the internal URL from GARAGE_API_PORT. Credentials must match Garage. # ============================================================================= # SSO — Authentik OIDC (optional) diff --git a/apps/web/docs/COOLIFY.md b/apps/web/docs/COOLIFY.md index 33a0dd0..7ce936b 100644 --- a/apps/web/docs/COOLIFY.md +++ b/apps/web/docs/COOLIFY.md @@ -6,16 +6,16 @@ beenvoice stores receipt files in S3-compatible storage when `S3_BUCKET`, `S3_AC Docker DNS resolves service names **only inside the same Docker network**. -| Setup | Does `http://garage:3900` work? | +| Setup | Does `http://garage:` work? | |-------|--------------------------------| | Single Compose stack (app + garage together) | Yes — Compose service name `garage` | | beenvoice **Application** + Garage **separate Compose** | **No** — each resource has its own network by default | | Application + Garage with shared destination network + correct hostname | Yes — hostname is usually **`garage-`**, not bare `garage` | -| Application + Garage via **public domain** (`SERVICE_URL_GARAGE_3900`) | Yes — no Docker DNS needed | +| Application + Garage via **public domain** (`SERVICE_URL_GARAGE`) | Yes — no Docker DNS needed | -Setting `S3_ENDPOINT=http://garage:3900` on a standalone beenvoice Application fails because the app container is not on the Garage stack's network. Node returns `ENOTFOUND garage`. +Setting `S3_ENDPOINT=http://garage:` on a standalone beenvoice Application fails because the app container is not on the Garage stack's network. Node returns `ENOTFOUND garage`. -Also avoid `http://localhost:3900` inside the app container — that points at the app itself, not Garage. +Also avoid `http://localhost:` inside the app container — that points at the app itself, not Garage. --- @@ -27,9 +27,9 @@ Use this if you are **not** migrating to a single Compose stack today. This is the most reliable fix when beenvoice is a Coolify **Application** (Dockerfile) and Garage is a separate Compose resource. -1. **Update the Garage stack** to the latest `docker-compose.coolify-garage.yml` from this repo (includes `SERVICE_FQDN_GARAGE_3900`) and **redeploy** the Garage resource. -2. In the **Garage Compose resource** → assign a domain for **port 3900** (e.g. `s3.yourdomain.com`). Coolify generates TLS via Traefik/Caddy. -3. Open the Garage resource **Environment** tab and copy **`SERVICE_URL_GARAGE_3900`** (e.g. `https://s3.yourdomain.com`). +1. **Update the Garage stack** to the latest `docker-compose.coolify-garage.yml` from this repo (includes `SERVICE_FQDN_GARAGE`) and **redeploy** the Garage resource. +2. In the **Garage Compose resource** → assign a domain to `GARAGE_API_PORT` (default **3900**, e.g. `s3.yourdomain.com`). Coolify generates TLS via Traefik/Caddy. +3. Open the Garage resource **Environment** tab and copy **`SERVICE_URL_GARAGE`** (e.g. `https://s3.yourdomain.com`). 4. On the **beenvoice Application** → Environment: ```env @@ -55,12 +55,12 @@ Use when you want S3 API traffic to stay on the Docker network. 5. Set on beenvoice Application: ```env -S3_ENDPOINT=http://garage-:3900 +S3_ENDPOINT=http://garage-: ``` -Example: resource UUID `k8w2o0g4s0g8` → `S3_ENDPOINT=http://garage-k8w2o0g4s0g8:3900`. +Example with the default port and resource UUID `k8w2o0g4s0g8`: `S3_ENDPOINT=http://garage-k8w2o0g4s0g8:3900`. -**Do not use bare `garage`** unless you verified it resolves from inside the beenvoice container (recent Coolify versions may also register the short service name when both sides use Connect to Predefined Network — if `wget http://garage:3900` fails, use the `garage-` form or Path A). +**Do not use bare `garage`** unless you verified it resolves from inside the beenvoice container (recent Coolify versions may also register the short service name when both sides use Connect to Predefined Network — if `wget http://garage:` fails, use the `garage-` form or Path A). 6. Match credentials and bucket: @@ -79,10 +79,12 @@ Deploy the root **[`docker-compose.coolify.yml`](../../../docker-compose.coolify 1. Coolify → **New Resource** → **Docker Compose** 2. Point at this repo; compose file: **`docker-compose.coolify.yml`** -3. Set env vars from [`.env.example`](../.env.example): `AUTH_SECRET`, `POSTGRES_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, etc. +3. Set env vars from [`.env.example`](../.env.example): `AUTH_SECRET`, `POSTGRES_PASSWORD`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, and a 64-character hexadecimal `GARAGE_RPC_SECRET` (`openssl rand -hex 32`). Garage has a valid development fallback, but production should override it. 4. Assign a domain to the **`app`** service (Coolify fills `SERVICE_URL_APP` / `BETTER_AUTH_URL` automatically). -5. **Do not** override `S3_ENDPOINT` — the compose file sets `S3_ENDPOINT=http://garage:3900` on the shared network. -6. Redeploy. +5. Optionally set `APP_PORT`, `POSTGRES_PORT`, `GARAGE_API_PORT`, `GARAGE_RPC_PORT`, `GARAGE_WEB_PORT`, or `GARAGE_ADMIN_PORT`. Defaults are `3000`, `5432`, and `3900`–`3903` respectively. +6. Only the app port is exposed to Coolify's proxy. PostgreSQL and every Garage listener remain reachable only through the stack's internal Docker network. +7. **Do not** override `S3_ENDPOINT` — the compose file derives it from `GARAGE_API_PORT` on the shared network. +8. Redeploy. Alternative: [`docker-compose.yml`](../../../docker-compose.yml) works the same way; `docker-compose.coolify.yml` adds Coolify magic vars (`SERVICE_FQDN_APP`) and omits host port bindings for db/Garage. @@ -95,7 +97,7 @@ Alternative: [`docker-compose.yml`](../../../docker-compose.yml) works the same | Garage / MinIO compose | Remove after data migrated (rclone) or re-point receipts (new bucket) | | Env vars | Move `AUTH_SECRET`, Resend, Authentik, etc. to the Compose resource env | -**Migrating from MinIO:** Garage uses port **3900** (not 9000) and Garage-format access keys (`GK…`). Update `S3_ENDPOINT`, `S3_REGION=garage`, and credentials. Receipt blobs in the old MinIO volume are not auto-migrated. +**Migrating from MinIO:** Garage defaults to port **3900** (not 9000) and uses Garage-format access keys (`GK…`). Update `S3_ENDPOINT`, `S3_REGION=garage`, and credentials. Receipt blobs in the old MinIO volume are not auto-migrated. --- @@ -114,9 +116,8 @@ Do **not** add `networks: coolify: external: true` unless you know the exact ext ## Checklist (Application + separate Garage) - [ ] Garage stack redeployed with current `docker-compose.coolify-garage.yml` -- [ ] **Path A:** domain on port 3900 + `S3_ENDPOINT` = `SERVICE_URL_GARAGE_3900` - **or Path B:** Connect to Predefined Network on **both** resources + `S3_ENDPOINT=http://garage-:3900` -- [ ] `S3_ENDPOINT` is **not** `http://garage:3900`, **not** `localhost` +- [ ] **Path A:** domain targets `GARAGE_API_PORT` + `S3_ENDPOINT` = `SERVICE_URL_GARAGE`; **or Path B:** Connect to Predefined Network on **both** resources + `S3_ENDPOINT=http://garage-:` +- [ ] `S3_ENDPOINT` is not a bare `http://garage:` across separate resources and is **not** `localhost` - [ ] `S3_ACCESS_KEY` / `S3_SECRET_KEY` match the Garage stack env - [ ] `S3_BUCKET` exists (Garage `--default-bucket` creates `beenvoice-receipts` on first start) - [ ] Redeployed beenvoice after env or network changes @@ -131,7 +132,7 @@ docker exec -it sh wget -qO- "https://s3.yourdomain.com" || curl -sf "https://s3.yourdomain.com" # Path B — internal host from S3_ENDPOINT -wget -qO- "http://garage-:3900" || curl -sf "http://garage-:3900" +wget -qO- "http://garage-:" || curl -sf "http://garage-:" ``` If this fails with "bad address" or timeout, fix networking / `S3_ENDPOINT` before debugging app code. On first S3 use, the app logs a hint if DNS fails or if `S3_ENDPOINT` still uses bare `garage` in production. diff --git a/docker-compose.coolify-garage.yml b/docker-compose.coolify-garage.yml index ecfd235..e2a7fd9 100644 --- a/docker-compose.coolify-garage.yml +++ b/docker-compose.coolify-garage.yml @@ -5,15 +5,15 @@ # ── Pair with a beenvoice Application (pick ONE) ─────────────────────────────── # # A) Public Garage URL (most reliable — no shared Docker network required) -# 1. Redeploy this stack (includes SERVICE_FQDN_GARAGE_3900 below). -# 2. Garage resource → assign a domain for port 3900 (e.g. s3.example.com). -# 3. Copy SERVICE_URL_GARAGE_3900 from this resource's Environment tab. +# 1. Redeploy this stack (includes SERVICE_FQDN_GARAGE below). +# 2. Garage resource → assign a domain to GARAGE_API_PORT (default 3900). +# 3. Copy SERVICE_URL_GARAGE from this resource's Environment tab. # 4. beenvoice Application → S3_ENDPOINT= → redeploy beenvoice. # # B) Internal Docker DNS (same Coolify destination network) # 1. Garage resource → Advanced → enable "Connect to Predefined Network" → redeploy. # 2. beenvoice Application → same destination → enable "Connect to Predefined Network". -# 3. beenvoice → S3_ENDPOINT=http://garage-:3900 +# 3. beenvoice → S3_ENDPOINT=http://garage-: # # Recommended long-term: deploy docker-compose.coolify.yml as one stack (app+db+garage). # See docs/COOLIFY.md. @@ -24,7 +24,7 @@ services: GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY} GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY} GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} - SERVICE_FQDN_GARAGE_3900: + SERVICE_FQDN_GARAGE: configs: - source: garage_config target: /etc/garage.toml @@ -33,7 +33,7 @@ services: - beenvoice_garage_data:/var/lib/garage/data command: ["/garage", "server", "--single-node", "--default-bucket"] expose: - - "3900" + - "${GARAGE_API_PORT:-3900}" healthcheck: test: ["CMD", "/garage", "status"] interval: 5s @@ -54,21 +54,21 @@ configs: db_engine = "sqlite" replication_factor = 1 - rpc_bind_addr = "[::]:3901" - rpc_public_addr = "garage:3901" - rpc_secret = "rpc_secret_change_me_in_production" + 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 = "[::]:3900" + api_bind_addr = "[::]:${GARAGE_API_PORT:-3900}" root_domain = ".s3.garage" [s3_web] - bind_addr = "[::]:3902" + bind_addr = "[::]:${GARAGE_WEB_PORT:-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" + 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}" diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml index afa9cab..c7d78e5 100644 --- a/docker-compose.coolify.yml +++ b/docker-compose.coolify.yml @@ -4,8 +4,10 @@ # # 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. Do NOT override S3_ENDPOINT — this stack sets http://garage:3900 on the shared network. -# 4. Rebuild after changing NEXT_PUBLIC_* (image build args use SERVICE_URL_APP). +# 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. @@ -15,17 +17,18 @@ services: build: context: . args: - NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:3000}} - BETTER_AUTH_URL: ${SERVICE_URL_APP:-${BETTER_AUTH_URL:-http://localhost:3000}} + 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:5432/${POSTGRES_DB:-postgres} + 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:3000}} - NEXT_PUBLIC_APP_URL: ${SERVICE_URL_APP:-${NEXT_PUBLIC_APP_URL:-http://localhost:3000}} + 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:-} @@ -37,13 +40,13 @@ services: AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-} AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-} AUTHENTIK_ORIGIN: ${AUTHENTIK_ORIGIN:-} - S3_ENDPOINT: http://garage:3900 + 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: - - "3000" + - "${APP_PORT:-3000}" depends_on: db: condition: service_healthy @@ -58,7 +61,7 @@ services: image: ${BEENVOICE_WORKER_IMAGE:-beenvoice-worker:coolify} environment: NODE_ENV: production - DATABASE_URL: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-postgres} + 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} @@ -76,11 +79,13 @@ services: 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 -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] + ["CMD-SHELL", 'pg_isready -h 127.0.0.1 -p "$${POSTGRES_PORT}" -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"'] interval: 5s timeout: 5s retries: 10 @@ -92,7 +97,6 @@ services: GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY} GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY} GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-beenvoice-receipts} - SERVICE_FQDN_GARAGE_3900: configs: - source: garage_config target: /etc/garage.toml @@ -100,8 +104,6 @@ services: - beenvoice_garage_meta:/var/lib/garage/meta - beenvoice_garage_data:/var/lib/garage/data command: ["/garage", "server", "--single-node", "--default-bucket"] - expose: - - "3900" healthcheck: test: ["CMD", "/garage", "status"] interval: 5s @@ -123,21 +125,21 @@ configs: db_engine = "sqlite" replication_factor = 1 - rpc_bind_addr = "[::]:3901" - rpc_public_addr = "garage:3901" - rpc_secret = "rpc_secret_change_me_in_production" + 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 = "[::]:3900" + api_bind_addr = "[::]:${GARAGE_API_PORT:-3900}" root_domain = ".s3.garage" [s3_web] - bind_addr = "[::]:3902" + bind_addr = "[::]:${GARAGE_WEB_PORT:-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" + 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}"