Merge pull request #10 from madhura68/docs/tailscale-setup-runbook

docs(runbook): tailscale-setup plan + uitvoering-addendum
This commit is contained in:
Janpeter Visser 2026-05-14 10:48:41 +00:00 committed by GitHub
commit 84b3afbefa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,446 @@
# Ubuntu-omgeving (Postgres + app) via Tailscale bereikbaar maken vanaf de Mac
## Context
Er zijn twee Scrum4Me-omgevingen:
- **Omgeving 1** — productie: Vercel + Neon (managed Postgres).
- **Omgeving 2** — nieuw: een eigen Ubuntu-server (`scrum4me-srv`) die de
**volledige Scrum4Me-app (Next.js achter een reverse proxy) + zelf-gehoste
Postgres** gaat draaien.
Het doel: vanaf de Mac (`janpeters-macbook-pro`) omgeving 2 kunnen gebruiken —
voor (1) een DB-client (psql/GUI), (2) de `scrum4me-docker` runner lokaal in
Docker, en (3) lokale dev van de hoofd-Scrum4Me-app.
Tailscale is al geïnstalleerd en verbonden op beide machines:
- `janpeters-macbook-pro``100.73.234.116`
- `scrum4me-srv``100.118.195.120` (Linux, SSH aan)
Wat nog ontbreekt: zowel Postgres als de Next.js-app op de Ubuntu-server
luisteren standaard alleen op `localhost` en zijn nog niet bereikbaar over de
Tailscale-interface. De database zelf is **al volledig ingericht** (schema +
data) — er is geen migratie- of seed-werk nodig, alleen netwerk-, auth- en
connectie-configuratie.
**Beslissingen (van de gebruiker):**
- App-deploy op Ubuntu: **reverse proxy** (nginx/Caddy) vóór Next.js.
- DB-toegang: **hele tailnet** mag erbij (`100.64.0.0/10`) — bewuste keuze;
later eventueel te versmallen via Tailscale ACLs/groups.
- Postgres-rol: **nog onzeker** — het plan voegt een controle toe en adviseert
een dedicated rol.
**Canonieke `SCRUM4ME_BASE_URL`:** `http://100.118.195.120` (reverse proxy op
poort 80 op de Tailscale-interface, **plain HTTP**). Tailscale (WireGuard)
verzorgt de transportencryptie binnen de tailnet, dus een tweede TLS-laag is
hier niet nodig. Dit raw-IP-adres resolvet ook vanuit een Docker-container
(geen MagicDNS-afhankelijkheid). HTTPS op de proxy is optionele hardening — zie
de noot onderaan; kies je daarvoor, gebruik dan een hostnaam die óók vanuit
Docker oplost en pas álle URL's hieronder consistent aan.
**Bevinding uit de codebase:** in `scrum4me-docker` is de DB-koppeling puur
config. Zowel `bin/run-one-job.ts` (regel 30, 115) als de MCP-server
(`mcp-config.json` regel 9-10) lezen `DATABASE_URL` / `DIRECT_URL` uit de
omgeving. `bin/check-tokens.sh` (regel 35-38) doet bovendien een harde
`curl ${SCRUM4ME_BASE_URL}/api/products` — onbereikbaarheid is fataal
(regel 52-57). Er zijn **geen code-wijzigingen** nodig — alleen `.env`.
## Voorwaarden (aantoonbaar voldaan vóór uitvoering)
- [ ] Tailscale actief op beide machines (`tailscale status` toont beide nodes)
- [ ] SSH naar scrum4me-srv werkt (`ssh scrum4me-srv echo ok`)
- [ ] DB-schema aanwezig (tabellen + data) — géén migratie nodig
## Voorwaarden (input van de gebruiker nodig)
- Postgres-rol + wachtwoord + databasenaam op de Ubuntu-server (de "USER",
"PASS", "DBNAME" hieronder). Niet in de chat delen — alleen lokaal invullen.
- De reverse proxy biedt de app aan op `http://100.118.195.120` (poort 80).
Wijkt dit af, pas dan overal de canonieke URL consistent aan.
---
## Deel A — Ubuntu: Postgres openstellen op de Tailscale-interface
Uit te voeren op `scrum4me-srv` (via `ssh scrum4me-srv` of `tailscale ssh`).
1. **Tailscale-IP bevestigen**
```bash
tailscale status
tailscale ip -4 # verwacht: 100.118.195.120
```
2. **`listen_addresses` uitbreiden** — Postgres bindt standaard alleen aan
localhost. Vind het configbestand en pas aan:
```bash
sudo -u postgres psql -c 'SHOW config_file;' # bv. /etc/postgresql/16/main/postgresql.conf
```
Zet in dat bestand:
```
listen_addresses = 'localhost,100.118.195.120'
```
Bewust **niet** `'*'` — zo bindt Postgres alleen aan localhost + het
Tailscale-adres, nooit aan de publieke interface.
> ⚠️ **Boot-order:** door aan `100.118.195.120` te binden moet `tailscale0`
> al bestaan bij boot. Stap A6 maakt de systemd-ordering verplicht — sla A6
> niet over, anders faalt Postgres na een reboot.
3. **Rol, auth-methode en grants controleren/instellen** (voorkomt een
login- of permission-fout ná goede netwerkconfig). De rol is nog onzeker,
dus eerst inventariseren:
```bash
sudo -u postgres psql -c '\du'
sudo -u postgres psql -c 'SHOW password_encryption;'
```
**Advies:** gebruik (of maak) een **dedicated runtime-rol** die alleen de
rechten heeft die de app/runner nodig heeft — geen superuser:
```sql
CREATE ROLE scrum4me_app LOGIN PASSWORD 'lokaal-wachtwoord';
GRANT CONNECT ON DATABASE DBNAME TO scrum4me_app;
-- runtime-rechten op het bestaande (gevulde) public-schema:
GRANT USAGE ON SCHEMA public TO scrum4me_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO scrum4me_app;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO scrum4me_app;
-- zodat ook later toegevoegde tabellen/sequences werken:
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO scrum4me_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO scrum4me_app;
```
**Migraties:** deze runtime-rol krijgt bewust géén DDL-rechten. De DB is al
ingericht, dus in normale operatie draaien er geen migraties via deze rol.
Moet je later vanaf de Mac toch een Prisma-migratie draaien, gebruik dan de
DB-owner-rol (apart wachtwoord), niet `scrum4me_app`.
**SCRAM-verifier:** stap A4 kiest `scram-sha-256`. Een rol waarvan het
wachtwoord nog als **md5** is opgeslagen kan dan niet inloggen. Forceer een
SCRAM-verifier door het wachtwoord opnieuw te zetten (alleen lokaal op de
server, niet in chat/docs delen):
```sql
ALTER ROLE scrum4me_app WITH PASSWORD 'lokaal-wachtwoord';
```
4. **`pg_hba.conf` — toegang vanaf de tailnet toestaan**
```bash
sudo -u postgres psql -c 'SHOW hba_file;'
```
Voeg een regel toe (boven de bestaande `host`-regels). De gebruiker koos
bewust voor toegang vanaf de **hele tailnet**; maak rol en database wel
expliciet i.p.v. `all all`:
```
# Scrum4Me clients via Tailscale
host DBNAME scrum4me_app 100.64.0.0/10 scram-sha-256
```
Let op: hiermee mag elke tailnet-node mét geldige credentials verbinden.
Wil je dat later inperken, doe dat via Tailscale ACLs/groups of versmal
het CIDR naar specifieke node-IP's.
5. **Firewall (defense-in-depth)** — alleen relevant als `ufw` actief is:
```bash
sudo ufw status
sudo ufw allow in on tailscale0 to any port 5432 proto tcp
```
Open 5432 **nooit** generiek (`sudo ufw allow 5432` zonder interface) —
dat zou de DB internet-breed openstellen.
6. **Boot-order — VERPLICHT.** Postgres bindt aan `100.118.195.120`, een adres
dat pas bestaat nadat `tailscaled` `tailscale0` heeft opgezet.
**Zonder deze override faalt Postgres bij reboot** ("cannot assign requested
address"). Voeg een systemd-override toe:
```bash
sudo systemctl edit postgresql # of postgresql@<versie>-main
```
```ini
[Unit]
After=tailscaled.service
Requires=tailscaled.service
```
7. **Postgres herstarten en verifiëren**
```bash
sudo systemctl restart postgresql
sudo ss -tlnp | grep 5432 # moet 127.0.0.1:5432 én 100.118.195.120:5432 tonen
```
---
## Deel B — Ubuntu: de Scrum4Me-app (reverse proxy) bereikbaar maken op Tailscale
De runner-tokencheck (`check-tokens.sh`) cURL't `${SCRUM4ME_BASE_URL}/api/products`
en faalt hard als die URL onbereikbaar is. De Next.js-app draait achter een
reverse proxy, dus de **proxy** moet op het Tailscale-adres luisteren — niet
alleen op `localhost`. Canoniek: `http://100.118.195.120` (poort 80, plain HTTP).
1. **Reverse proxy op de Tailscale-interface laten luisteren (poort 80)**
- **nginx:** in het server-block het `listen`-adres aan het Tailscale-IP
binden:
```
listen 100.118.195.120:80;
```
`sudo nginx -t``sudo systemctl reload nginx`.
- **Caddy:** site-adres `http://100.118.195.120:80` in de `Caddyfile`.
- Next.js zelf mag op `127.0.0.1:<intern>` blijven; alleen de proxy is
extern bereikbaar.
2. **Boot-order — VERPLICHT.** Net als Postgres bindt de proxy aan een adres
dat `tailscaled` eerst moet aanmaken. **Zonder deze override faalt nginx/Caddy
bij reboot.**
```bash
sudo systemctl edit nginx # of caddy
```
```ini
[Unit]
After=tailscaled.service
Requires=tailscaled.service
```
3. **Firewall voor poort 80** — alleen bij actieve `ufw`:
```bash
sudo ufw allow in on tailscale0 to any port 80 proto tcp
```
4. **Lokaal op de server verifiëren**
```bash
curl -fsS -H "Authorization: Bearer $SCRUM4ME_TOKEN" \
http://100.118.195.120/api/products >/dev/null && echo OK
```
---
## Deel C — Mac: connectiviteit verifiëren (DB én app)
1. **Tailscale-bereik** (al bevestigd: `scrum4me-srv` zichtbaar op
`100.118.195.120`):
```bash
tailscale status
tailscale ping scrum4me-srv
```
2. **MagicDNS check** — kan de Mac de server op hostnaam bereiken?
```bash
ping -c1 scrum4me-srv
```
Zo ja: native macOS-clients mogen `scrum4me-srv` als host gebruiken.
3. **Postgres — TCP- en psql-test**
```bash
nc -vz 100.118.195.120 5432
psql "postgresql://USER:PASS@100.118.195.120:5432/DBNAME?sslmode=disable" -c '\dt'
```
4. **App — vanaf de Mac én vanuit een Docker-container** (Docker Desktop heeft
geen Tailscale-MagicDNS; daarom de canonieke raw-IP-URL):
```bash
# vanaf de Mac
curl -fsS -H "Authorization: Bearer $SCRUM4ME_TOKEN" \
http://100.118.195.120/api/products >/dev/null && echo "mac OK"
# vanuit een container (simuleert de runner)
docker run --rm --env SCRUM4ME_TOKEN alpine sh -lc \
'wget -qO- --header "Authorization: Bearer $SCRUM4ME_TOKEN" \
http://100.118.195.120/api/products >/dev/null && echo "docker OK"'
```
Slaagt de container-test niet (geen route naar de tailnet vanuit Docker
Desktop), dan moet Tailscale in/naast de runner-container draaien — apart
uit te zoeken; eerst de directe route testen.
---
## Deel D — De drie consumenten koppelen
Welke host elke consument gebruikt verschilt — MagicDNS werkt wél native op
macOS, maar niet binnen een Docker-container:
| Consumer | Host | Reden |
|---|---|---|
| DB-client (psql/TablePlus) native | `scrum4me-srv` | MagicDNS werkt op macOS |
| scrum4me-docker runner (Docker) | `100.118.195.120` | Docker Desktop heeft geen MagicDNS |
| Hoofd-app lokale dev | `scrum4me-srv` | MagicDNS werkt op macOS |
### 1. DB-client (psql / TablePlus / DBeaver) — native op macOS
Connection-string:
```
postgresql://USER:PASS@scrum4me-srv:5432/DBNAME?sslmode=disable
```
GUI-clients: host `scrum4me-srv` (of `100.118.195.120`), poort `5432`,
SSL uit.
### 2. scrum4me-docker runner — bewerk `/Users/janpetervisser/Development/scrum4me-docker/.env`
```
DATABASE_URL=postgresql://USER:PASS@100.118.195.120:5432/DBNAME?sslmode=disable
DIRECT_URL=postgresql://USER:PASS@100.118.195.120:5432/DBNAME?sslmode=disable
SCRUM4ME_BASE_URL=http://100.118.195.120
```
Belangrijk:
- **Gebruik het rauwe Tailscale-IP, niet `scrum4me-srv`.** Docker Desktop-
containers krijgen geen Tailscale-MagicDNS; de hostnaam resolvet niet
binnen de container.
- Laat de Neon-specifieke params (`channel_binding=require`,
`sslmode=verify-full`) weg — die gelden niet voor zelf-gehoste Postgres.
- Zorg dat `SCRUM4ME_TOKEN` een token van **omgeving 2** is — de tokencheck
loopt tegen de Ubuntu-app, niet meer tegen Vercel.
> `SCRUM4ME_TOKEN` haal je op via de Ubuntu-app: Settings → API Tokens → nieuw
> token aanmaken. Een bestaand Vercel-token werkt **niet** tegen de
> Ubuntu-omgeving.
### 3. Hoofd-Scrum4Me-app (lokale dev) — bewerk `/Users/janpetervisser/Development/Scrum4Me`
Dit is een **andere repo** dan `scrum4me-docker`. In die repo de `.env.local`
(of `.env`) aanpassen. De app draait native op macOS, dus de MagicDNS-hostnaam
`scrum4me-srv` mag hier wél:
```
DATABASE_URL=postgresql://USER:PASS@scrum4me-srv:5432/DBNAME?sslmode=disable
DIRECT_URL=postgresql://USER:PASS@scrum4me-srv:5432/DBNAME?sslmode=disable
```
---
## SSL-keuze
Aanbeveling: **`sslmode=disable`** voor Postgres en **plain HTTP** voor de app-
URL. Tailscale (WireGuard) versleutelt het transport al end-to-end binnen de
tailnet; een tweede TLS-laag op een zelf-gehoste Postgres of op de proxy levert
hier vooral configuratie-gedoe op.
Optionele hardening (later, samenhangend uit te voeren):
- TLS-certs op Postgres + `sslmode=require`.
- HTTPS op de reverse proxy. Doe dit dan met een **DNS-naam die ook vanuit
Docker oplost** (raw-IP + cert geeft validatiefouten), en pas `SCRUM4ME_BASE_URL`
én alle verificatie-`curl`s consistent aan naar die `https://`-hostnaam.
## Twee omgevingen naast elkaar houden
Omdat omgeving 1 (Neon) blijft bestaan: bewaar twee env-varianten, bv.
`.env.neon` en `.env.ubuntu`, en symlink de actieve naar `.env`:
```bash
ln -sf .env.ubuntu .env # activeer Ubuntu-omgeving
ln -sf .env.neon .env # activeer Neon-omgeving
```
Lichtgewicht en voorkomt dat je per ongeluk de verkeerde DB raakt.
## Te wijzigen bestanden
**Op `scrum4me-srv`:**
- `postgresql.conf``listen_addresses`.
- `pg_hba.conf` — tailnet-regel voor `DBNAME` + dedicated rol.
- Postgres-rol — dedicated `scrum4me_app`-rol + grants + `ALTER ROLE ... PASSWORD`.
- nginx/Caddy-config — `listen` op `100.118.195.120:80`.
- systemd-overrides — `After=/Requires=tailscaled.service` voor `postgresql`
en de proxy.
- evt. `ufw`-regels voor poort 5432 en 80 op `tailscale0`.
**Op de Mac:**
- `/Users/janpetervisser/Development/scrum4me-docker/.env``DATABASE_URL`,
`DIRECT_URL`, `SCRUM4ME_BASE_URL`, `SCRUM4ME_TOKEN`.
- `/Users/janpetervisser/Development/Scrum4Me/.env.local``DATABASE_URL`,
`DIRECT_URL` (andere repo).
- Géén codewijzigingen in `scrum4me-docker`.
## Verificatie (end-to-end)
1. **Netwerk:** `nc -vz 100.118.195.120 5432` vanaf de Mac slaagt.
2. **DB-client:** `psql ".../DBNAME?sslmode=disable" -c '\dt'` toont de
Scrum4Me-tabellen; een test-`INSERT`/`SELECT` bevestigt dat de
`scrum4me_app`-grants kloppen.
3. **App-bereik:** de `curl`/`docker run`-tests uit Deel C-4 geven beide `OK`.
4. **Reboot-test:** herstart `scrum4me-srv`; controleer daarna met
`sudo ss -tlnp` dat Postgres én de proxy weer op `100.118.195.120` luisteren,
en herhaal de Mac/Docker-connectiviteitstests.
5. **Runner:** na `.env`-update `docker compose up -d --force-recreate`,
dan `docker compose logs -f``check-tokens.sh` moet
"OK: 100.118.195.120:5432 reachable" én "OK: SCRUM4ME_TOKEN works" loggen,
en de daemon-loop moet een job kunnen claimen uit de Ubuntu-DB.
6. **Hoofd-app:** lokale dev-server in `/Users/janpetervisser/Development/Scrum4Me`
start en leest data uit de Ubuntu-DB.
## Veelvoorkomende fouten
| Fout | Oorzaak | Fix |
|---|---|---|
| `could not translate host name "scrum4me-srv"` | MagicDNS niet actief (Docker) | Gebruik raw IP `100.118.195.120` |
| `cannot assign requested address` bij Postgres-start | `tailscale0` bestaat nog niet | A6 systemd-override toevoegen |
| `FATAL: password authentication failed` | SCRAM-verifier niet bijgewerkt | `ALTER ROLE scrum4me_app WITH PASSWORD '...'` herhalen |
---
# Addendum — uitvoering Ubuntu-kant 2026-05-14
> Deel A + B zijn uitgevoerd. De server bleek een **andere topologie** te hebben
> dan dit plan aannam: Postgres én de reverse proxy draaien als **Docker
> containers**, niet host-geïnstalleerd. Dit addendum beschrijft wat er feitelijk
> is gebeurd. Deel C + D (Mac-kant) staan nog open.
## Vastgestelde topologie (wijkt af van de aannames)
| Plan nam aan | Werkelijkheid op `scrum4me-srv` |
|---|---|
| Host-Postgres (`/etc/postgresql/...`, `systemctl postgresql`) | Docker container `scrum4me-postgres` (postgres:17), data-volume `/srv/scrum4me/postgres` |
| Host nginx/Caddy | Docker container `scrum4me-caddy` (caddy:2), al luisterend op `0.0.0.0:80` + `:443` |
| Migratie/seed mogelijk nodig | Bevestigd niet nodig — db `scrum4me` was gevuld |
Concrete waarden die het plan openliet:
- **Host** = dit ís `scrum4me-srv` (`100.118.195.120`) — Deel A/B dus direct uitgevoerd, niet via SSH.
- **DBNAME** = `scrum4me`
- **Rol** = `scrum4me_app` aangemaakt (non-superuser, DML-only), wachtwoord lokaal gegenereerd via `openssl rand -hex 24`.
## Deel A — zoals feitelijk uitgevoerd (Docker-variant)
| Plan-stap | Aanpassing |
|---|---|
| **A2** `listen_addresses` in `postgresql.conf` | **N.v.t.** — de container luistert intern al op `0.0.0.0`. Host-exposure = Docker port-mapping. In `/srv/scrum4me/compose/docker-compose.yml` toegevoegd: `- "100.118.195.120:5432:5432"` náást de bestaande `127.0.0.1:5432:5432`. Specifiek IP i.p.v. `0.0.0.0` — Docker's iptables-DNAT scoped dan op dat IP, publiek blijft dicht. |
| **A3** rol + grants | Identiek SQL, maar uitgevoerd via `docker exec -i scrum4me-postgres psql -U scrum4me -d scrum4me`. Idempotent script (`CREATE ROLE` of `ALTER ROLE ... PASSWORD`). De `ALTER ROLE ... PASSWORD` zet meteen een SCRAM-verifier. **Let op:** `CREATE ROLE` op de gedeelde productie-DB wordt door de auto-mode classifier geblokkeerd — moet via een script dat de gebruiker zelf draait. |
| **A4** `pg_hba.conf` | Bestand zit in het data-volume: host-pad `/srv/scrum4me/postgres/pg_hba.conf` (root-owned, sudo nodig). Regel toegevoegd onderaan (append is veilig — first-match, geen conflict). **Bevinding:** de postgres-image heeft al een catch-all `host all all all scram-sha-256` — onze scoped regel is dáárdoor strikt genomen redundant. Echte bescherming = IP-scoped port-binding + ufw. Catch-all strakker maken = aparte taak (hij draagt de docker-netwerk-clients). |
| **A5** ufw | Identiek: `ufw allow in on tailscale0 to any port 5432 proto tcp`. |
| **A6** boot-order | **Niet** `postgresql.service` (bestaat niet) maar `docker.service`. Drop-in `/etc/systemd/system/docker.service.d/tailscale-order.conf`. Bewust `After=tailscaled.service` + **`Wants=`** i.p.v. het door het plan voorgestelde `Requires=``Requires` op `docker.service` is fragiel (faalt tailscaled ooit, dan start de hele docker-stack niet). `After=` lost de race op; `Wants=` trekt tailscaled mee zonder hard-fail. |
| **A7** restart + verify | `docker compose up -d postgres` (recreate — `restart` pakt port-wijzigingen niet). `ss -tln` toont nu `127.0.0.1:5432` én `100.118.195.120:5432`. Verificatie met een wegwerp-container: `docker run --rm --network host postgres:17 psql "postgresql://scrum4me_app:...@100.118.195.120:5432/scrum4me?sslmode=disable" -c '\dt'``--network host` simuleert exact hoe de Mac het ziet. |
## Deel B — zoals feitelijk uitgevoerd (Docker-variant)
| Plan-stap | Aanpassing |
|---|---|
| **B1** proxy op tailscale-interface | **Grotendeels al gedaan** — de Caddy-container publiceert al `0.0.0.0:80`, dus luistert al op `tailscale0`. Alleen een site-block toegevoegd aan `/srv/scrum4me/caddy/Caddyfile`: `100.118.195.120:80 { reverse_proxy 172.18.0.1:3000 }`. |
| **B2** boot-order proxy | **Niet nodig.** Caddy bindt aan `0.0.0.0:80`, niet aan een IP-specifiek adres — er is geen `tailscale0`-race. (Alleen Postgres had de IP-specifieke binding, vandaar dat A6 wél nodig was.) |
| **B3** ufw poort 80 | **Niet nodig.** Poort 80 stond al op `ALLOW IN Anywhere`. |
| **B4** verifiëren | `curl -sI http://100.118.195.120/``200 OK`, geen redirect. `/api/products` → 401 (bereikbaar, auth vereist). |
## Bugs / valkuilen tegengekomen tijdens uitvoering
1. **Caddy single-file bind-mount wordt stale na een atomic-rename edit.**
`/srv/scrum4me/caddy/Caddyfile` is als enkel bestand ge-bind-mount. Editors
(en de Edit-tooling) schrijven vaak via write-temp + rename = nieuwe inode.
De container blijft naar de oude inode wijzen → `caddy reload` leest de
**oude** content, schijnbaar zonder fout. Symptoom hier: het nieuwe
site-block leek "stil gedropt" door Caddy's adapter, maar de container zág
het block simpelweg niet.
**Fix / regel:** na een Caddyfile-edit `docker compose up -d --force-recreate
caddy` (of `restart`) — **niet** `caddy reload`. De recreate her-bindt de
mount op de nieuwe inode. (Eerder in het project werkte een Caddyfile-edit
wél, juist omdat daar toevallig een `restart` op volgde.)
2. **`http://<IP>` vs `<IP>:80` syntax — bleek een rode haring.**
Aanvankelijk leek Caddy's Caddyfile-adapter `http://100.118.195.120` te
droppen. Geïsoleerd getest werkte beide syntaxen prima; het echte probleem
was bug #1 (stale mount). De definitieve regel gebruikt `100.118.195.120:80`
— ondubbelzinnig plain-HTTP-op-poort-80.
## Verificatie-status
| Plan verificatie-stap | Status |
|---|---|
| 1. `nc`/TCP naar 5432 | ✓ `psql` als `scrum4me_app` via `100.118.195.120:5432` werkt, ziet tabellen |
| 2. DB-client grants | ✓ `SELECT` op `idea_products` werkt onder `scrum4me_app` |
| 3. App-bereik | ✓ `http://100.118.195.120/` → 200, `/api/products` → 401 |
| 4. **Reboot-test** | ✗ **Nog niet gedaan** — productie-server niet herstart. Handmatig uitvoeren op rustig moment; check daarna `ss -tlnp \| grep 5432`. |
| 5. Runner | — Mac-kant (Deel C/D), nog open |
| 6. Hoofd-app lokale dev | — Mac-kant, nog open |
## Gewijzigde bestanden op `scrum4me-srv`
- `/srv/scrum4me/compose/docker-compose.yml` — postgres `ports`: extra `100.118.195.120:5432:5432`
- `/srv/scrum4me/caddy/Caddyfile` — site-block `100.118.195.120:80`
- `/srv/scrum4me/postgres/pg_hba.conf` — tailnet-regel (+ `.bak-<timestamp>`)
- `/etc/systemd/system/docker.service.d/tailscale-order.conf` — boot-order drop-in (nieuw)
- ufw — regel `5432/tcp on tailscale0`
- Postgres-rol `scrum4me_app` — aangemaakt met grants op db `scrum4me`