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>
This commit is contained in:
parent
6b5680af07
commit
d3e6b92d9f
1 changed files with 62 additions and 29 deletions
91
INSTALL.md
91
INSTALL.md
|
|
@ -10,15 +10,39 @@ Postal-instans (selvhostet mailserver — se CLAUDE.md's "Tech stack")
|
||||||
med DKIM sat op for det domæne der sendes mails fra. Selve
|
med DKIM sat op for det domæne der sendes mails fra. Selve
|
||||||
Postal-opsætningen er uden for denne vejlednings omfang.
|
Postal-opsætningen er uden for denne vejlednings omfang.
|
||||||
|
|
||||||
## 1. Forudsætninger på serveren
|
**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`)
|
- Python 3.13 (se `.python-version`)
|
||||||
- [`uv`](https://docs.astral.sh/uv/) (Python-pakkehåndtering)
|
- [`uv`](https://docs.astral.sh/uv/) (Python-pakkehåndtering)
|
||||||
- Node.js + npm (til at bygge admin-UI'et, `admin-ui/`)
|
- Node.js + npm (til at bygge admin-UI'et, `admin-ui/`)
|
||||||
- PostgreSQL
|
- PostgreSQL (`apt install postgresql`, da databasen kører i samme
|
||||||
|
container som appen)
|
||||||
- git
|
- git
|
||||||
|
|
||||||
## 2. Hent koden
|
## 3. Hent koden
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url> /sti/til/vinindkoeb
|
git clone <repo-url> /sti/til/vinindkoeb
|
||||||
|
|
@ -26,7 +50,7 @@ cd /sti/til/vinindkoeb
|
||||||
uv sync
|
uv sync
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Database
|
## 4. Database
|
||||||
|
|
||||||
Opret en Postgres-bruger og -database der matcher formatet i
|
Opret en Postgres-bruger og -database der matcher formatet i
|
||||||
`DATABASE_URL` (`postgresql+psycopg://<bruger>:<kodeord>@<host>:5432/<database>`):
|
`DATABASE_URL` (`postgresql+psycopg://<bruger>:<kodeord>@<host>:5432/<database>`):
|
||||||
|
|
@ -36,7 +60,7 @@ CREATE USER vinindkoeb WITH PASSWORD '...';
|
||||||
CREATE DATABASE vinindkoeb OWNER vinindkoeb;
|
CREATE DATABASE vinindkoeb OWNER vinindkoeb;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Miljøvariabler
|
## 5. Miljøvariabler
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
@ -46,7 +70,7 @@ Udfyld hvert felt i `.env`:
|
||||||
|
|
||||||
| Variabel | Hvor den kommer fra |
|
| Variabel | Hvor den kommer fra |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DATABASE_URL` | Fra trin 3 |
|
| `DATABASE_URL` | Fra trin 4 (`@localhost`, da Postgres kører i samme container) |
|
||||||
| `ENVIRONMENT` | `production` |
|
| `ENVIRONMENT` | `production` |
|
||||||
| `SECRET_KEY` | Generér med `openssl rand -hex 32` |
|
| `SECRET_KEY` | Generér med `openssl rand -hex 32` |
|
||||||
| `ACCESS_TOKEN_EXPIRE_MINUTES` / `ELEVATION_EXPIRE_MINUTES` | Standardværdierne er fine, med mindre andet ønskes |
|
| `ACCESS_TOKEN_EXPIRE_MINUTES` / `ELEVATION_EXPIRE_MINUTES` | Standardværdierne er fine, med mindre andet ønskes |
|
||||||
|
|
@ -54,7 +78,7 @@ Udfyld hvert felt i `.env`:
|
||||||
| `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=` |
|
| `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` |
|
| `ADMIN_DOMAIN` | Domænet admin-UI'et skal serveres på, fx `admin.vinindkoeb.dk` |
|
||||||
|
|
||||||
## 5. Migrations
|
## 6. Migrations
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run alembic upgrade head
|
uv run alembic upgrade head
|
||||||
|
|
@ -63,7 +87,7 @@ uv run alembic upgrade head
|
||||||
Dette opretter alle tabeller og seeder `WineCategory` automatisk
|
Dette opretter alle tabeller og seeder `WineCategory` automatisk
|
||||||
(vinkategorierne er globale, delt af alle organisationer/ruter).
|
(vinkategorierne er globale, delt af alle organisationer/ruter).
|
||||||
|
|
||||||
## 6. Første organisation, rute og admin-bruger
|
## 7. Første organisation, rute og admin-bruger
|
||||||
|
|
||||||
Der findes ingen selvbetjenings-registrering — uden en bruger i
|
Der findes ingen selvbetjenings-registrering — uden en bruger i
|
||||||
databasen kan ingen logge ind. Brug bootstrap-scriptet:
|
databasen kan ingen logge ind. Brug bootstrap-scriptet:
|
||||||
|
|
@ -81,7 +105,7 @@ Scriptet genbruger en organisation/rute med samme navn, hvis den
|
||||||
allerede findes — sikkert at køre igen for at oprette endnu en
|
allerede findes — sikkert at køre igen for at oprette endnu en
|
||||||
admin-bruger på en eksisterende organisation.
|
admin-bruger på en eksisterende organisation.
|
||||||
|
|
||||||
## 7. Byg admin-UI'et
|
## 8. Byg admin-UI'et
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd admin-ui
|
cd admin-ui
|
||||||
|
|
@ -94,10 +118,9 @@ Uden dette trin kører API'et og den offentlige side fint, men
|
||||||
`ADMIN_DOMAIN` viser ingenting (se `app/admin_site.py`s
|
`ADMIN_DOMAIN` viser ingenting (se `app/admin_site.py`s
|
||||||
sikkerhedsnet).
|
sikkerhedsnet).
|
||||||
|
|
||||||
## 8. Kør backend'en som en vedvarende service
|
## 9. Kør backend'en som en vedvarende service
|
||||||
|
|
||||||
Eksempel på en `systemd`-unit (`/etc/systemd/system/vinindkoeb.service`),
|
Eksempel på en `systemd`-unit (`/etc/systemd/system/vinindkoeb.service`):
|
||||||
uafhængig af hvilken reverse proxy der bruges foran den:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
|
|
@ -108,7 +131,7 @@ After=network.target postgresql.service
|
||||||
Type=simple
|
Type=simple
|
||||||
User=vinindkoeb
|
User=vinindkoeb
|
||||||
WorkingDirectory=/sti/til/vinindkoeb
|
WorkingDirectory=/sti/til/vinindkoeb
|
||||||
ExecStart=/sti/til/vinindkoeb/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
|
ExecStart=/sti/til/vinindkoeb/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
Restart=always
|
Restart=always
|
||||||
EnvironmentFile=/sti/til/vinindkoeb/.env
|
EnvironmentFile=/sti/til/vinindkoeb/.env
|
||||||
|
|
||||||
|
|
@ -121,33 +144,43 @@ sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now vinindkoeb
|
sudo systemctl enable --now vinindkoeb
|
||||||
```
|
```
|
||||||
|
|
||||||
Backend'en lytter kun på `127.0.0.1` — den skal ikke være direkte
|
Da reverse proxy'en (CloudPanel) kører i en anden container, skal
|
||||||
tilgængelig udefra, kun via reverse proxy'en i næste trin.
|
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.
|
||||||
|
|
||||||
## 9. Domæner + reverse proxy
|
## 10. Domæner + reverse proxy
|
||||||
|
|
||||||
Løsningen afgør selv hvilken rute/hvilket indhold der skal vises ud
|
Løsningen afgør selv hvilken rute/hvilket indhold der skal vises ud
|
||||||
fra requestens `Host`-header (se CLAUDE.md's "Route" og
|
fra requestens `Host`-header (se CLAUDE.md's "Route" og
|
||||||
"Admin-UI-arkitektur") — det betyder at **alle** domæner nedenfor
|
"Admin-UI-arkitektur") — det betyder at **alle** domæner nedenfor
|
||||||
bare skal pege på samme kørende backend (samme port, fra trin 8),
|
bare skal pege på samme kørende backend (samme container-IP og port,
|
||||||
uanset hvilken reverse proxy-løsning der bruges:
|
fra trin 9), uanset hvilken reverse proxy-løsning der bruges:
|
||||||
|
|
||||||
- Organisationens/rutens `public_domain`-felt(er) i databasen, fx
|
- Organisationens/rutens `public_domain`-felt(er) i databasen, fx
|
||||||
`vinindkoeb.dk` og `test.vinindkoeb.dk`
|
`vinindkoeb.dk` og `test.vinindkoeb.dk`
|
||||||
- `ADMIN_DOMAIN` fra `.env`, fx `admin.vinindkoeb.dk`
|
- `ADMIN_DOMAIN` fra `.env`, fx `admin.vinindkoeb.dk`
|
||||||
|
|
||||||
**Med CloudPanel:** opret ét "Reverse Proxy"-site pr. domæne, der
|
**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
|
peger på `<vinindkoeb-containerens-ip>:8000` (IP'en fra trin 1, porten
|
||||||
Let's Encrypt-certifikat for hvert site. Bruges en anden løsning
|
fra trin 9), og aktivér Let's Encrypt-certifikat for hvert site.
|
||||||
(nginx, Caddy, Traefik, ...), er princippet det samme: én
|
Bruges en anden løsning (nginx, Caddy, Traefik, ...), er princippet
|
||||||
vhost/site pr. domæne, alle proxyet til samme port.
|
det samme: én vhost/site pr. domæne, alle proxyet til samme
|
||||||
|
container-IP og port.
|
||||||
|
|
||||||
## 10. DNS
|
## 11. DNS
|
||||||
|
|
||||||
A-records (eller CNAME, alt efter opsætning) for hvert domæne fra
|
A-records (eller CNAME, alt efter opsætning) for hvert domæne fra
|
||||||
trin 9, der peger på serverens IP-adresse.
|
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).
|
||||||
|
|
||||||
## 11. Postal-webhook
|
## 12. Postal-webhook
|
||||||
|
|
||||||
I Postal-instansens konfiguration: sæt webhook-URL'en til
|
I Postal-instansens konfiguration: sæt webhook-URL'en til
|
||||||
`https://<det rigtige domæne>/webhooks/postal` (fx
|
`https://<det rigtige domæne>/webhooks/postal` (fx
|
||||||
|
|
@ -155,7 +188,7 @@ I Postal-instansens konfiguration: sæt webhook-URL'en til
|
||||||
leveringsstatus (sendt/leveret/afvist) opdateres asynkront på
|
leveringsstatus (sendt/leveret/afvist) opdateres asynkront på
|
||||||
`MailLog` (se opgave 7c).
|
`MailLog` (se opgave 7c).
|
||||||
|
|
||||||
## 12. Sæt rutens felter
|
## 13. Sæt rutens felter
|
||||||
|
|
||||||
For hver rute (den rigtige, og evt. en test-rute) skal disse 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
|
sættes direkte i databasen — der findes ingen rute-indstillings-skærm
|
||||||
|
|
@ -169,16 +202,16 @@ UPDATE route SET
|
||||||
WHERE id = <rute-id>;
|
WHERE id = <rute-id>;
|
||||||
```
|
```
|
||||||
|
|
||||||
## 13. Verifikation
|
## 14. Verifikation
|
||||||
|
|
||||||
1. `curl https://<rigtigt domæne>/health` → `{"status":"ok","database":"ok"}`.
|
1. `curl https://<rigtigt domæne>/health` → `{"status":"ok","database":"ok"}`.
|
||||||
2. Besøg det rigtige domæne i en browser → bestillings-/tilmeldingssiden.
|
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.
|
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
|
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
|
sat op) og bekræft at en rigtig mail bliver sendt og logget i
|
||||||
`MailLog`.
|
`MailLog`.
|
||||||
|
|
||||||
## 14. Fremtidige opdateringer
|
## 15. Fremtidige opdateringer
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue