# 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. ## 1. Forudsætninger på serveren - 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 - git ## 2. Hent koden ```bash git clone /sti/til/vinindkoeb cd /sti/til/vinindkoeb uv sync ``` ## 3. Database Opret en Postgres-bruger og -database der matcher formatet i `DATABASE_URL` (`postgresql+psycopg://:@:5432/`): ```sql CREATE USER vinindkoeb WITH PASSWORD '...'; CREATE DATABASE vinindkoeb OWNER vinindkoeb; ``` ## 4. Miljøvariabler ```bash cp .env.example .env ``` Udfyld hvert felt i `.env`: | Variabel | Hvor den kommer fra | |---|---| | `DATABASE_URL` | Fra trin 3 | | `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 ._domainkey.`, brug værdien af `p=` | | `ADMIN_DOMAIN` | Domænet admin-UI'et skal serveres på, fx `admin.vinindkoeb.dk` | ## 5. Migrations ```bash uv run alembic upgrade head ``` Dette opretter alle tabeller og seeder `WineCategory` automatisk (vinkategorierne er globale, delt af alle organisationer/ruter). ## 6. 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. ## 7. 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). ## 8. Kør backend'en som en vedvarende service Eksempel på en `systemd`-unit (`/etc/systemd/system/vinindkoeb.service`), uafhængig af hvilken reverse proxy der bruges foran den: ```ini [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 127.0.0.1 --port 8000 Restart=always EnvironmentFile=/sti/til/vinindkoeb/.env [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now vinindkoeb ``` Backend'en lytter kun på `127.0.0.1` — den skal ikke være direkte tilgængelig udefra, kun via reverse proxy'en i næste trin. ## 9. 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 port, fra trin 8), 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å `127.0.0.1:8000` (porten fra trin 8), 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 port. ## 10. DNS A-records (eller CNAME, alt efter opsætning) for hvert domæne fra trin 9, der peger på serverens IP-adresse. ## 11. Postal-webhook I Postal-instansens konfiguration: sæt webhook-URL'en til `https:///webhooks/postal` (fx `https://vinindkoeb.dk/webhooks/postal`) — det er sådan leveringsstatus (sendt/leveret/afvist) opdateres asynkront på `MailLog` (se opgave 7c). ## 12. 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 = ; ``` ## 13. Verifikation 1. `curl https:///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 6. 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`. ## 14. 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.