CouchDB 3.5 derives the Set-Cookie Secure flag from the raw mochiweb socket scheme and ignores X-Forwarded-Proto/Ssl for cookie attribution. Behind a TLS-terminating proxy the cookie was emitted without Secure, so browsers dropped the SameSite=None AuthSession cookie on cross-origin requests — POST /_session returned 200 but the next GET /_session showed userCtx.name: null and PouchDB stayed unauthenticated. Add an nginx sidecar that adds Secure via proxy_cookie_flags, route the public domain through it, and make couchdb internal. Also persist same_site=none in couchdb-init so the setting survives container recreation (local.d/ is not in the data volume).
116 lines
5.5 KiB
Markdown
116 lines
5.5 KiB
Markdown
# coolcouch
|
|
|
|
CouchDB 3 on Coolify, fronted by an nginx sidecar. The `couchdb-init` sidecar waits for the server to come up, creates the three system databases, and applies CORS + cookie config via the config API — no file mounts on CouchDB itself.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
client ──https──> Traefik ──http──> nginx ──http──> couchdb (internal)
|
|
```
|
|
|
|
nginx exists for one reason: to add `Secure` to the `AuthSession` cookie that CouchDB emits. CouchDB only marks cookies `Secure` when its embedded mochiweb sees a true HTTPS socket; behind a TLS-terminating proxy it never does, and it ignores `X-Forwarded-Proto`/`X-Forwarded-Ssl` for cookie attribution. Without `Secure`, browsers reject `SameSite=None` cookies, and cross-origin PouchDB logins silently fail (login returns 200, the next `_session` shows `userCtx.name: null`). The fix is `proxy_cookie_flags AuthSession secure;` in `nginx/default.conf`.
|
|
|
|
## Deploy
|
|
|
|
1. In Coolify: **New Resource → Docker Compose** and point it at this repository.
|
|
2. In the resource's **Environment Variables** tab, set:
|
|
- `COUCHDB_PASSWORD` — required
|
|
- `COUCHDB_USER` — optional, defaults to `admin`
|
|
- `INVITE_CODE` — required for the signup service
|
|
3. In the resource's **Domains** tab, assign your CouchDB domain to the **`nginx`** service on port `5984` (not `couchdb` directly — that's now internal). Coolify fills `SERVICE_FQDN_NGINX_5984` from this. Assign the signup domain to the `signup` service on port `8080`.
|
|
4. **Deploy.** `couchdb-init` runs once, exits, and won't restart.
|
|
|
|
## Verify
|
|
|
|
Replace `$DOMAIN` and `$PASS`:
|
|
|
|
```bash
|
|
curl -fsS https://$DOMAIN/_up
|
|
curl -fsS -u admin:$PASS https://$DOMAIN/
|
|
curl -fsS -u admin:$PASS https://$DOMAIN/_all_dbs
|
|
curl -fsS -I -X OPTIONS https://$DOMAIN/ \
|
|
-H "Origin: https://example.com" \
|
|
-H "Access-Control-Request-Method: GET"
|
|
```
|
|
|
|
Expected: `_up` returns `{"status":"ok",...}`; `/` returns the CouchDB welcome JSON; `_all_dbs` returns `["_global_changes","_replicator","_users"]`; the preflight returns 2xx with an `Access-Control-Allow-Origin` header.
|
|
|
|
Verify the cookie has both `SameSite=None` and `Secure`:
|
|
|
|
```bash
|
|
curl -i -X POST https://$DOMAIN/_session \
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
-d "name=admin&password=$PASS" | grep -i set-cookie
|
|
```
|
|
|
|
Expected:
|
|
```
|
|
set-cookie: AuthSession=...; Version=1; Expires=...; Max-Age=600; Path=/; HttpOnly; SameSite=None; Secure
|
|
```
|
|
|
|
If `Secure` is missing, the nginx sidecar isn't fronting the request — check that the Coolify domain is assigned to `nginx:5984` and not to `couchdb` directly.
|
|
|
|
If the init logs show CORS errors, you can re-trigger only that step by redeploying — the PUTs against `/_node/_local/_config/...` are idempotent.
|
|
|
|
## CORS
|
|
|
|
`credentials = true` with an explicit origin list — required for PouchDB-style cookie sessions (`POST /_session`). Wildcard origin is incompatible with credentials per the CORS spec, so allowed origins must be enumerated.
|
|
|
|
Update the `put_config cors origins` line in `docker-compose.yml` (comma-separated, no spaces) and redeploy when you add a new frontend. Or apply live:
|
|
|
|
```
|
|
curl -fsS -X PUT -u admin:$PASS \
|
|
-H "Content-Type: application/json" \
|
|
https://couch.apoena.dev/_node/_local/_config/cors/origins \
|
|
-d '"http://localhost:8080,https://new-frontend.example.com"'
|
|
```
|
|
|
|
## Hardcoded values (cloning to another Coolify resource)
|
|
|
|
Two strings in `docker-compose.yml` are pinned to this specific Coolify deployment and must be updated if you redeploy as a new resource:
|
|
|
|
- The host in `traefik.http.routers.coolcouch.rule=Host(`couch.apoena.dev`)`
|
|
- The per-resource Coolify network name, appearing twice: `traefik.docker.network=lvw8efvnvsxduodrkg68zul3` and `networks.coolify.name: lvw8efvnvsxduodrkg68zul3`
|
|
|
|
They're hardcoded because Coolify does not interpolate `${...}` inside the `labels:` block (it does inside `environment:` and `networks.<name>.name:`). The network UUID matches `COOLIFY_RESOURCE_UUID` for the resource — find it in Coolify's UI or in `docker network ls` after first deploy.
|
|
|
|
A `sed` swap before push, or a Coolify "Custom Build Command" running `sed -i` over the file, is the simplest way to keep this template portable.
|
|
|
|
## Signup service
|
|
|
|
A small Gleam HTTP service in `signup/` exposes one endpoint that creates CouchDB users gated by a shared invite code. Lives on `signup-couch.apoena.dev`.
|
|
|
|
### Env vars (set in Coolify UI)
|
|
|
|
- `INVITE_CODE` — required. The shared code users must present to sign up.
|
|
|
|
The signup service reuses `COUCHDB_USER` / `COUCHDB_PASSWORD` for its CouchDB admin call (already required by the couchdb service).
|
|
|
|
### Endpoint
|
|
|
|
```
|
|
POST https://signup-couch.apoena.dev/signup
|
|
Content-Type: application/json
|
|
|
|
{"username": "julien", "password": "at-least-8-chars", "invite_code": "<INVITE_CODE>"}
|
|
```
|
|
|
|
Responses:
|
|
- `201 {"ok": true, "username": "..."}` — user created
|
|
- `400 {"error": "invalid_payload" | "invalid_username" | "password_too_short"}`
|
|
- `401 {"error": "invalid_invite_code"}`
|
|
- `409 {"error": "user_exists"}`
|
|
- `502 {"error": "couchdb_unreachable" | "couchdb_error"}`
|
|
|
|
The frontend should then `POST https://couch.apoena.dev/_session` with the same credentials to obtain the AuthSession cookie.
|
|
|
|
### CORS
|
|
|
|
`SIGNUP_ALLOWED_ORIGINS` (already wired in `docker-compose.yml`) holds the comma-separated origin allow-list. Edit it there or override via Coolify env vars.
|
|
|
|
## Files
|
|
|
|
- `docker-compose.yml` — services, volume, Traefik labels, init sidecar
|
|
- `nginx/default.conf` — reverse-proxy config that adds `Secure` to AuthSession cookies
|
|
- `signup/` — Gleam signup service (Dockerfile, gleam.toml, src/)
|