# Ledgerix — Cloud / Linux deployment
> Officiële handleiding voor het zelf draaien van Ledgerix op een
> Linux-server, eigen cloud-VPS (Hetzner / AWS / Azure / Scaleway /
> DigitalOcean), of een Kubernetes-cluster. Ledgerix biedt geen
> managed SaaS — klanten hosten zelf.
**Inhoudsopgave**
1. [Welke aanpak past bij mij?](#welke-aanpak-past-bij-mij)
2. [Snel beginnen — Docker single-host](#snel-beginnen--docker-single-host)
3. [Docker Compose met reverse-proxy + TLS](#docker-compose-met-reverse-proxy--tls)
4. [Native Linux install (systemd, geen Docker)](#native-linux-install-systemd-geen-docker)
5. [Reverse-proxy keuzes — Caddy / Traefik / nginx](#reverse-proxy-keuzes--caddy--traefik--nginx)
6. [Configuratie via environment-variabelen](#configuratie-via-environment-variabelen)
7. [Backups en restore](#backups-en-restore)
8. [Upgrades — naar een nieuwe versie](#upgrades--naar-een-nieuwe-versie)
9. [Health-check en monitoring](#health-check-en-monitoring)
10. [Security-checklist productie](#security-checklist-productie)
11. [OCR (OCRix) als sidecar](#ocr-ocrix-als-sidecar)
---
## Welke aanpak past bij mij?
| Doel | Aanbevolen aanpak |
|------|-------------------|
| Lokaal testen op je laptop | Docker — `docker compose up`, klaar in 2 minuten |
| Productie op één VPS | Docker Compose + Caddy (automatische TLS) |
| Productie op meerdere hosts / Kubernetes | Het Docker-image deployen via je eigen manifest |
| Linux-server zonder Docker | systemd-service met het Linux-binary |
| Windows-server | De Inno Setup `Ledgerix-Setup-X.Y.Z.exe` installer (geen Docker nodig) |
Voor alle Linux-aanpakken geldt: Ledgerix is een **stateless single binary**
met een **SQLite-database in één directory**. Backup = die directory.
Upgrade = nieuw binary / nieuw image starten; migraties draaien
automatisch bij boot.
---
## Snel beginnen — Docker single-host
Vereist: Docker Engine 20.10+ en `docker compose` (v2).
Er zijn twee wegen. **A** (aanbevolen) gebruikt het officiële, kant-en-klare
image van de downloadpagina — daar zit de licentie-portaal-pubkey al in
gebakken, dus je hoeft alleen je licentiesleutel in te voeren. **B** bouwt
zelf vanuit de broncode; dat is het geavanceerde pad en vergt dat je de
pubkey zelf meegeeft (zie de sectie **Licentie**).
### A. Officieel image gebruiken (aanbevolen)
Download de cloud-bundle van
— het bestand
`ledgerix-cloud-.tar.gz`. Die bevat het Docker-image (mét
ingebakken pubkey), een `docker-compose.yml` en de reverse-proxy-configs.
```bash
# 1. Uitpakken en het image laden (geen registry/rebuild nodig)
tar xzf ledgerix-cloud-.tar.gz
cd ledgerix-cloud-
docker load < ledgerix-image-.tar
# 2. (optioneel) admin-account vooraf scripten i.p.v. de setup-wizard
# echo "ADMIN_PASSWORD=KiesEenSterkInitieelWachtwoord!" > .env
# chmod 600 .env
# 3. Start
docker compose up -d
docker compose logs -f ledgerix
```
Daarna activeer je je licentie in de UI via **Instellingen → Licentie** met
de LXLIC-sleutel die je bij je licentie hebt gekregen.
### B. Zelf bouwen vanuit de broncode (geavanceerd)
```bash
# 1. Clone de repo
git clone https://github.com/CodingKaspers/ledgerix.git
cd ledgerix
# 2. Bouw het image. Voor licentie-activatie moet je de portal-pubkey
# meebakken (zie de sectie Licentie); zonder pubkey draait de app wel
# maar faalt activatie (tenzij je LEDGERIX_DEV_MODE=1 gebruikt).
docker build --build-arg LEDGERIX_LICENSE_PUBKEY_BUILTIN= -t ledgerix:latest .
# 3. Start (docker-compose.yml in de repo bouwt/gebruikt ledgerix:latest)
docker compose up -d
docker compose logs -f ledgerix
```
Browse naar `http://127.0.0.1:8080`. Bij de eerste keer verschijnt een
**setup-wizard** waarin je het beheerdersaccount (gebruiker + wachtwoord)
aanmaakt. Zette je een `ADMIN_PASSWORD`, dan slaat de wizard over en log je
in met `admin` + dat wachtwoord (wijzig het daarna via **Gebruikers**).
> Default `docker-compose.yml` bindt port 8080 op `127.0.0.1` — niet
> publiek bereikbaar. Voor productie zet je er een reverse-proxy voor;
> zie volgende sectie.
---
## Docker Compose met reverse-proxy + TLS
Voor publieke toegang heb je TLS nodig en wil je niet de Rust-server
zelf laten praten met internet. **Caddy** is de eenvoudigste keuze —
automatische Let's Encrypt out-of-the-box.
```bash
# Installeer Caddy op de host (Debian/Ubuntu)
sudo apt install caddy
# Plaats de meegeleverde Caddyfile
sudo cp deploy/caddy/Caddyfile /etc/caddy/Caddyfile
# Bewerk: vervang `ledgerix.example.com` door je echte domein
sudo nano /etc/caddy/Caddyfile
# Reload
sudo systemctl reload caddy
```
Zorg dat:
- Je DNS A-record naar de host wijst (vóór de eerste reload — Caddy
vraagt direct een certificaat aan).
- Poort 80 + 443 publiek bereikbaar zijn (ACME-challenge + TLS-traffic).
- De Ledgerix-container op `127.0.0.1:8080` luistert zoals in
`docker-compose.yml` ingesteld (default is dit zo).
Browse naar `https://ledgerix.example.com` — TLS werkt vanaf nu
automatisch en certs vernieuwen via Caddy zonder verdere actie.
Alternatieven: zie [reverse-proxy keuzes](#reverse-proxy-keuzes--caddy--traefik--nginx).
---
## Native Linux install (systemd, geen Docker)
Voor klanten die geen Docker willen of een minder-resource-intensieve
install zoeken.
```bash
# 1. Bouw het binary op een Linux-machine (of build-server)
# Voor licentie-activatie: bak de portal-pubkey in met de env-var
# LEDGERIX_LICENSE_PUBKEY_BUILTIN= vóór cargo build, of geef
# 'm op runtime mee via LEDGERIX_LICENSE_PUBKEY (zie sectie Licentie).
cargo build --release --bin ledgerix
# Resultaat: target/release/ledgerix
# 2. Plaats op de doelserver
sudo useradd --system --home /opt/ledgerix --shell /usr/sbin/nologin ledgerix
sudo mkdir -p /opt/ledgerix/data
sudo cp target/release/ledgerix /opt/ledgerix/
sudo cp -r crates/ledgerix-web/static /opt/ledgerix/
sudo cp -r docs /opt/ledgerix/
sudo chown -R ledgerix:ledgerix /opt/ledgerix
# 3. Installeer de systemd-unit
sudo cp deploy/systemd/ledgerix.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ledgerix
# 4. Check
sudo systemctl status ledgerix
sudo journalctl -u ledgerix -f
```
De unit-file in [`deploy/systemd/ledgerix.service`](../deploy/systemd/ledgerix.service)
bevat strakke sandboxing-defaults (NoNewPrivileges, ProtectSystem=strict,
SystemCallFilter, MemoryMax=2G). Pas ze aan voor je specifieke setup
indien nodig.
Reverse-proxy: zelfde stap als Docker-aanpak. Caddy/Traefik/nginx
proxy naar `127.0.0.1:8080`.
---
## Reverse-proxy keuzes — Caddy / Traefik / nginx
Alle drie werken; kies wat je al draait of het minst werk geeft.
| Optie | Voor wie | Voorbeeld-config |
|-------|----------|------------------|
| **Caddy** | Single-host, zero-effort TLS | [`deploy/caddy/Caddyfile`](../deploy/caddy/Caddyfile) |
| **Traefik** | Multi-service Docker-setup, label-driven | [`deploy/traefik/ledgerix.yml`](../deploy/traefik/ledgerix.yml) |
| **nginx** | Bestaande nginx-infra, certbot voor TLS | [`deploy/nginx/ledgerix.conf`](../deploy/nginx/ledgerix.conf) |
Alle drie configs zetten security-headers (HSTS, X-Content-Type-Options,
Referrer-Policy) en een body-size-limit van 25M voor OCR + Peppol-XML
(Caddy `request_body max_size`, nginx `client_max_body_size`, Traefik de
`ledgerix-bodylimit`-middleware `buffering.maxRequestBodyBytes`).
Ledgerix stuurt z'n eigen CSP server-side; laat de proxy die niet
overschrijven.
---
## Configuratie via environment-variabelen
Ledgerix is volledig env-driven. Wat de installer/systemd-unit zet kun
je in een Docker-context ook gewoon via `-e` of de compose
`environment:` sectie meegeven.
| Variabele | Default | Doel |
|-----------|---------|------|
| `DATABASE_URL` | `sqlite:./data/ledgerix.db` | Pad naar de SQLite-file. Voor Docker: `sqlite:/data/ledgerix.db`. |
| `PORT` | `8080` | HTTP-poort waarop Ledgerix luistert |
| `LEDGERIX_BIND` | `127.0.0.1` | Bind-interface. **In Docker zet je 'm op `0.0.0.0`** zodat de container van buiten bereikbaar is; op een bare-metal install hou je 'm op `127.0.0.1` mét reverse-proxy ervoor. |
| `STATIC_DIR` | auto-detect | Map met `static/`-frontend |
| `DOCS_DIR` | auto-detect | Map met `docs/`-markdown |
| `ADMIN_PASSWORD` | _(leeg)_ | Optioneel. Leeg = setup-wizard maakt het eerste account aan. Gezet = admin-account wordt bij de allereerste boot met dit wachtwoord aangemaakt (daarna genegeerd). |
| `LEDGERIX_CORS_ORIGINS` | _(leeg)_ | Komma-gescheiden toegestane origins voor cross-origin requests, of `*` voor de oude permissieve modus. Leeg = alleen same-origin/localhost. |
| `LEDGERIX_SINGLE_USER` | _(uit)_ | `1` = single-user kiosk-modus (auto-login, geen loginscherm). Niet gebruiken op een multi-user of internet-facing install. |
| `RUST_LOG` | `info` | Tracing-level: `error` / `warn` / `info` / `debug` / `trace` |
| `LEDGERIX_LICENSE_PUBKEY` | _(leeg)_ | Licentie-portaal-pubkey op runtime. Nodig om een licentie te kunnen activeren als de pubkey niet is ingebakken bij de build (zie **Licentie** hieronder). Alternatief voor de build-arg, zonder rebuild. |
| `LEDGERIX_DEV_MODE` | _(uit)_ | `1` = omzeilt de licentiecontrole volledig (alle features open). **Alleen voor test/dev**, nooit op een productie-install. |
> **Compile-time variant:** `LEDGERIX_LICENSE_PUBKEY_BUILTIN` is geen
> runtime-env maar een **build-arg** die de pubkey in de binary bakt (zie
> Dockerfile en de **Licentie**-sectie). Ofwel bak je 'm in bij de build,
> ofwel geef je `LEDGERIX_LICENSE_PUBKEY` op runtime mee — één van beide is
> nodig voor licentie-activatie.
Alle andere applicatie-instellingen (SMTP, OCR-engine, backup-schedule,
KvK-API-key, OAuth-providers) staan in de `settings`-tabel in de
database en zijn via **Instellingen** in de UI te beheren. Géén
losse config-file om bij te houden.
---
## Licentie
Ledgerix controleert bij het activeren van een licentie de handtekening
van die licentie tegen de **publieke sleutel van het licentie-portaal**.
Die pubkey moet in de build/omgeving zitten, anders faalt activatie met
`error.license.portal_pubkey_unset`.
**In het normale geval hoef je hier niets voor te doen.** De officiële
distributies (de Windows-installer én het cloud-image uit de downloadpagina,
pad **A** hierboven) hebben de pubkey al ingebakken. Licentie-activatie is
dan simpelweg: je LXLIC-sleutel invoeren via **Instellingen → Licentie**.
De pubkey reist dus mee met de uitgifte, precies zoals verwacht.
De pubkey wordt alleen een aandachtspunt als je **zelf vanuit de broncode
bouwt** (pad **B**), want `keys/` staat bewust niet in de repo (die map
bevat óók de geheime signing-key). Dan heb je één van deze opties:
1. **Ingebakken bij de build**: geef de pubkey mee als build-arg.
```bash
docker build --build-arg LEDGERIX_LICENSE_PUBKEY_BUILTIN= -t ledgerix:latest .
```
In `docker-compose.yml` kan dit via `LEDGERIX_LICENSE_PUBKEY_BUILTIN`
in je `.env` (de compose-`build.args` pikt 'm op).
2. **Op runtime**: zet `LEDGERIX_LICENSE_PUBKEY=` als
omgevingsvariabele. Geen rebuild nodig.
3. **Zonder licentie (test/evaluatie)**: `LEDGERIX_DEV_MODE=1` omzeilt de
licentiecontrole en zet alle features open. Niet voor productie.
> De pubkey is een *publieke* sleutel (geen geheim). Bouw je zelf en heb je
> de bijbehorende portal-pubkey nodig, vraag 'm op bij je licentie-uitgifte.
---
## Backups en restore
**Wat moet ik back-uppen?**
Alles staat in één directory: `/data` (Docker) of `/opt/ledgerix/data`
(systemd). Daarin:
- `ledgerix.db` — de live database
- `ledgerix.db-wal` + `ledgerix.db-shm` — WAL-bestanden (mogen mee in backup)
- `backups/` — door Ledgerix zelf gegenereerde backups als je dat in
Instellingen hebt aangezet
**Drie aanpakken:**
1. **Cloud-volume snapshot** — als je VPS-aanbieder volume-snapshots
ondersteunt (Hetzner, AWS EBS, Azure Disk), is een nightly
snapshot de eenvoudigste oplossing. Crash-consistency is OK voor
SQLite met WAL.
2. **In-app backup** — Activeer in Instellingen → Backup. Schrijft
een AES-256-GCM versleutelde `.db.enc` naar `/data/backups/`. Stuur
die via `restic` of `rclone` naar S3/B2/etc.
3. **Cold `tar`** — Voor een vluchtige snapshot vanuit een
cron-script:
```bash
docker run --rm \
-v ledgerix-data:/d \
-v /var/backups/ledgerix:/out \
busybox sh -c "tar czf /out/ledgerix-$(date +%F).tar.gz /d"
```
**Restore:**
```bash
# Stop Ledgerix
docker compose stop ledgerix
# of: sudo systemctl stop ledgerix
# Plaats de oude db terug
docker run --rm -v ledgerix-data:/d -v /pad/naar/backup:/in busybox \
cp /in/ledgerix.db /d/ledgerix.db
# Start
docker compose start ledgerix
```
Sinds v0.76.0 kun je een backup ook **als scenario** importeren via
de **Backup & herstel**-pagina — risico-vrij rondkijken in oude data
zonder je live admin te raken.
---
## Upgrades — naar een nieuwe versie
Ledgerix is **stateless**: alle persistente staat zit in de
data-directory. Migraties draaien automatisch bij boot (`include_str!`
in de storage-crate; idempotent).
**Docker — officieel image (pad A):**
```bash
# Nieuwe cloud-bundle downloaden, image laden, herstarten
docker load < ledgerix-image-.tar
docker compose up -d ledgerix
```
**Docker — zelf gebouwd (pad B):**
```bash
# Bouw de nieuwe versie (docker-compose.yml gebruikt standaard het lokaal
# gebouwde ledgerix:latest); geef zo nodig de licentie-pubkey mee.
docker build -t ledgerix:latest .
docker compose up -d ledgerix
```
**Systemd:**
```bash
sudo systemctl stop ledgerix
sudo cp target/release/ledgerix /opt/ledgerix/ledgerix
sudo cp -r crates/ledgerix-web/static/* /opt/ledgerix/static/
sudo systemctl start ledgerix
```
**Hou de release-notes bij:** [`CHANGELOG.md`](../CHANGELOG.md). De
in-app version-banner waarschuwt sinds v0.11.2 als er een nieuwe
release is.
**Rollback** is simpel: deploy het vorige binary / image. Een schema-
migratie is altijd `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE ADD
COLUMN` — bestaande data blijft compatibel met de oudere release.
Bewaar wel altijd een pre-upgrade backup als verzekering.
---
## Health-check en monitoring
**Health-check endpoint:**
```
GET /api/v1/settings/version → 200 OK { "current": "0.95.41" }
```
Geen auth nodig. Docker `HEALTHCHECK` uit het meegeleverde image
gebruikt 'm al; eigen orchestrators (Kubernetes liveness/readiness,
HAProxy, Pingdom) kunnen 'm direct pingen.
**Tracing-logs** komen op stdout via de `tracing`-crate. Log-niveau
stel je in met `RUST_LOG` (`error`/`warn`/`info`/`debug`/`trace`).
Gestructureerde JSON-logs richting Loki/Datadog/CloudWatch zijn nog
niet ingebouwd; gebruik tot die tijd de JSON-logdriver van je
container-runtime (`docker logs`, of `--log-driver` naar je collector).
**Systeem-monitor in de app** — `/monitor.html` (admin-only) toont
proces-RAM, systeem-RAM, uptime, DB-grootte en laatste backup-status.
Voor externe monitoring is dit niet vervangend, wel een snelle sanity-
check vanuit de browser.
---
## Security-checklist productie
Voor je publiekelijk live gaat:
- [ ] **Admin-wachtwoord gewijzigd** vanaf de default (eerste login)
- [ ] **2FA actief** voor minimaal alle Admin-accounts (Gebruikers → 2FA)
- [ ] **TLS via reverse-proxy** — geen plain HTTP op publiek IP
- [ ] **`LEDGERIX_BIND=0.0.0.0` alleen in Docker** (intern netwerk);
op bare metal blijft `127.0.0.1` + proxy de standaard
- [ ] **Firewall**: alleen poort 80 + 443 publiek, poort 8080 dicht
- [ ] **Volume-permissies**: `/data` alleen leesbaar voor de
ledgerix-user (UID 10001 in het Docker-image)
- [ ] **Backup-strategie aan** — getest met een restore
- [ ] **Auto-lock geactiveerd** (Instellingen → Beveiliging) als
Ledgerix op een gedeelde machine staat (accountantskantoor)
- [ ] **Backup-encryptie wachtwoord** ingesteld als je backups naar
een derde-partij-cloud (S3/B2) stuurt
- [ ] **Audit-log review-routine** ingericht (wekelijks via de
Audit Trail-pagina of CSV-export)
- [ ] **Rate-limiters** staan aan default (`LoginRateLimiter`
5/15 min, `HeavyOpRateLimiter` 20/5 min) — wijzig niet zonder
gegronde reden
Ledgerix's eigen permissies-systeem (groep-gebaseerd RBAC + per-
endpoint permissions sinds v0.70) is de primaire verdedigingslinie
ná de proxy. Hou per-account toegang minimaal: een boekhouder hoeft
geen Admin te zijn.
---
## OCR (OCRix) als sidecar
OCR is **optioneel**. Zonder ocrix-service werkt Ledgerix prima — alleen
de bon-scan-flow in `/eenvoudig.html` valt terug op handmatige invoer.
**Opstelling 1: OCRix uit, alleen Tesseract op de host.**
Installeer `tesseract-ocr` + `tesseract-ocr-nld` op de host, zet in
Instellingen → OCR `ocr_engine = tesseract`. Voor Docker: voeg de
packages toe aan het Dockerfile (komt niet default mee — Tesseract
voegt ~50 MB image-grootte toe).
**Opstelling 2: OCRix als sidecar-container.**
Clone https://github.com/CodingKaspers/ocrix naast deze repo en bouw
'm tot een eigen image (`ocrix:latest`). Uncomment de `ocrix:` block
in `docker-compose.yml`. In Ledgerix-instellingen: `ocr_engine = ocrix`,
`ocrix_service_url = http://ocrix:8081` (de Docker-DNS-naam van de
sidecar).
**Opstelling 3: OCR helemaal uit.**
Default. Niets doen. De Mailbox-inbox accepteert dan alleen PDFs
waar pdf-extract de tekst-laag uit haalt; gescande JPEG-bonnen
worden niet auto-uitgelezen maar kunnen wel handmatig geboekt
worden.