Port 25/tcp åpnet i UFW for innkommende SMTP (forutsetning for epost-mottak). Dokumentert nøyaktige MX/A/SPF-records som trengs i docs/setup/produksjon.md. Selve DNS-endringene må gjøres manuelt i Hetzner DNS Console (dns.hetzner.com) — Claude har ikke browser eller API-token. Detaljerte instruksjoner i tasks.md.
16 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.orgsynops.no+*.synops.novegard.info+*.vegard.info
- DNS MX-records for epost-mottak (settes i Hetzner DNS Console):
synops.no→ MXmail.synops.no(prioritet 10)sidelinja.org→ MXmail.sidelinja.org(prioritet 10)vegard.info→ MXmail.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
# 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
# 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,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)
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>
# === Maskinrommet ===
AUTHENTIK_ISSUER=https://auth.sidelinja.org/application/o/sidelinja/
AUTHENTIK_CLIENT_ID=<fra Authentik OIDC-provider>
AUTHENTIK_CLIENT_SECRET=<fra Authentik OIDC-provider>
# === 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)
- Docker-nettverk: Opprett internt nettverk
sidelinja-net - PostgreSQL: Start, opprett databaser for Authentik og Forgejo, verifiser (
pg_isready) - Caddy: Start med Caddyfile for alle domener, verifiser at HTTPS fungerer
- Authentik: Start, gjennomfør initial setup via
https://auth.sidelinja.org - Forgejo: Start med Authentik som OAuth2-provider, opprett organisasjon og repo
Lag B: Sanntid (krever nettverk)
- LiveKit: Start, verifiser at WebRTC fungerer
Lag C: Applikasjon (krever alt over)
- SvelteKit: Bygg og start container, verifiser at frontenden laster
- 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) 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)
# === 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:
-- 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/:
- Opprett admin-konto
- Opprett OAuth2/OpenID Connect-provider for Forgejo
- Opprett OAuth2/OpenID Connect-provider for SvelteKit (senere)
- 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 reposidelinja
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
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)
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.
# Commit og push til Forgejo
cd /home/vegard/synops
git add <filer> && git commit -m "beskrivelse"
git push forgejo main
# Bygg og deploy tjeneste (krever godkjenning fra Vegard)
cd /srv/synops
docker compose build --no-cache <tjeneste>
docker compose up -d <tjeneste>
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.
# 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. Verifisering etter oppsett
Lag A (minimum fungerende server)
https://auth.sidelinja.orgviser Authentik loginhttps://git.sidelinja.orgviser Forgejo, innlogging via Authentik fungerer- PostgreSQL:
docker compose exec postgres pg_isreadyreturnerer OK - Git push til Forgejo fungerer
Lag B-C
https://api.sidelinja.org/healthreturnerer{"status":"ok"}med PG tilkoblet (verifisert 2026-03-17)https://api.sidelinja.org/mereturnerer 401 uten token (verifisert 2026-03-17)https://sidelinja.orglaster SvelteKit-appen (deployet 2025-03-15)https://sidelinja.org/api/healthreturnerer 200- Authentik OIDC-innlogging fungerer fra nettleser (verifisert 2025-03-15)
- Chat: meldinger sendes og vises med riktig brukernavn (verifisert 2025-03-15)
https://synops.noviser placeholderhttps://vegard.infosvarer- LiveKit: Container kjører, signaling proxyet via Caddy (verifisert 2026-03-17)
- Media:
curl -I https://sidelinja.org/media/podcast/test.mp3returnererAccept-Ranges: bytes