vinindkoeb/INSTALL.md
carsten 17cbc1af57 Ret INSTALL.md: systemd-service skal køre som root, ikke en dedikeret bruger
uv's managede Python-interpreter installeres under den kørende brugers
hjemmemappe (fx /root/... hvis uv sync køres som root) — en separat
lavprivilegeret servicebruger kan ikke eksekvere den, hvilket fejler
med systemd status=203/EXEC. Fundet under den første rigtige
produktionsopsætning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:32:04 +02:00

239 lines
8.2 KiB
Markdown

# Installationsvejledning — produktionsopsætning
Denne vejledning dækker opsætning af Vinindkøb-backend'en (API +
offentlig side + admin-UI) på en rigtig server, fra en frisk
installation til en kørende løsning. Se [README.md](README.md) for
lokal udvikling.
Vejledningen forudsætter at der allerede findes en kørende
Postal-instans (selvhostet mailserver — se CLAUDE.md's "Tech stack")
med DKIM sat op for det domæne der sendes mails fra. Selve
Postal-opsætningen er uden for denne vejlednings omfang.
**Antaget topologi** (Proxmox): appen kører i sin egen LXC-container,
sammen med sin PostgreSQL-database. Reverse proxy'en (CloudPanel)
kører i en **anden** container/VM på samme Proxmox-host — appens
container skal derfor være tilgængelig over det interne
Proxmox-netværk, ikke kun lokalt i sig selv. Kører reverse proxy'en i
stedet i **samme** container som appen, kan trin 9's `--host` i
stedet sættes til `127.0.0.1`, og firewall-reglen i samme trin
udelades.
## 1. Opret Proxmox-containeren
- **Unprivileged LXC-container** — intet i denne app kræver
privilegeret adgang.
- **OS-template:** Debian 12 anbefales (stabil, letvægt). `uv` styrer
selv Python 3.13 uafhængigt af systemets Python-version, så
distro-valget er ikke kritisk.
- **Størrelse:** appen er lille (én organisation i drift i dag) og
kører sin egen Postgres i samme container — 1-2 vCPU, 1-2 GB RAM og
8-10 GB disk er rigeligt til at starte med.
- **Netværk:** giv containeren en **statisk IP** på Proxmox' interne
bridge (fx `vmbr0`), så CloudPanel-containeren kan nå den
konsistent.
## 2. Forudsætninger i containeren
- Python 3.13 (se `.python-version`)
- [`uv`](https://docs.astral.sh/uv/) (Python-pakkehåndtering)
- Node.js + npm (til at bygge admin-UI'et, `admin-ui/`)
- PostgreSQL (`apt install postgresql`, da databasen kører i samme
container som appen)
- git
## 3. Hent koden
```bash
git clone <repo-url> /sti/til/vinindkoeb
cd /sti/til/vinindkoeb
uv sync
```
## 4. Database
Opret en Postgres-bruger og -database der matcher formatet i
`DATABASE_URL` (`postgresql+psycopg://<bruger>:<kodeord>@<host>:5432/<database>`):
```sql
CREATE USER vinindkoeb WITH PASSWORD '...';
CREATE DATABASE vinindkoeb OWNER vinindkoeb;
```
## 5. Miljøvariabler
```bash
cp .env.example .env
```
Udfyld hvert felt i `.env`:
| Variabel | Hvor den kommer fra |
|---|---|
| `DATABASE_URL` | Fra trin 4 (`@localhost`, da Postgres kører i samme container) |
| `ENVIRONMENT` | `production` |
| `SECRET_KEY` | Generér med `openssl rand -hex 32` |
| `ACCESS_TOKEN_EXPIRE_MINUTES` / `ELEVATION_EXPIRE_MINUTES` | Standardværdierne er fine, med mindre andet ønskes |
| `POSTAL_BASE_URL` / `POSTAL_API_KEY` | Fra Postal-instansens admin-UI (opret en API-nøgle til denne server) |
| `POSTAL_WEBHOOK_PUBLIC_KEY_B64` | DKIM-nøglen for afsenderdomænet — find med `dig +short TXT <dkim-selector>._domainkey.<domæne>`, brug værdien af `p=` |
| `ADMIN_DOMAIN` | Domænet admin-UI'et skal serveres på, fx `admin.vinindkoeb.dk` |
## 6. Migrations
```bash
uv run alembic upgrade head
```
Dette opretter alle tabeller og seeder `WineCategory` automatisk
(vinkategorierne er globale, delt af alle organisationer/ruter).
## 7. Første organisation, rute og admin-bruger
Der findes ingen selvbetjenings-registrering — uden en bruger i
databasen kan ingen logge ind. Brug bootstrap-scriptet:
```bash
uv run python scripts/create_admin_user.py \
--org "Din Organisation" \
--route "Din Rute" \
--email admin@example.com \
--name "Dit Navn" \
--password "et-sikkert-kodeord"
```
Scriptet genbruger en organisation/rute med samme navn, hvis den
allerede findes — sikkert at køre igen for at oprette endnu en
admin-bruger på en eksisterende organisation.
## 8. Byg admin-UI'et
```bash
cd admin-ui
npm install
npm run build # skriver til ../app/admin_dist
cd ..
```
Uden dette trin kører API'et og den offentlige side fint, men
`ADMIN_DOMAIN` viser ingenting (se `app/admin_site.py`s
sikkerhedsnet).
## 9. Kør backend'en som en vedvarende service
Eksempel på en `systemd`-unit (`/etc/systemd/system/vinindkoeb.service`):
```ini
[Unit]
Description=Vinindkøb backend
After=network.target postgresql.service
[Service]
Type=simple
WorkingDirectory=/sti/til/vinindkoeb
ExecStart=/sti/til/vinindkoeb/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
EnvironmentFile=/sti/til/vinindkoeb/.env
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now vinindkoeb
```
**Om `User=`:** eksemplet ovenfor kører som root (ingen `User=`-linje)
— simplest, og rimeligt for en lille, isoleret container som denne.
Vil du hellere køre som en dedikeret systembruger
(`useradd --system --no-create-home --shell /usr/sbin/nologin
vinindkoeb`, `chown -R vinindkoeb:vinindkoeb /sti/til/vinindkoeb`,
`User=vinindkoeb` i unit-filen), **skal `uv sync` også køres som den
samme bruger** — `uv` installerer sin egen Python-interpreter under
den kørende brugers hjemmemappe (fx `/root/.local/share/uv/...`),
som andre brugere ikke kan tilgå. Kører man `uv sync` som root og
prøver bagefter at starte servicen som en anden bruger, fejler
`systemd` med `status=203/EXEC` (kunne ikke eksekvere programmet) —
fundet under den første rigtige produktionsopsætning.
Da reverse proxy'en (CloudPanel) kører i en anden container, skal
backend'en kunne nås over Proxmox' interne netværk — deraf
`--host 0.0.0.0` i stedet for `127.0.0.1`. Det skal dog **ikke** stå
åbent for alt andet: brug Proxmox' egen indbyggede firewall på
containeren (fanen "Firewall" på containeren, eller Datacenter →
Firewall) til én regel: tillad TCP på port `8000` kun fra
CloudPanel-containerens IP, afvis ellers. Det er mere idiomatisk og
nemmere at vedligeholde end `iptables`/`nftables` inde i selve
gæste-OS'et.
## 10. Domæner + reverse proxy
Løsningen afgør selv hvilken rute/hvilket indhold der skal vises ud
fra requestens `Host`-header (se CLAUDE.md's "Route" og
"Admin-UI-arkitektur") — det betyder at **alle** domæner nedenfor
bare skal pege på samme kørende backend (samme container-IP og port,
fra trin 9), uanset hvilken reverse proxy-løsning der bruges:
- Organisationens/rutens `public_domain`-felt(er) i databasen, fx
`vinindkoeb.dk` og `test.vinindkoeb.dk`
- `ADMIN_DOMAIN` fra `.env`, fx `admin.vinindkoeb.dk`
**Med CloudPanel:** opret ét "Reverse Proxy"-site pr. domæne, der
peger på `<vinindkoeb-containerens-ip>:8000` (IP'en fra trin 1, porten
fra trin 9), og aktivér Let's Encrypt-certifikat for hvert site.
Bruges en anden løsning (nginx, Caddy, Traefik, ...), er princippet
det samme: én vhost/site pr. domæne, alle proxyet til samme
container-IP og port.
## 11. DNS
A-records (eller CNAME, alt efter opsætning) for hvert domæne fra
trin 10, der peger på **CloudPanel-serverens** offentlige IP-adresse
(ikke Vinindkøb-containerens — det er CloudPanel der tager imod
trafik udefra og sender den videre internt).
## 12. Postal-webhook
I Postal-instansens konfiguration: sæt webhook-URL'en til
`https://<det rigtige domæne>/webhooks/postal` (fx
`https://vinindkoeb.dk/webhooks/postal`) — det er sådan
leveringsstatus (sendt/leveret/afvist) opdateres asynkront på
`MailLog` (se opgave 7c).
## 13. Sæt rutens felter
For hver rute (den rigtige, og evt. en test-rute) skal disse felter
sættes direkte i databasen — der findes ingen rute-indstillings-skærm
i admin-UI'et endnu:
```sql
UPDATE route SET
public_domain = 'vinindkoeb.dk',
sender_name = 'Afsendernavn',
sender_email = 'afsender@vinindkoeb.dk'
WHERE id = <rute-id>;
```
## 14. Verifikation
1. `curl https://<rigtigt domæne>/health` → `{"status":"ok","database":"ok"}`.
2. Besøg det rigtige domæne i en browser → bestillings-/tilmeldingssiden.
3. Besøg `ADMIN_DOMAIN` → login-siden, log ind med brugeren fra trin 7.
4. Afprøv en tilmelding/bestilling på test-ruten (hvis en sådan er
sat op) og bekræft at en rigtig mail bliver sendt og logget i
`MailLog`.
## 15. Fremtidige opdateringer
```bash
git pull
uv sync
uv run alembic upgrade head
cd admin-ui && npm install && npm run build && cd ..
sudo systemctl restart vinindkoeb
```
## Backup
Ikke uddybet her, men en simpel `pg_dump`-baseret cron-opgave af
databasen anbefales som minimum.