fix(auth): front CouchDB with nginx to mark AuthSession cookie Secure

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).
This commit is contained in:
Julien Calixte
2026-06-02 22:05:47 +02:00
parent c3c75787f4
commit 2aa10b1711
3 changed files with 83 additions and 8 deletions

View File

@@ -1,6 +1,14 @@
# coolcouch
CouchDB 3 on Coolify. The `couchdb-init` sidecar waits for the server to come up, creates the three system databases, and enables CORS via the config API — no file mounts required.
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
@@ -8,7 +16,8 @@ CouchDB 3 on Coolify. The `couchdb-init` sidecar waits for the server to come up
2. In the resource's **Environment Variables** tab, set:
- `COUCHDB_PASSWORD` — required
- `COUCHDB_USER` — optional, defaults to `admin`
3. In the resource's **Domains** tab, assign a domain to the `couchdb` service on port `5984`. Coolify fills `SERVICE_FQDN_COUCHDB_5984` from this.
- `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
@@ -26,6 +35,21 @@ curl -fsS -I -X OPTIONS https://$DOMAIN/ \
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
@@ -87,4 +111,5 @@ The frontend should then `POST https://couch.apoena.dev/_session` with the same
## 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/)

View File

@@ -3,7 +3,6 @@ services:
image: couchdb:3
restart: unless-stopped
environment:
SERVICE_FQDN_COUCHDB_5984:
COUCHDB_USER: ${COUCHDB_USER:-admin}
COUCHDB_PASSWORD: ${COUCHDB_PASSWORD}
expose:
@@ -12,6 +11,24 @@ services:
- couchdb_data:/opt/couchdb/data
networks:
- default
# nginx fronts CouchDB to add `Secure` to the AuthSession cookie. CouchDB only
# marks the cookie Secure when mochiweb sees a true HTTPS socket, which never
# happens behind a TLS-terminating proxy — so SameSite=None cookies would be
# dropped by browsers on cross-origin requests. See nginx/default.conf.
nginx:
image: nginx:alpine
restart: unless-stopped
depends_on:
- couchdb
environment:
SERVICE_FQDN_NGINX_5984:
expose:
- "5984"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- default
- coolify
labels:
- "traefik.enable=true"
@@ -59,11 +76,15 @@ services:
"$$base/_node/_local/_config/$$1/$$2" -d "\"$$3\"" >/dev/null
echo "cfg $$1/$$2 = $$3"
}
put_config chttpd enable_cors true
put_config cors origins 'http://localhost:8080,http://localhost:5173,http://localhost:3000,http://localhost:4173,https://vaquant.at'
put_config cors credentials true
put_config cors methods 'GET, PUT, POST, HEAD, DELETE'
put_config cors headers 'accept, authorization, content-type, origin, referer, x-csrf-token'
put_config chttpd enable_cors true
put_config cors origins 'http://localhost:8080,http://localhost:5173,http://localhost:3000,http://localhost:4173,https://vaquant.at'
put_config cors credentials true
put_config cors methods 'GET, PUT, POST, HEAD, DELETE'
put_config cors headers 'accept, authorization, content-type, origin, referer, x-csrf-token'
# SameSite=None is required for PouchDB cross-origin cookie sessions.
# Browsers also require Secure on SameSite=None cookies — that flag is
# added by the nginx sidecar via proxy_cookie_flags.
put_config couch_httpd_auth same_site none
signup:
build:

29
nginx/default.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 5984;
server_name _;
# PouchDB attachments and _bulk_docs can be large.
client_max_body_size 256m;
# CouchDB's _changes?feed=continuous needs HTTP/1.1, no buffering, long timeouts.
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 1h;
location / {
proxy_pass http://couchdb:5984;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header Connection "";
# CouchDB never marks AuthSession cookies Secure behind a TLS-terminating
# proxy — mochiweb only checks the raw socket scheme, and ignores
# x_forwarded_proto/x_forwarded_ssl for cookie attribution. Add Secure
# here so SameSite=None cookies survive cross-origin requests.
proxy_cookie_flags AuthSession secure;
}
}