vinindkoeb/INSTALL.md
carsten d3e6b92d9f Opdatér INSTALL.md til Proxmox LXC-container-topologi
Tilføjer trin 1 (opret containeren: unprivileged LXC, Debian 12,
statisk IP) og retter netværks-antagelsen: da reverse proxy'en
(CloudPanel) kører i en anden container/VM på samme Proxmox-host end
appen, skal backend'en bindes til 0.0.0.0 i stedet for 127.0.0.1 og
være tilgængelig over det interne Proxmox-netværk — låst ned med
Proxmox' egen container-firewall (kun CloudPanel-containerens IP får
adgang) i stedet for 127.0.0.1-only. Reverse proxy-trinnet peger nu på
containerens IP, og PostgreSQL installeres nativt i samme container
som appen (bekræftet valg).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 10:31:41 +02:00

7.4 KiB

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 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 (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

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>):

CREATE USER vinindkoeb WITH PASSWORD '...';
CREATE DATABASE vinindkoeb OWNER vinindkoeb;

5. Miljøvariabler

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

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:

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

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.pys sikkerhedsnet).

9. Kør backend'en som en vedvarende service

Eksempel på en systemd-unit (/etc/systemd/system/vinindkoeb.service):

[Unit]
Description=Vinindkøb backend
After=network.target postgresql.service

[Service]
Type=simple
User=vinindkoeb
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
sudo systemctl daemon-reload
sudo systemctl enable --now vinindkoeb

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:

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

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.