Files
Archivum/ARCHITECTURE.md
Bjorn Blomberg cd588197b9
All checks were successful
build-and-push / build (push) Successful in 15m15s
feat: Authentik OIDC SSO, allow/deny RBAC, admin console & Gitea CI
Authentication & RBAC
- Add confidential OIDC client (Authentik) with /auth/oidc/login +
  /auth/oidc/callback: discovery, code exchange, id_token verify (go-oidc),
  groups claim → role (Archivum-admin → admin, else user). Sessions carry groups.
- Rework ACL into an allow/deny model (new `effect` column + migration).
  db.EffectiveAccess resolves user + all groups over the path and its ancestors:
  default deny, explicit deny always beats allow.
- Enforce ACL for ALL non-admin users (not just guest) across list/read/save/
  delete/move/create/history/diff/images/upload. Admins bypass.
- Seed built-in Archivum-admin / Archivum-reader groups; login allow-list on
  users & groups; public (guest) user access is ACL-configurable.

Admin API & UI
- New GraphQL ops: oidcConfig/updateOidcConfig, group CRUD, membership,
  setUserRole/setUserLogin/setGroupLogin, userGroups, loginOptions.
- Rebuilt AdminView: SSO config, user/group management + membership, login
  toggles, and an allow/deny access-control matrix per path.
- LoginView: "Sign in with Authentik" + public-user option; OIDC callback route.

Rendering/editor
- Fix bug where inline marks (bold/italic/code/strike/link) were dropped on
  TipTap→AsciiDoc save. Add RENDERING_IMPROVEMENTS.md with proposals.

CI / build
- .gitea/workflows/build.yaml: build on the Pi5 runner, push
  localhost:5000/archivum:{latest,<sha>}. Add .dockerignore; bump Go image to 1.25.
- Docs: ARCHITECTURE.md, README.md, docs/AUTHENTIK_SETUP.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:58:50 +02:00

580 lines
24 KiB
Markdown

# Archivum — Arkitektur och designdokumentation
## Innehållsförteckning
1. [Systemöversikt](#1-systemöversikt)
2. [Frontend](#2-frontend)
3. [Backend](#3-backend)
4. [Exekveringsflöden](#4-exekveringsflöden)
5. [Kodstruktur](#5-kodstruktur)
6. [Containerisering och driftsättning](#6-containerisering-och-driftsättning)
---
## 1. Systemöversikt
Archivum är en självhostad wiki/dokumenthanterare. Alla dokument lagras som AsciiDoc-filer (`.adoc`) på disk och versionshanteras automatiskt med Git. Frontenden är en SPA (Single-Page Application) som kommunicerar med backenden uteslutande via GraphQL.
```
Webbläsare
│ HTTPS / HTTP
┌──────────────────────────────┐
│ Go HTTP-server :4000 │
│ ┌──────────┐ ┌──────────┐ │
│ │ /graphql │ │ SPA / │ │
│ └────┬─────┘ └──────────┘ │
│ │ │
│ ┌────▼──────────────────┐ │
│ │ GraphQL dispatcher │ │
│ └─┬──────┬──────┬───────┘ │
│ Auth Storage Git │
│ (SQLite)(disk) (os/exec) │
└──────────────────────────────┘
```
### Teknologistack
| Lager | Teknik |
|--------------|----------------------------------------------------------------|
| Frontend | Vue 3 (Composition API), Vite, TypeScript, Tailwind CSS |
| State | Pinia |
| Routing | Vue Router 4 |
| Editor | TipTap 2 (visuell, med custom image extension), CodeMirror 6 (AsciiDoc-källa) |
| AsciiDoc | Asciidoctor.js (rendering i webbläsaren) |
| PWA | vite-plugin-pwa (Service Worker, manifest) |
| Backend | Go 1.22, HTTP-standardbibliotek |
| Databas | SQLite via modernc.org/sqlite (CGO-fri, multi-arch) |
| Autentisering| OIDC/SSO (Authentik) + lokala konton (bcrypt) + LDAP, sessions-tokens i minnet |
| RBAC / ACL | Roll via OIDC-grupper; allow/deny-ACL per sökväg för användare & grupper |
| Versionshantering | Git via `os/exec` |
| Containerisering | Docker (multi-stage, multi-arch: amd64 + arm64) |
---
## 2. Frontend
### Paket och beroenden
```
frontend/
├── src/
│ ├── App.vue # Rotemponent — kontrollerar setup/login/app-läge
│ ├── main.ts # Appstart, monterar Vue + Pinia + Router
│ ├── lib/
│ │ ├── gql.ts # Minimal GraphQL-klient (fetch-baserad)
│ │ └── crypto.ts # Lösenords-hashing (SHA-256 via Web Crypto API)
│ ├── stores/
│ │ ├── app.ts # Global state: token, requiresSetup, login/logout
│ │ └── theme.ts # Mörkt/ljust läge, persistas i localStorage
│ ├── router/index.ts # SPA-routes
│ ├── views/ # Sidkomponenter
│ ├── components/ # Återanvändningsbara UI-komponenter
│ │ ├── editor/ # Visuell och källkodseditor
│ │ ├── layout/ # AppLayout, Sidebar, ThemeToggle
│ │ └── wizard/ # Installationsguide (SetupWizard)
│ ├── bridge/
│ │ └── asciidoc-bridge.ts # AsciiDoc ↔ TipTap JSON-konvertering
│ └── assets/main.css # Tailwind-bas + CSS-variabler
```
### Routing
| Sökväg | Komponent | Åtkomstkrav |
|---------------|------------------|---------------------|
| `/` | HomeView | Inloggad |
| `/doc/:slug` | DocumentView | Inloggad |
| `/admin` | AdminView | Inloggad, admin |
| `/setup` | SetupWizard | Ej konfigurerat |
Routeskydd hanteras **inte** med router guards utan direkt i `App.vue` — se avsnitt 4.1.
### GraphQL-klienten (`lib/gql.ts`)
Alla API-anrop görs med en enkel `fetch`-wrapper. Klienten:
- Läser JWT-token från `localStorage` och skickar den som `Authorization: Bearer <token>`
- Kastar ett `Error` om svaret innehåller GraphQL-errors
- Hanterar automatisk utloggning vid `UNAUTHORIZED`-fel
### Lösenordshantering (`lib/crypto.ts`)
Lösenordet hashas på klienten **innan** det skickas till servern med:
```
SHA-256(lowercase(username) + ":" + password)
```
Web Crypto API (`crypto.subtle.digest`) används — ingen extern beroende. Username fungerar som domän-separator (salt) så att samma lösenord ger olika digest för olika konton. Backenden tar emot denna digest och lagrar `bcrypt(digest)`.
### PWA
Konfigureras i `vite.config.ts` via `vite-plugin-pwa`:
- Service Worker med `skipWaiting` + `clientsClaim` (ny SW aktiveras omedelbart)
- Cachar statiska assets (JS, CSS, fonts)
- HTML och `/graphql` går alltid till nätverket (`NetworkFirst`)
- `navigateFallback: null` — förhindrar att SW returnerar cachat HTML för routes
---
## 3. Backend
### Paket
```
backend/
├── cmd/server/main.go # Startpunkt — laddar config, startar HTTP-server
├── internal/
│ ├── config/config.go # Config-struktur, Load/Save, ErrRequireSetup
│ ├── auth/auth.go # Session-hantering, bcrypt, LDAP-bind
│ ├── db/db.go # SQLite-wrapper — users-tabell
│ ├── storage/storage.go # Filhantering — Read/Write/List .adoc-filer
│ ├── git/git.go # Git-wrapper via os/exec — Commit/Log/Diff
│ └── graph/
│ ├── server.go # HTTP-handler + handgjord GraphQL-dispatcher
│ ├── resolver.go # Resolver-stub (plats för gqlgen-genererad kod)
│ └── schema.graphql # GraphQL-schema
```
### GraphQL-dispatcher
Backenden använder **inte** gqlgen-genererad kod i produktion. `server.go` implementerar en handgjord dispatcher som parsar query-strängen med `strings.Contains` och router till respektive handler-funktion. Detta ger enkel deployment utan kodgenerering men kräver manuell underhåll av schema och handlers.
### Autentisering och sessioner
```
Login-flöde (lokalt konto):
1. Frontend: kontrollerar authType (query userAuthType) → "local"
2. Frontend: SHA-256(username:password) → skickas som "password"
3. Backend: bcrypt.CompareHashAndPassword(stored_hash, received_digest)
4. Vid match: slumpmässig hex-token genereras och sparas i minnet
5. Token returneras till klient, lagras i localStorage
Login-flöde (LDAP-konto):
1. Frontend: kontrollerar authType → "ldap"
2. Frontend: skickar klartextlösenord i fältet "ldapPassword" (EJ hashat)
3. Backend: hämtar användarens DN via LDAP-sökning (baseDN + filter)
4. Backend: binder till LDAP med DN + klartextlösenord (ldapBind)
5. Vid lyckad bind: session skapas med roll "user"
Login-flöde (gäst):
1. Frontend: klickar "Continue as Guest" → skickar username="guest", password=""
2. Backend: identifierar gäst, skapar session utan lösenordskontroll, roll "guest"
3. Gäst ser bara dokument/mappar som admin explicit gett tillåtelse via ACL
Session-storage: in-memory map[token]*Session (försvinner vid omstart)
Session-livstid: 24 timmar (konfigurerat i auth.go)
**Rollbaserad UI-synlighet:**
Frontendens Pinia-store (`app.ts`) lagrar den inloggade användarens roll (`admin`, `user`, `guest`) via query `currentUserRole` som körs direkt efter inloggning. Admin-panelslänken i sidofältet visas bara om `role === 'admin'`.
```
**Viktigt:** Sessions lagras enbart i minnet. Vid server-omstart måste alla klienter logga in igen. LDAP-konfigurationsändringar (via admin-panelen) nollställer **inte** sessioner — befintliga sessioner är fortfarande giltiga.
### Gästanvändare
En inbyggd `guest`-användare skapas automatiskt vid installation och vid serverstart (migrations-safe). Gästen:
- Kräver inget lösenord
- Har roll `"guest"` (skild från `"user"` och `"admin"`)
- Kan som standard inte se något
- Admin ger tillgång per dokument eller mapp via ACL-panelen
### Autentisering via OIDC (Authentik)
Archivum är en confidential OIDC-klient mot Authentik. Flödet är rent
browser-redirect och ligger utanför GraphQL (två HTTP-endpoints):
```
Login-sida → GET /auth/oidc/login
→ 302 till Authentik (authorize, scope: openid profile email groups, state)
→ användaren autentiseras (Authentik → OpenLDAP)
→ 302 tillbaka GET /auth/oidc/callback?code&state
├─ validerar state (CSRF), byter code→token (backend↔Authentik, TLS)
├─ verifierar id_token (JWKS via go-oidc), läser preferred_username + groups
├─ mappar grupp → roll: Archivum-admin → admin, annars user
├─ speglar användare + grupper till SQLite (för admin-UI + ACL-mål)
├─ login-grind: användare/grupp måste vara tillåten
└─ 302 till /oidc/callback#token=… (SPA:n läser token ur fragmentet)
```
Konfiguration ligger i `config.oidc` (issuer, client_id/secret, grupp-mappning)
och sätts i admin-panelen. Discovery körs i bakgrunden vid start/ändring så att
en otillgänglig IdP inte blockerar servern. Se `docs/AUTHENTIK_SETUP.md`.
### Åtkomstkontroll (allow/deny-ACL)
ACL-poster lagras i SQLite-tabellen `acl`. Varje post kopplar en **sökväg** (slug
eller mappnamn) till ett **subjekt** (användare eller grupp), en **effekt**
(`allow`/`deny`) och sju rättighetsflaggor:
| Flagga | Innebär |
|------------|----------------------------------------|
| canSearch | Syns i sökning/listning |
| canView | Syns i mapplistning |
| canRead | Kan öppna och läsa innehållet |
| canEdit | Kan redigera och spara |
| canCreate | Kan skapa nya dokument i mappen |
| canDelete | Kan radera |
| canMove | Kan flytta/byta namn |
**Effektiv rättighet** (`db.EffectiveAccess`) beräknas för (användare, sökväg,
handling) genom att kombinera **alla** poster som gäller subjektet — användaren
själv **plus alla dess grupper** (från OIDC-claimet eller lokalt medlemskap) —
över sökvägen **och alla dess överliggande mappar**:
1. Standard = **deny**.
2. En `allow` på handlingen ger tillgång.
3. En `deny` på handlingen **vinner alltid** över allow.
Alltså: tillgång ges bara om någon post tillåter och ingen post nekar. Sökvägar
är hierarkiska (en `allow` på mappen `docs` gäller allt under `docs/`).
Enforcement sker för **alla icke-admins** (inloggade användare, `Archivum-reader`
och den publika/gäst-användaren) i alla operationer — listning, läsning, spara,
radera, flytta, skapa, historik/diff och bilduppladdning. **Admin kringgår ACL.**
> **Känd begränsning:** `/media/<bild>` serveras utan ACL eftersom bilder laddas
> via `<img>` utan Authorization-header. Skydda känsliga bilder på annat sätt
> (t.ex. signerade URL:er) om det behövs.
### Setup-detektion — `needsSetup`
Backenden räknar ett system som oklonfigurerat (`REQUIRE_SETUP`) om **något** av följande är sant:
1. `config.json` saknas eller är ofullständig (`cfg == nil`)
2. Databasen kunde inte öppnas (`database == nil`)
3. Databasen är tom — ingen adminanvändare har skapats (`!database.HasUsers()`)
`db.HasUsers()` gör en enkel `SELECT COUNT(*) FROM users` och returnerar `true` om minst ett konto finns. Detta innebär att en nyskapad, tom SQLite-fil (som Go skapar vid `db.New()`) fortfarande triggar setup-läge.
### DB-sökväg — `resolveDBPath()`
Setup-handleren bestämmer DB-sökvägen dynamiskt för att fungera i både Docker och dev-miljö:
```
resolveDBPath(configPath):
├─ Kan /data/db skapas/skrivas? → /data/db/archivum.db (Docker-volym)
└─ Annars → <config-katalog>/archivum.db (dev-miljö)
```
### Konfiguration
`config.json` hanteras av `internal/config`:
- `storage_path` — rotkatalog för .adoc-filer och Git-repo
- `db_path` — sökväg till SQLite-fil
- `jwt_secret` — används för session-token generation (ej JWT i klassisk mening)
- `listen_addr` — TCP-adress att lyssna på, t.ex. `:4000`
- `public_url` — extern bas-URL (t.ex. `https://archivum.brasse-pc.eu`), används
för att härleda OIDC-redirect-URI
- `oidc` — valfri OIDC/SSO-konfiguration (Authentik):
- `enabled`, `issuer`, `client_id`, `client_secret`, `redirect_url`
- `groups_claim` (default `groups`), `username_claim` (default `preferred_username`)
- `admin_group` (default `Archivum-admin`), `reader_group` (default `Archivum-reader`)
- `ldap` — valfri LDAP-konfiguration (direkt bind, äldre alternativ):
- `url` — LDAP-server URL, t.ex. `ldap://openldap:389`
- `base_dn` — rot-DN att söka ifrån, t.ex. `dc=example,dc=com`
- `admin_user` — tjänstekonto DN för uppslag (valfritt)
- `admin_pass` — tjänstekontoets lösenord (valfritt)
Om filen saknas returnerar `Load()` `ErrRequireSetup` — backenden startar i setup-läge.
---
## 4. Exekveringsflöden
### 4.1 Appstart och setup-detektion
```
Browser laddar /
App.vue onMounted()
├─ theme.init() (laddar tema från localStorage)
├─ app.checkStatus() → POST /graphql { systemStatus }
│ │ Backend: needsSetup = cfg==nil || db==nil || !db.HasUsers()
│ │
│ ├─ svar: "REQUIRE_SETUP"
│ │ → app.requiresSetup = true
│ │ → visas: <SetupWizard /> (renderas direkt, ej via router)
│ │
│ └─ svar: "OK"
│ → app.requiresSetup = false
│ ├─ app.token finns: → visas: <AppLayout />
│ └─ app.token saknas: → visas: <LoginView />
└─ ready = true (spinner försvinner)
```
`SetupWizard`, `LoginView` och `AppLayout` renderas direkt i `App.vue` (inte via `<router-view>`) för att undvika race conditions mellan `router.replace()` och Vue-renderingen.
### 4.2 Installationsguiden (SetupWizard)
```
Steg 1: Ange storage-sökväg, admin-användare, lösenord
Steg 2: Valfri LDAP-konfiguration
Steg 3: Bekräftelse
Vid submit:
1. SHA-256(adminUser:adminPass) → hashedPass
2. POST /graphql mutation Setup { setup(input: {..., adminPass: hashedPass}) }
├─ Backend: resolveDBPath() → bestämmer db_path (Docker vs dev)
├─ Backend: skapar SQLite-DB och admin-användare med bcrypt(hashedPass) ← FÖRST
├─ Backend: sparar config.json ← SEDAN
└─ Backend: initierar storage-katalog och Git-repo
3. app.requiresSetup = false
4. app.login(adminUser, adminPass) → SHA-256 igen → POST /graphql mutation Login
5. Redirect till /
```
Ordningen är viktig: om DB-skapandet eller user-insert misslyckas sparas **ingen** `config.json`. Nästa sidladdning ger återigen `REQUIRE_SETUP` och guiden visas på nytt. Tidigare sparades config först, vilket kunde lämna ett halvfärdigt tillstånd där backenden startade med config men utan adminanvändare.
### 4.3 Inloggningsflöde
```
Användaren fyller i användarnamn + lösenord (eller klickar "Continue as Guest")
app.login(username, password)
├─ query userAuthType(username) → "local" | "ldap" | "guest"
├─ authType == "local":
│ SHA-256(lowercase(username):password) → hashed
│ → POST /graphql mutation Login(u, p: hashed)
├─ authType == "ldap":
│ → POST /graphql mutation Login(u, p: "", ldapPassword: plaintext)
└─ authType == "guest":
→ POST /graphql mutation Login(u: "guest", p: "")
handleLogin (server.go)
├─ "guest": returnerar token utan lösenordskontroll
├─ "ldap" (is_ldap=1 i DB): ldapBind(username, ldapPassword)
│ → OK: session med roll "user"
└─ "local" (is_ldap=0): bcrypt.Compare(stored, hashed)
→ OK: session med roll baserad på DB-värde
Gäst-session:
- Roll "guest" — se ACL-enforcement i handleDocuments / handleFolders
- Kan bara se dokument/mappar som admin explicit beviljat via ACL
```
### 4.4 Spara dokument
```
Användaren redigerar och klickar Spara
POST /graphql mutation SaveDocument
{ slug, content (AsciiDoc), commitMessage, author }
handleSaveDocument (server.go)
├─ Kontrollerar session (Bearer token → Manager.Validate())
├─ storage.Write(slug, content) → skriver slug.adoc till disk
└─ git.Commit(slug.adoc, message, author, email)
└─ os/exec: git add → git commit
```
### 4.5 Versionshistorik och diff
```
GET /graphql { history(slug) }
└─ git.Log(slug.adoc) → os/exec: git log --format=...
→ []LogEntry { Hash, Author, Date, Subject }
GET /graphql { diff(slug, fromHash, toHash) }
└─ git.Diff(slug.adoc, from, to) → os/exec: git diff
→ unified diff-sträng
```
---
## 5. Kodstruktur
### Fullständig katalogstruktur
```
Archivum/
├── backend/
│ ├── cmd/server/main.go # Entrypoint
│ ├── config.json # Lokal dev-konfiguration (gitignoreras ej — bör .gitignore:as i produktion)
│ ├── go.mod / go.sum
│ └── internal/
│ ├── auth/auth.go # Session, bcrypt, LDAP
│ ├── config/config.go # Config-struktur och fil-I/O
│ ├── db/db.go # SQLite users-tabell
│ ├── git/git.go # Git-wrapper
│ ├── storage/storage.go # .adoc fil-I/O
│ └── graph/
│ ├── schema.graphql # GraphQL-schema (source of truth)
│ ├── resolver.go # Resolver-stub
│ └── server.go # HTTP-server + dispatcher + alla handlers
├── frontend/
│ ├── index.html
│ ├── vite.config.ts # Vite + PWA + dev-proxy konfiguration
│ ├── tailwind.config.js
│ ├── package.json
│ └── src/
│ ├── App.vue # Rotkomponent med setup/login/app-routing
│ ├── main.ts
│ ├── lib/
│ │ ├── gql.ts # GraphQL fetch-klient
│ │ └── crypto.ts # SHA-256 lösenordshashing
│ ├── stores/
│ │ ├── app.ts # Global app-state (Pinia)
│ │ └── theme.ts # Temahantering (Pinia)
│ ├── router/index.ts
│ ├── views/
│ │ ├── HomeView.vue # Dokumentlista
│ │ ├── DocumentView.vue # Visa/redigera dokument
│ │ ├── AdminView.vue # Administrationspanel
│ │ └── LoginView.vue # Inloggningsformulär
│ ├── components/
│ │ ├── editor/
│ │ │ ├── VisualEditor.vue # TipTap WYSIWYG-editor
│ │ │ └── SourceEditor.vue # CodeMirror AsciiDoc-källeditor
│ │ ├── layout/
│ │ │ ├── AppLayout.vue # Huvudlayout med sidebar
│ │ │ ├── Sidebar.vue # Dokumentnavigering
│ │ │ └── ThemeToggle.vue # Ljust/mörkt läge-knapp
│ │ └── wizard/
│ │ └── SetupWizard.vue # Installationsguide (3 steg)
│ ├── bridge/
│ │ └── asciidoc-bridge.ts # AsciiDoc ↔ TipTap JSON
│ └── assets/main.css
├── docker/
│ ├── Dockerfile # Multi-stage build (frontend → backend → runtime)
│ ├── entrypoint.sh # PUID/PGID-hantering, katalogskapande
│ ├── docker-compose.yml # Dev/standard deploy
│ └── docker-compose.prod.yml # Produktionsinställningar
└── ARCHITECTURE.md # Detta dokument
```
### Nyckelgränssnitt och datakontrakt
**GraphQL-schema (urval):**
```graphql
type Query {
systemStatus: SystemStatus!
userAuthType(username: String!): String! # "local" | "ldap" | "guest"
currentUserRole: String! # Returnerar rollen för inloggad session ("admin" | "user" | "guest")
documents(prefix: String): [DocumentMeta!]!
document(slug: String!): Document
folders: [String!]!
history(slug: String!): [CommitEntry!]!
diff(slug: String!, fromHash: String!, toHash: String!): String!
acl(path: String!): [ACLEntry!]!
aclSubjects: ACLSubjects! # Användare + grupper för ACL-picker (inkl. guest)
ldapBrowse(url: String, baseDN: String, adminUser: String, adminPass: String): LDAPTree!
}
type Mutation {
login(username: String!, password: String!, ldapPassword: String): String!
logout: Boolean!
setup(input: SetupInput!): Boolean!
saveDocument(input: SaveDocumentInput!): Document!
deleteDocument(slug: String!): Boolean!
updateLdapConfig(input: LDAPInput): Boolean!
importLdapSubject(type: String!, name: String!): Boolean!
setAcl(input: ACLInput!): Boolean!
removeAcl(id: Int!): Boolean!
createFolder(path: String!): Boolean!
moveDocument(oldSlug: String!, newSlug: String!): Boolean!
}
input LDAPInput {
url: String!
baseDN: String
adminUser: String!
adminPass: String # Utelämnas → befintligt lösenord behålls
}
```
---
## 6. Containerisering och driftsättning
### Dockerfile (multi-stage)
```
Stage 1 (frontend-builder) — node:20-alpine
npm install && npm run build → /app/frontend/dist
Stage 2 (backend-builder) — golang:1.22-alpine
CGO_ENABLED=0 go build → /archivum (statisk binär)
Stage 3 (runtime) — alpine:3.20
Installerar: su-exec, git
Kopierar: dist/ → /srv/archivum/ui
archivum → /usr/local/bin/archivum
entrypoint.sh
Volumes: /config, /data
Port: 4000
Entrypoint: entrypoint.sh
```
### Volymer och kataloger
| Volym | Innehåll | Env-variabel |
|-----------|---------------------------------------------------|----------------|
| `/config` | `config.json` | `DOCKER_PATH` |
| `/data` | `wiki/` (AsciiDoc + Git-repo), `db/archivum.db` | — |
### Miljövariabler
| Variabel | Standard | Beskrivning |
|---------------|-------------------|------------------------------------------|
| `DOCKER_PATH` | — | Host-sökväg som monteras som `/config` |
| `UI_DIR` | `/srv/archivum/ui`| Sökväg till kompilerad frontend |
| `PUID` | `1000` | Filsystemsägare (UID) |
| `PGID` | `1000` | Filsystemsägare (GID) |
| `TZ` | `Europe/Stockholm`| Tidszon |
| `HOST_PORT` | `8080` | Extern port i docker-compose |
### Multi-arch build
Bilden stöder `linux/amd64` (server/desktop) och `linux/arm64` (Raspberry Pi 5):
```bash
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry:5000/archivum:latest \
-f docker/Dockerfile --push .
```
### entrypoint.sh — PUID/PGID-hantering
Containern startar som `root`. `entrypoint.sh`:
1. Skapar `/config`, `/data/wiki`, `/data/db` om de saknas
2. Ändrar ägare på volymerna till `PUID:PGID`
3. Kör applikationen som `PUID:PGID` via `su-exec`
### docker-compose.yml (minimal .env)
```env
DOCKER_PATH=/opt/archivum # Host-sökväg — config/ och data/ skapas här
HOST_PORT=8080
PUID=1000
PGID=1000
TZ=Europe/Stockholm
```
### Uppgraderingsförfarande
1. Bygg/hämta ny image
2. `docker compose down`
3. `docker compose up -d`
Inga databas-migreringar behövs i de flesta uppgraderingar; SQLite-schemat är bakåtkompatibelt (`CREATE TABLE IF NOT EXISTS`).