INSTALL.md dækker hele vejen fra en frisk server til en kørende løsning: forudsætninger, database, .env, migrations, admin-UI-build, en systemd-unit til backend'en, domæne-/reverse proxy-krav (holdt værktøjs-uafhængigt, med en kort CloudPanel-note), DNS, Postal-webhook, rute-opsætning, verifikation og fremtidige opdateringer. scripts/create_admin_user.py lukker en reel mangel: der findes ingen selvbetjenings-registrering, så uden en bruger i databasen kan ingen logge ind på en frisk installation. Scriptet opretter (eller genbruger) en organisation + rute, samt en ny superadmin-bruger med korrekt hashet adgangskode — samme mønster som det eksisterende scripts/import_participants.py. Verificeret i en isoleret test (oprettet bruger kunne logge ind via POST /auth/login), testdata ryddet op igen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
5.7 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.
1. Forudsætninger på serveren
- Python 3.13 (se
.python-version) uv(Python-pakkehåndtering)- Node.js + npm (til at bygge admin-UI'et,
admin-ui/) - PostgreSQL
- git
2. Hent koden
git clone <repo-url> /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://<bruger>:<kodeord>@<host>:5432/<database>):
CREATE USER vinindkoeb WITH PASSWORD '...';
CREATE DATABASE vinindkoeb OWNER vinindkoeb;
4. Miljøvariabler
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 <dkim-selector>._domainkey.<domæne>, brug værdien af p= |
ADMIN_DOMAIN |
Domænet admin-UI'et skal serveres på, fx admin.vinindkoeb.dk |
5. Migrations
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:
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
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).
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:
[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
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, fxvinindkoeb.dkogtest.vinindkoeb.dk ADMIN_DOMAINfra.env, fxadmin.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://<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).
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:
UPDATE route SET
public_domain = 'vinindkoeb.dk',
sender_name = 'Afsendernavn',
sender_email = 'afsender@vinindkoeb.dk'
WHERE id = <rute-id>;
13. Verifikation
curl https://<rigtigt domæne>/health→{"status":"ok","database":"ok"}.- Besøg det rigtige domæne i en browser → bestillings-/tilmeldingssiden.
- Besøg
ADMIN_DOMAIN→ login-siden, log ind med brugeren fra trin 6. - 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
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.