Julien Calixte 243f9a8ed5 docs: add handoff for longpoll HTTP/2 truncation bug
The AuthSession cookie fix is shipped, but PouchDB longpoll requests
get truncated at ~43s by Traefik on HTTP/2. HTTP/1.1 holds fine;
CouchDB and nginx are clean. Captures the diagnosis trail and the
three options for the next session so the work isn't re-derived.
2026-06-02 23:26:13 +02:00

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:

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:

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/)
Description
No description provided
Readme 71 KiB
Languages
Gleam 94%
Dockerfile 6%