Improve expenses receipts UX and Coolify MinIO deployment.

Extract receipt UI components, add view/edit/create dialog modes with list receipt previews, add docker-compose.coolify.yml and clearer COOLIFY/S3 path-style guidance for Application + MinIO setups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 00:47:59 -04:00
co-authored by Cursor
parent 5e6e51337a
commit c267b3e1c8
15 changed files with 1236 additions and 361 deletions
+82 -98
View File
@@ -8,144 +8,128 @@ Docker DNS resolves service names **only inside the same Docker network**.
| Setup | Does `http://minio:9000` work? |
|-------|-------------------------------|
| Single `docker-compose.yml` stack (app + minio together) | Yes — Compose service name `minio` |
| Beevoice **Application** + MinIO **separate Compose** resource | **No** — each Coolify resource gets its own network by default |
| Both resources share a Coolify **destination** network + correct hostname | Yes — but hostname is often **not** bare `minio` |
| Single Compose stack (app + minio together) | Yes — Compose service name `minio` |
| Beevoice **Application** + MinIO **separate Compose** | **No** — each resource has its own network by default |
| Application + MinIO with shared destination network + correct hostname | Yes — hostname is usually **`minio-<resource-uuid>`**, not bare `minio` |
| Application + MinIO via **public domain** (`SERVICE_URL_MINIO_9000`) | Yes — no Docker DNS needed |
Setting `S3_ENDPOINT=http://minio:9000` on a standalone Beevoice Application fails because the app container is not on the MinIO stack's internal network. Node's DNS lookup returns `ENOTFOUND minio`.
Setting `S3_ENDPOINT=http://minio:9000` on a standalone Beevoice Application fails because the app container is not on the MinIO stack's network. Node returns `ENOTFOUND minio`.
Also avoid `http://localhost:9000` inside the app container — that points at the app itself, not MinIO.
---
## Recommended: Option C — one Compose stack (simplest)
## Quick fix — keep Beevoice as Application + separate MinIO compose
Deploy the repo's full [`docker-compose.yml`](../docker-compose.yml) as **one** Coolify **Docker Compose** resource (app + Postgres + MinIO + minio-init).
Use this if you are **not** migrating to a single Compose stack today.
1. Coolify → **New Resource****Docker Compose**
2. Point at this repo; compose file: `docker-compose.yml`
3. Set env vars from [`.env.example`](../.env.example) (`AUTH_SECRET`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_APP_URL`, etc.)
4. **Do not** override `S3_ENDPOINT` — the compose file sets `S3_ENDPOINT=http://minio:9000` for the app service automatically
5. Redeploy
### Path A — public MinIO URL (recommended, works without shared Docker network)
All services share one Compose network; `minio` resolves correctly.
This is the most reliable fix when Beevoice is a Coolify **Application** (Dockerfile) and MinIO is a separate Compose resource.
---
## Option A — separate resources, shared Coolify network
Use when Beevoice stays a standalone **Application** (Dockerfile) and MinIO is a separate Compose resource.
### 1. Same Coolify project and destination
Put both resources in the **same Coolify project** and deploy them to the **same destination** (same Docker network / server).
### 2. Connect the Beevoice app to that network
On the **Beevoice Application** resource:
1. Open **Advanced** (or network settings)
2. Enable **Connect to Predefined Network**
3. Select the **same destination/network** as the MinIO stack
4. **Redeploy** the app (required after toggling network)
The MinIO stack does **not** need this option — only the service that **initiates** connections (Beevoice) needs it.
### 3. Set `S3_ENDPOINT` to the real internal hostname
Bare `minio` usually still fails across separate Coolify resources. Use the hostname Coolify assigns on the shared network:
1. Open the **MinIO Compose** resource in Coolify
2. Find the **internal URL** / connection info (eye icon next to internal connection string)
3. Copy the **hostname** from that URL (not `localhost`, not bare `minio` unless you verified it resolves)
Typical patterns:
| What you see | Use as `S3_ENDPOINT` |
|--------------|----------------------|
| Internal URL host `minio-abc123def456` | `http://minio-abc123def456:9000` |
| Container name `x8k2j4...` (random id) | `http://x8k2j4...:9000` |
| Same compose stack only | `http://minio:9000` |
On the Coolify server you can confirm:
```bash
# List MinIO containers
docker ps --filter name=minio
# See DNS aliases on the shared network (replace CONTAINER and NETWORK)
docker inspect CONTAINER --format '{{json .NetworkSettings.Networks}}' | jq
```
Set on the **Beevoice Application** env:
1. **Update the MinIO stack** to the latest `docker-compose.coolify-minio.yml` from this repo (includes `SERVICE_FQDN_MINIO_9000`) and **redeploy** the MinIO resource.
2. In the **MinIO Compose resource** → assign a domain for **port 9000** (e.g. `s3.yourdomain.com`). Coolify generates TLS via Traefik/Caddy.
3. Open the MinIO resource **Environment** tab and copy **`SERVICE_URL_MINIO_9000`** (e.g. `https://s3.yourdomain.com`).
4. On the **Beevoice Application** → Environment:
```env
S3_ENDPOINT=http://<hostname-from-step-3>:9000
S3_ENDPOINT=https://s3.yourdomain.com
S3_BUCKET=beenvoice-receipts
S3_ACCESS_KEY=<same as MINIO_ROOT_USER>
S3_SECRET_KEY=<same as MINIO_ROOT_PASSWORD>
S3_REGION=us-east-1
```
Redeploy Beevoice after changing env.
5. **Redeploy Beevoice** (restart is not enough after env changes on some Coolify versions — trigger a full redeploy).
### 4. Enable on MinIO stack too (only if A still fails)
`S3_FORCE_PATH_STYLE` defaults to on when `S3_ENDPOINT` is set (required for MinIO behind a reverse proxy). Only set `S3_FORCE_PATH_STYLE=false` if you use AWS S3 with virtual-hosted-style buckets.
If the app still cannot resolve the hostname, enable **Connect to Predefined Network** on the **MinIO Compose** resource as well (same destination), redeploy MinIO, then re-check the internal URL — Coolify may expose a `minio-<resource-id>` alias on the shared network.
### Path B — internal Docker DNS (same destination, no public MinIO domain)
---
Use when you want MinIO API traffic to stay on the Docker network.
## Option B — internal URL from Coolify UI (quick fix)
Same as Option A step 3, without re-architecting:
1. MinIO resource → copy **internal** hostname (from internal URL field)
2. Beevoice Application → `S3_ENDPOINT=http://<that-hostname>:9000`
3. Enable **Connect to Predefined Network** on Beevoice if not already
4. Redeploy Beevoice
If DNS still fails, the app is not on the network where that hostname is registered — go back to Option A or use Option C.
---
## Option D — public / external MinIO URL (fallback)
If internal Docker DNS cannot be made to work:
1. Put Beevoice Application and MinIO Compose in the **same Coolify project** and **same destination** (server/network).
2. **MinIO Compose resource****Advanced** → enable **Connect to Predefined Network****redeploy MinIO**.
3. **Beevoice Application****Advanced** → enable **Connect to Predefined Network** (same destination) → **redeploy Beevoice**.
4. Find the MinIO resource **UUID** (in the Coolify URL, e.g. `.../service/abc123def456`, or env `COOLIFY_RESOURCE_UUID` on the MinIO container).
5. Set on Beevoice Application:
```env
S3_ENDPOINT=https://minio.yourdomain.com
S3_ENDPOINT=http://minio-<MINIO_RESOURCE_UUID>:9000
```
Expose MinIO API (port 9000) via Coolify proxy or a public domain. Less ideal (traffic leaves the Docker network, TLS/path-style config may need tuning) but avoids internal DNS entirely.
Example: resource UUID `k8w2o0g4s0g8``S3_ENDPOINT=http://minio-k8w2o0g4s0g8:9000`.
**Do not use bare `minio`** unless you verified it resolves from inside the Beevoice container (recent Coolify versions may also register the short service name when both sides use Connect to Predefined Network — if `wget http://minio:9000/minio/health/live` fails, use the `minio-<uuid>` form or Path A).
6. Match credentials and bucket:
```env
S3_BUCKET=beenvoice-receipts
S3_ACCESS_KEY=<MINIO_ROOT_USER>
S3_SECRET_KEY=<MINIO_ROOT_PASSWORD>
S3_REGION=us-east-1
```
---
## Separate MinIO-only Compose file
## Recommended long-term — one Compose stack
[`docker-compose.coolify-minio.yml`](../docker-compose.coolify-minio.yml) deploys only MinIO + bucket init for a dedicated Coolify Compose resource. Pair it with a Beevoice Application using Option A or B.
Deploy **[`docker-compose.coolify.yml`](../docker-compose.coolify.yml)** as **one** Coolify **Docker Compose** resource (app + Postgres + MinIO + minio-init). This is the lowest-friction production layout on Coolify.
Do **not** add `networks: coolify: external: true` unless you know the exact external network name on your Coolify server. Coolify v4 uses **destinations**; network names are often UUID-based. Prefer the UI **Connect to Predefined Network** toggle over hard-coding `coolify` in compose.
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`, `MINIO_ROOT_*`, etc.
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://minio:9000` on the shared network.
6. 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/MinIO.
### Migrating from Application + external Postgres + MinIO
| Current | Action |
|---------|--------|
| Beevoice Application | Remove after Compose stack is live |
| Separate Postgres | Dump/restore into stack `db`, or keep external DB and delete the `db` service from the compose file |
| MinIO compose | Remove after data migrated or re-point receipts (new bucket) |
| Env vars | Move `AUTH_SECRET`, Resend, Authentik, etc. to the Compose resource env |
---
## Checklist
## Compose file reference
- [ ] Beevoice and MinIO in the same Coolify **project**
- [ ] Same **destination** / server
- [ ] Beevoice Application: **Connect to Predefined Network** enabled (when MinIO is a separate resource)
- [ ] `S3_ENDPOINT` uses internal hostname from Coolify UI — not `localhost`, not unverified `minio`
- [ ] `S3_ACCESS_KEY` / `S3_SECRET_KEY` match MinIO `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`
- [ ] `S3_BUCKET` exists ( `minio-init` in compose creates `beenvoice-receipts` by default)
- [ ] Redeployed after env or network changes
| File | Purpose |
|------|---------|
| [`docker-compose.coolify.yml`](../docker-compose.coolify.yml) | **Recommended** — full stack for one Coolify Compose resource |
| [`docker-compose.yml`](../docker-compose.yml) | Full stack (local/VPS); also valid on Coolify |
| [`docker-compose.coolify-minio.yml`](../docker-compose.coolify-minio.yml) | MinIO + bucket init only; pair with Beevoice Application (Path A or B above) |
Do **not** add `networks: coolify: external: true` unless you know the exact external network name on your server. Coolify v4 uses **destinations**; network names are often UUID-based. Prefer the UI **Connect to Predefined Network** toggle over hard-coding `coolify` in compose.
---
## Checklist (Application + separate MinIO)
- [ ] MinIO stack redeployed with current `docker-compose.coolify-minio.yml`
- [ ] **Path A:** domain on port 9000 + `S3_ENDPOINT` = `SERVICE_URL_MINIO_9000`
**or Path B:** Connect to Predefined Network on **both** resources + `S3_ENDPOINT=http://minio-<uuid>:9000`
- [ ] `S3_ENDPOINT` is **not** `http://minio:9000`, **not** `localhost`
- [ ] `S3_ACCESS_KEY` / `S3_SECRET_KEY` match `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD`
- [ ] `S3_BUCKET` exists (`minio-init` creates `beenvoice-receipts` by default)
- [ ] Redeployed Beevoice after env or network changes
## Verify from the Beevoice container
```bash
# Shell into Beevoice app container on Coolify server
# Shell into Beevoice app container on the Coolify server
docker exec -it <beenvoice-container> sh
# Replace HOST with your S3_ENDPOINT hostname (no scheme/port)
wget -qO- "http://HOST:9000/minio/health/live" || curl -sf "http://HOST:9000/minio/health/live"
# Path A — public URL (include scheme; path is /minio/health/live on API port)
wget -qO- "https://s3.yourdomain.com/minio/health/live" || curl -sf "https://s3.yourdomain.com/minio/health/live"
# Path B — internal host from S3_ENDPOINT (no scheme/port in HOST)
wget -qO- "http://minio-<uuid>:9000/minio/health/live" || curl -sf "http://minio-<uuid>:9000/minio/health/live"
```
If this fails with "bad address" or timeout, fix networking before debugging app code.
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 `minio` in production.