# 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 ` - 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/` serveras utan ACL eftersom bilder laddas > via `` 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 → /archivum.db (dev-miljö) ``` ### Git-synk mot remote (SSH) Wiki-repot kan synkas mot ett remote git-repo (t.ex. Gitea) över SSH. Implementation: `internal/git/sync.go` (remote-operationer + mutex) och `internal/graph/gitsync.go` (GraphQL-handlers, nyckelhantering, bakgrunds- ticker). Admin-UI: `frontend/src/components/GitSyncSection.vue` med `MergeConflictDialog.vue` som popup vid konflikter. - **SSH-nyckel:** servern genererar själv ett ed25519-nyckelpar vid första aktivering, lagrat i `/ssh/` (0600). Publika nyckeln visas i admin-panelen och registreras som deploy key hos git-värden. `GIT_SSH_COMMAND` pekar på nyckeln + egen `known_hosts` (accept-new) eftersom appen kör utan hemkatalog. Privata nyckeln exponeras aldrig via API:t. - **Live-gren** (`live_branch`): grenen som bakgrundssynken håller uppdaterad. Bakgrundssynk (intervall i `auto_sync_minutes`, 0 = manuell) gör endast fast-forward-pulls; allt som kräver riktig merge flaggas för admin (`attention` i `gitSyncStatus`). - **Read-only** (`read_only`): pull tillåts men aldrig push (spärras i backend). - **Grenar:** skapa och byt gren i admin-panelen; remote-grenar spåras automatiskt vid byte. - **Merge-konflikter:** pull lämnar mergen öppen och UI:t visar en popup där man väljer lokal/remote per fil eller för allt; alternativt avbryt mergen. - **Reset från remote:** tvingar lokala grenen att matcha remoten; lokalt läge sparas alltid först i en backup-gren (`backup/`). Används också för bootstrap när historikerna är orelaterade (`UNRELATED_HISTORIES`). - **Headless-konfiguration:** allt kan sättas via `config.json`-sektionen `git_sync` + omstart, eller via GraphQL-mutationen `updateGitSync` — inget webbgui krävs. ### 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 - `git_sync` — se avsnittet ovan: `enabled`, `remote_url`, `live_branch`, `read_only`, `auto_sync_minutes` - `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: (renderas direkt, ej via router) │ │ │ └─ svar: "OK" │ → app.requiresSetup = false │ ├─ app.token finns: → visas: │ └─ app.token saknas: → visas: │ └─ ready = true (spinner försvinner) ``` `SetupWizard`, `LoginView` och `AppLayout` renderas direkt i `App.vue` (inte via ``) 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`).