# Oppsett: Produksjonsserver (Hetzner VPS) **Filsti:** `docs/setup/produksjon.md` Denne oppskriften tar en fersk Ubuntu VPS fra null til en komplett Synops-installasjon. Hvert steg er sekvensielt — ikke hopp over noe. ## 0. Forutsetninger - Hetzner VPS med Ubuntu 24.04 LTS (8 vCPU, 16 GB RAM minimum) - DNS A-records som peker til VPS-ens IP: - `sidelinja.org` + `*.sidelinja.org` - `synops.no` + `*.synops.no` - `vegard.info` + `*.vegard.info` - DNS MX-records for epost-mottak (settes i Hetzner DNS Console): - `synops.no` → MX `mail.synops.no` (prioritet 10) - `sidelinja.org` → MX `mail.sidelinja.org` (prioritet 10) - `vegard.info` → MX `mail.vegard.info` (prioritet 10) - A-records for `mail.*` peker til samme IP (157.180.81.26) - SPF TXT-record på hvert domene: `v=spf1 include:_spf.brevo.com a mx ~all` - Brannmur: port 25/tcp åpen for innkommende SMTP - SSH-tilgang med nøkkelpar (passordautentisering deaktiveres i steg 1) ## 1. Grunnsikring av VPS ```bash # Oppdater systemet apt update && apt upgrade -y # Opprett tjenestebruker (ikke kjør alt som root) adduser sidelinja usermod -aG sudo sidelinja # Kopier SSH-nøkkel til ny bruker mkdir -p /home/sidelinja/.ssh cp ~/.ssh/authorized_keys /home/sidelinja/.ssh/ chown -R sidelinja:sidelinja /home/sidelinja/.ssh # Deaktiver passordautentisering og root-login sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config systemctl restart sshd # Brannmur: SSH, HTTP, HTTPS + LiveKit WebRTC ufw allow OpenSSH ufw allow 80/tcp ufw allow 443/tcp ufw allow 7881/tcp # LiveKit ICE/TCP fallback ufw allow 50000:50100/udp # LiveKit WebRTC media ufw enable ``` **Logg ut og logg inn som `sidelinja` fra nå av.** ## 2. Installer Docker ```bash # Docker Engine (offisiell repo) sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin # Kjør Docker uten sudo sudo usermod -aG docker sidelinja newgrp docker ``` ## 3. Opprett mappestruktur ```bash sudo mkdir -p /srv/synops/{config,data,media,logs} sudo mkdir -p /srv/synops/config/{caddy,authentik} sudo mkdir -p /srv/synops/data/{postgres,forgejo,authentik} sudo mkdir -p /srv/synops/data/whisper-models sudo mkdir -p /srv/synops/media/podcast sudo mkdir -p /srv/synops/logs/caddy sudo chown -R sidelinja:sidelinja /srv/synops ``` Resultat: ``` /srv/synops/ ├── docker-compose.yml ├── .env ├── config/ │ ├── caddy/Caddyfile │ └── authentik/ ├── data/ │ ├── postgres/ │ ├── forgejo/ │ ├── whisper-models/ │ └── authentik/ ├── media/ │ └── podcast/ └── logs/ └── caddy/ ``` ## 4. Miljøvariabler (.env) ```bash cat > /srv/synops/.env << 'EOF' # === Domener === DOMAIN_SIDELINJA=sidelinja.org DOMAIN_VEGARD=vegard.info DOMAIN_AUTH=auth.sidelinja.org COMPOSE_PROJECT_NAME=sidelinja # === PostgreSQL === POSTGRES_USER=sidelinja POSTGRES_PASSWORD= POSTGRES_DB=sidelinja # === Authentik === AUTHENTIK_SECRET_KEY= AUTHENTIK_POSTGRESQL_PASSWORD= # Authentik bruker sin egen database i samme PostgreSQL-instans AUTHENTIK_POSTGRESQL_HOST=postgres AUTHENTIK_POSTGRESQL_USER=authentik AUTHENTIK_POSTGRESQL_NAME=authentik # === Forgejo === FORGEJO_DB_PASSWD= # === LiveKit === LIVEKIT_API_KEY= LIVEKIT_API_SECRET= # === OpenRouter === OPENROUTER_API_KEY= # === Maskinrommet === AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/ AUTHENTIK_CLIENT_ID= AUTHENTIK_CLIENT_SECRET= # === Whisper (STT) === # Modell lastes ned automatisk ved oppstart. large-v3 gir best norsk kvalitet. # Ved GPU: bytt image til fedirz/faster-whisper-server:latest-cuda og WHISPER__COMPUTE_TYPE=float16 # === Intern === # Ingen porter eksponeres utenom 80/443. Alt rutes internt via Docker-nettverket. EOF chmod 600 /srv/synops/.env ``` ## 5. Tjeneste-installasjon (rekkefølge) Tjenestene startes i rekkefølge fordi noen avhenger av andre. Alle defineres i `docker-compose.yml`, men vi verifiserer hvert lag før vi går videre. ### Lag A: Fundament (ingen avhengigheter mellom seg) 1. **Docker-nettverk:** Opprett internt nettverk `sidelinja-net` 2. **PostgreSQL:** Start, opprett databaser for Authentik og Forgejo, verifiser (`pg_isready`) 3. **Caddy:** Start med Caddyfile for alle domener, verifiser at HTTPS fungerer 4. **Authentik:** Start, gjennomfør initial setup via `https://auth.sidelinja.org` 5. **Forgejo:** Start med Authentik som OAuth2-provider, opprett organisasjon og repo ### Lag B: Sanntid (krever nettverk) 6. **LiveKit:** Start, verifiser at WebRTC fungerer ### Lag C: Applikasjon (krever alt over) 8. **SvelteKit:** Bygg og start container, verifiser at frontenden laster 9. **Rust Workers:** Bygg og start container(e), verifiser at jobbkøen polles ## 6. docker-compose.yml (skjelett) ```yaml # Fullstendig docker-compose.yml bygges ut når tjenestene implementeres. # Denne seksjonen dokumenterer strukturen og viktige regler. # REGLER: # - Ingen "ports:" mot host UTENOM Caddy (80, 443) og LiveKit (UDP 50000-50100, TCP 7881) # - Alle tjenester på samme interne nettverk (sidelinja-net) # - Volumer bruker bind mounts til /srv/synops/ # - .env-filen lastes automatisk av Docker Compose # - RESSURSGRENSER: Worker-containere (Whisper) MÅ ha deploy.resources.limits # for å forhindre at de sultefôrer LiveKit og PostgreSQL. # Eksempel: workers: deploy: resources: limits: cpus: '4' memory: 8G networks: sidelinja-net: driver: bridge services: caddy: # Eneste tjeneste med eksponerte porter (80, 443) postgres: # data:/srv/synops/data/postgres authentik: # SSO for alle domener, på auth.sidelinja.org forgejo: # data:/srv/synops/data/forgejo, på git.sidelinja.org maskinrommet: # Rust/axum API, intern port 3100, proxyet via Caddy livekit: # Intern port, proxyet via Caddy sveltekit: # Intern port, proxyet via Caddy faster-whisper: # STT via OpenAI-kompatibelt API, intern port 8000 workers: # Rust job workers, ingen porter ``` ## 7. Caddy (Caddyfile grunnstruktur) ```caddyfile # === SSO === auth.sidelinja.org { reverse_proxy authentik-server:9000 } # === Sidelinja (hovedapplikasjon) === sidelinja.org { # LiveKit signaling (WebSocket upgrade) handle_path /livekit/* { reverse_proxy livekit:7880 } # Podcast media (statiske filer med byte-range support) handle_path /media/* { root * /srv/media file_server } # SvelteKit (frontend + SSR API) — aktiveres i fase 3 # reverse_proxy sveltekit:3000 } # === Maskinrommet API === api.sidelinja.org { reverse_proxy maskinrommet:3100 } # === Forgejo (Git) === git.sidelinja.org { reverse_proxy forgejo:3000 } # === Synops (plattformdomene) === # Subdomener (api.synops.no, auth.synops.no osv.) legges til individuelt # etter behov — HTTP-challenge fungerer per subdomain uten DNS-plugin. synops.no { respond "synops.no — plattform under utvikling" 200 } # === Vegard.info === vegard.info { respond "vegard.info — under construction" 200 } ``` **Merk:** Tjenester som ikke er deployet ennå er kommentert ut. Faktisk Caddyfile ligger i `config/caddy/Caddyfile` i repoet og synkes til `/srv/synops/config/caddy/Caddyfile` på serveren. Caddy bruker placeholders (`respond`) for tjenester som ikke er klare. Mediefiler i Caddy-containeren er montert som `/srv/synops/media:/srv/media:ro`. ## 8. PostgreSQL: Initielle databaser Ved første oppstart må det opprettes separate databaser og brukere for Authentik og Forgejo: ```sql -- Kjøres mot PostgreSQL etter første start -- (eller via init-script montert til /docker-entrypoint-initdb.d/) CREATE USER authentik WITH PASSWORD ''; CREATE DATABASE authentik OWNER authentik; CREATE USER forgejo WITH PASSWORD ''; CREATE DATABASE forgejo OWNER forgejo; ``` ## 9. Authentik: Initial konfigurasjon Etter oppstart, gå til `https://auth.sidelinja.org/if/flow/initial-setup/`: 1. Opprett admin-konto 2. Opprett OAuth2/OpenID Connect-provider for Forgejo 3. Opprett OAuth2/OpenID Connect-provider for SvelteKit (senere) 4. Konfigurer brukergrupper etter behov (redaksjon, admin) ## 10. Forgejo: Koble til Authentik Forgejo konfigureres med Authentik som OAuth2-kilde: - Authentication Source: OAuth2 - Provider: OpenID Connect - Discovery URL: `https://auth.sidelinja.org/application/o//.well-known/openid-configuration` - Etter oppsett: opprett organisasjon `sidelinja`, opprett repo `sidelinja` ## 11. Backup-strategi Se `docs/arkitektur.md` seksjon 2.2 for full dataklassifisering. Kun kategori 1 (kritisk) og Forgejo-data backupes. ### 11.1 PostgreSQL (daglig dump, 03:00) ```bash # pg_dump er konsistent selv under last — ingen nedetid docker compose exec -T postgres pg_dump -U sidelinja -Fc sidelinja \ > /srv/synops/backup/pg/sidelinja_$(date +%Y%m%d).dump # Behold 30 dager, slett eldre find /srv/synops/backup/pg/ -name "*.dump" -mtime +30 -delete ``` ### 11.1b PostgreSQL WAL-arkivering (kontinuerlig, PITR) Daglig dump gir opptil 24 timers datatap. WAL-arkivering muliggjør Point-In-Time Recovery til minuttet. ```bash # Installer pgBackRest (i PostgreSQL Docker-containeren eller som sidecar) # Alternativt: WAL-G for enklere S3-oppsett # postgresql.conf (legg til i Docker-volumet eller via environment) archive_mode = on archive_command = 'pgbackrest --stanza=sidelinja archive-push %p' wal_level = replica # pgbackrest.conf [sidelinja] pg1-path=/var/lib/postgresql/data [global] repo1-type=s3 repo1-s3-bucket=sidelinja-backup repo1-s3-endpoint=fsn1.your-objectstorage.com repo1-s3-region=fsn1 repo1-path=/pgbackrest repo1-retention-full=4 repo1-retention-diff=14 # Ukentlig full backup (søndag kl. 02:00) # 0 2 * * 0 sidelinja pgbackrest --stanza=sidelinja --type=full backup # Daglig differensiell (man-lør kl. 02:00) # 0 2 * * 1-6 sidelinja pgbackrest --stanza=sidelinja --type=diff backup # Recovery-eksempel (gjenopprett til spesifikt tidspunkt): # pgbackrest --stanza=sidelinja --target="2026-03-15 13:59:00" \ # --target-action=promote restore ``` **Merk:** WAL-arkivering erstatter IKKE daglig pg_dump — dumpen er en enkel, portabel backup som fungerer uavhengig av pgBackRest. WAL-arkivering er et tillegg for finkornet recovery. ### 11.2 Media-filer (daglig, 03:30) ```bash # Inkrementell med rsync til lokal backup-disk eller ekstern lagring rsync -a --delete /srv/synops/media/ /srv/synops/backup/media/ ``` ### 11.3 Forgejo-data (daglig, 04:00) ```bash # Forgejo-repos kan gjenskapes, men det er tidkrevende. # Sikkerhetsnett-backup av hele data-mappen: rsync -a --delete /srv/synops/data/forgejo/ /srv/synops/backup/forgejo/ ``` ### 11.4 Hemmeligheter (.env) ```bash # Manuell kopi ved endring — ALDRI i Git cp /srv/synops/.env /srv/synops/backup/env_$(date +%Y%m%d) chmod 600 /srv/synops/backup/env_* ``` ### 11.5 Off-site backup (rclone → Hetzner Object Storage) Lokal backup beskytter kun mot logiske feil. Ved fysisk nodefeil tapes alt. Kategori 1-data pushes daglig til Hetzner Object Storage via `rclone`. ```bash # Installer og konfigurer rclone curl https://rclone.org/install.sh | sudo bash rclone config # Opprett remote "hetzner-s3" med Hetzner Object Storage credentials # (S3-kompatibelt, endpoint: fsn1.your-objectstorage.com eller nbg1) # /srv/synops/scripts/backup-offsite.sh #!/bin/bash set -euo pipefail BUCKET="s3:hetzner-s3/sidelinja-backup" # PG-dump (siste lokale dump) LATEST_DUMP=$(ls -t /srv/synops/backup/pg/*.dump 2>/dev/null | head -1) if [ -n "$LATEST_DUMP" ]; then rclone copy "$LATEST_DUMP" "$BUCKET/pg/" fi # Media (inkrementell sync) rclone sync /srv/synops/media/ "$BUCKET/media/" --transfers 4 # Behold 90 dager PG-dumper off-site rclone delete "$BUCKET/pg/" --min-age 90d echo "$(date): Off-site backup ferdig" >> /srv/synops/logs/backup-offsite.log ``` ### 11.6 Cron-oppsett ```bash # /etc/cron.d/sidelinja-backup 0 3 * * * sidelinja /srv/synops/scripts/backup-pg.sh 30 3 * * * sidelinja /srv/synops/scripts/backup-media.sh 0 4 * * * sidelinja /srv/synops/scripts/backup-forgejo.sh 30 4 * * * sidelinja /srv/synops/scripts/backup-offsite.sh ``` ### 11.7 Hva som IKKE backupes (bevisst) - **Redis** — cache, regenereres automatisk - **Caddy-data** — sertifikater regenereres av Let's Encrypt - **Avledede data i PG** (ren tekst, segmenter, søkeindeks) — regenereres fra Git - **Logger** — rulleres med logrotate, arkiveres separat ved behov - **Whisper-modeller** — re-download fra HuggingFace ### 11.8 Restore-prosedyre ```bash # 1. PostgreSQL docker compose exec -T postgres pg_restore -U sidelinja -d sidelinja --clean \ < /srv/synops/backup/pg/sidelinja_YYYYMMDD.dump # 2. Media rsync -a /srv/synops/backup/media/ /srv/synops/media/ # 3. Forgejo docker compose down forgejo rsync -a /srv/synops/backup/forgejo/ /srv/synops/data/forgejo/ docker compose up -d forgejo # 4. Avledede data: trigges automatisk ved webhook eller manuelt # Rust-worker reimporterer alle SRT-filer fra Git til PG ``` ## 12. Deploy-workflow (etter initial setup) All utvikling og deploy skjer direkte på serveren. Claude Code kjører i `/home/vegard/synops/` og har direkte tilgang til Docker og alle tjenester. ```bash # Commit og push til Forgejo cd /home/vegard/synops git add && git commit -m "beskrivelse" git push forgejo main # Bygg og deploy tjeneste (krever godkjenning fra Vegard) cd /srv/synops docker compose build --no-cache docker compose up -d ``` ### Maskinrommet (Rust API) Maskinrommet kjører **native på hosten** som systemd-tjeneste (ikke i Docker). Dette gir tilgang til `claude` CLI for agent-chat og hele hostens verktøy. ```bash # Bygg cd /home/vegard/synops/maskinrommet && cargo build --release # Start/restart sudo systemctl restart maskinrommet # Logger sudo journalctl -u maskinrommet -f ``` Env-filen (`/tmp/maskinrommet.env`) genereres automatisk av `scripts/maskinrommet-env.sh` med Docker container-IPs for PG. Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`. Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes ikke i produksjon. ## 13. Postfix (innkommende epost) Postfix kjører som lokal MTA **kun for mottak**. Ingen relay, ingen utgående kø — utgående epost sendes via msmtp/Brevo (se synops-mail --send). ### Installasjon ```bash sudo DEBIAN_FRONTEND=noninteractive apt-get install -y postfix ``` ### Konfigurasjon **`/etc/postfix/main.cf`** — nøkkelinnstillinger: - `myhostname = mail.synops.no` - `virtual_mailbox_domains = synops.no, sidelinja.org, vegard.info` - `virtual_transport = synops-pipe:` — all epost pipes til synops-mail - `mydestination =` — tom, all levering via virtual - `default_transport = error:outbound mail is disabled` — blokkerer utgående - `relayhost =` — ingen relay **`/etc/postfix/virtual_mailbox`** — catch-all for alle domener: ``` @synops.no OK @sidelinja.org OK @vegard.info OK ``` Etter endring: `sudo postmap /etc/postfix/virtual_mailbox` **`/etc/postfix/master.cf`** — pipe transport (lagt til på slutten): ``` synops-pipe unix - n n - - pipe flags=DRhu user=vegard argv=/usr/local/bin/synops-mail --receive --recipient ${recipient} --sender ${sender} ``` ### Drift ```bash # Status sudo postfix status # Restart etter konfig-endring sudo systemctl restart postfix # Se epost-logg sudo tail -f /var/log/mail.log # Sjekk kø (bør alltid være tom) sudo postqueue -p ``` ### Arkitektur ``` Internett → port 25 → Postfix (smtpd) → virtual_transport = synops-pipe → pipe: synops-mail --receive --recipient --sender → (leser rå epost fra stdin, prosesserer, oppretter node) ``` Postfix eier kun transport — all forretningslogikk (brukeroppslag, validering, node-opprettelse) ligger i synops-mail. ## 14. Verifisering etter oppsett ### Lag A (minimum fungerende server) - [ ] `https://auth.sidelinja.org` viser Authentik login - [ ] `https://git.sidelinja.org` viser Forgejo, innlogging via Authentik fungerer - [ ] PostgreSQL: `docker compose exec postgres pg_isready` returnerer OK - [ ] Git push til Forgejo fungerer ### Lag B-C - [x] `https://api.sidelinja.org/health` returnerer `{"status":"ok"}` med PG tilkoblet (verifisert 2026-03-17) - [x] `https://api.sidelinja.org/me` returnerer 401 uten token (verifisert 2026-03-17) - [x] `https://sidelinja.org` laster SvelteKit-appen (deployet 2025-03-15) - [x] `https://sidelinja.org/api/health` returnerer 200 - [x] Authentik OIDC-innlogging fungerer fra nettleser (verifisert 2025-03-15) - [x] Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15) - [ ] `https://synops.no` viser placeholder - [ ] `https://vegard.info` svarer - [x] LiveKit: Container kjører, signaling proxyet via Caddy (verifisert 2026-03-17) - [ ] Media: `curl -I https://sidelinja.org/media/podcast/test.mp3` returnerer `Accept-Ranges: bytes`