synops/docs/setup/produksjon.md
vegard 00bf5d27ce Arkitekturbeslutninger: noder er sentrum, edges definerer alt
Grunnleggende arkitekturbeslutninger tatt og dokumentert:

- Alt er noder (brukere, team, innhold, mediefiler, samlings-noder)
- Edges definerer hva en node er (freeform typer, metadata i JSONB)
- Materialisert tilgangsmatrise (node_access) erstatter workspace-RLS
- Visibility (hidden/discoverable/readable/open) på noder
- Aliaser via usynlige system-edges
- Maskinrommet eier all skriving (SpacetimeDB først, PG asynk)
- SpacetimeDB holder hele grafen, PG er persistent backup
- Node- og edge-skjema spesifisert (docs/primitiver/)

Fjernet workspace-konseptet fra hele dokumentasjonen (~40 filer).
Fem retninger besluttet, én åpen (rom, ikke forum).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:29:54 +01:00

14 KiB

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
    • vegard.info + *.vegard.info
  • SSH-tilgang med nøkkelpar (passordautentisering deaktiveres i steg 1)

1. Grunnsikring av VPS

# 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: kun SSH, HTTP, HTTPS
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Logg ut og logg inn som sidelinja fra nå av.

2. Installer Docker

# 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

sudo mkdir -p /srv/synops/{config,data,media,logs}
sudo mkdir -p /srv/synops/config/{caddy,authentik}
sudo mkdir -p /srv/synops/data/{postgres,spacetimedb,forgejo,authentik}
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/
│   ├── spacetimedb/
│   ├── forgejo/
│   └── authentik/
├── media/
│   └── podcast/
└── logs/
    └── caddy/

4. Miljøvariabler (.env)

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=<generer med: openssl rand -hex 32>
POSTGRES_DB=sidelinja

# === Authentik ===
AUTHENTIK_SECRET_KEY=<generer med: openssl rand -hex 64>
AUTHENTIK_POSTGRESQL_PASSWORD=<generer med: openssl rand -hex 32>
# 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=<generer med: openssl rand -hex 32>

# === LiveKit ===
LIVEKIT_API_KEY=<generer>
LIVEKIT_API_SECRET=<generer med: openssl rand -hex 32>

# === OpenRouter ===
OPENROUTER_API_KEY=<fra openrouter.ai>

# === 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)

  1. SpacetimeDB: Start, verifiser tilkobling
  2. LiveKit: Start, verifiser at WebRTC fungerer

Lag C: Applikasjon (krever alt over)

  1. SvelteKit: Bygg og start container, verifiser at frontenden laster
  2. Rust Workers: Bygg og start container(e), verifiser at jobbkøen polles

6. docker-compose.yml (skjelett)

# 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)
# - 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
  spacetimedb:  # data:/srv/synops/data/spacetimedb
  livekit:      # Intern port, proxyet via Caddy
  sveltekit:    # Intern port, proxyet via Caddy
  workers:      # Rust job workers, ingen porter

7. Caddy (Caddyfile grunnstruktur)

# === SSO (felles for alle domener) ===
auth.sidelinja.org {
    reverse_proxy authentik:9000
}

# === Sidelinja (hovedapplikasjon) ===
sidelinja.org {
    # SvelteKit (frontend + API)
    reverse_proxy sveltekit:3000

    # LiveKit (WebSocket upgrade)
    handle_path /livekit/* {
        reverse_proxy livekit:7880
    }

    # SpacetimeDB (WebSocket)
    handle_path /spacetime/* {
        reverse_proxy spacetimedb:3000
    }

    # Podcast media (statiske filer med byte-range support)
    handle_path /media/* {
        root * /srv/synops/media
        file_server
    }

    # Podcast access log (kun media-forespørsler)
    log {
        output file /srv/synops/logs/caddy/podcast_access.log
        format json
    }
}

# === Forgejo (Git) ===
git.sidelinja.org {
    reverse_proxy forgejo:3000
}

# === Vegard.info ===
vegard.info {
    # Konfigureres når innhold er klart
    respond "Under construction" 200
}

8. PostgreSQL: Initielle databaser

Ved første oppstart må det opprettes separate databaser og brukere for Authentik og Forgejo:

-- Kjøres mot PostgreSQL etter første start
-- (eller via init-script montert til /docker-entrypoint-initdb.d/)

CREATE USER authentik WITH PASSWORD '<AUTHENTIK_POSTGRESQL_PASSWORD>';
CREATE DATABASE authentik OWNER authentik;

CREATE USER forgejo WITH PASSWORD '<FORGEJO_DB_PASSWD>';
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/<slug>/.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)

# 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.

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

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

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

# 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.

# 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

# /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
  • SpacetimeDB — sanntidsdata synkes til PG, in-memory state er flyktig

11.8 Restore-prosedyre

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

Etter at serveren er satt opp, er dette den daglige deploy-flyten:

# Fra lokal maskin (WSL2):
git push forgejo main

# SSH inn til server:
ssh sidelinja@<server-ip>
cd /srv/synops
git pull
docker compose build --no-cache <tjeneste>
docker compose up -d <tjeneste>

13. 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
  • SSH-push fra lokal WSL2 til Forgejo fungerer

Lag B-C

  • https://sidelinja.org laster SvelteKit-appen (deployet 2025-03-15)
  • https://sidelinja.org/api/health returnerer 200
  • Authentik OIDC-innlogging fungerer fra nettleser (verifisert 2025-03-15)
  • Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15)
  • https://vegard.info svarer
  • SpacetimeDB: WebSocket-tilkobling fra nettleser fungerer
  • LiveKit: Test-rom med video/lyd fungerer
  • Media: curl -I https://sidelinja.org/media/podcast/test.mp3 returnerer Accept-Ranges: bytes