- containers/126-plato.md: Plato (notes/discovery) on LXC 126 — stack, bind mount, caddy + DNS, fresh-DB bootstrap workaround - containers/125-seafile.md: removed (LXC destroyed 2026-05-13) - containers/index.md: 126 added; 125 moved to recently-destroyed - infrastructure/dns.md: plato entry added, files entry removed, Changelog reflects both - README.md: top-level quick-links table swaps 125 → 126
4.5 KiB
126 — plato
Docker host for Plato — a cross-linked notes workspace (SvelteKit SPA embedded into a Go HTTP server, SQLite-backed). LAN+mesh only, no public ingress.
At a glance
- Hostname:
plato - IP:
192.168.8.190 - Privilege: privileged
- Resources: 2 cores / 2 GiB RAM / 8 GiB rootfs / 1 GiB swap
- Mounts: host
/mnt/library/documents/plato↔ container/opt/plato/data - Public hostname:
plato.hubris.network→ caddy (121) →192.168.8.190:8080
Stack
Single-container deploy. The repo's Dockerfile is a three-stage build (Node → Go → distroless/static-debian12:nonroot, ~23 MiB final image). The container exposes :8080 and writes its SQLite db to /data.
- Checkout:
/opt/plato/app(clone ofhttp://192.168.8.121:3000/dtoro/Plato.git, using the cached gitea PAT in/root/.git-credentials— same pattern as caddy (121)). - Data: host
/mnt/library/documents/plato(owned65532:65532to match the distroless nonroot UID) bind-mounted into the LXC at/opt/plato/data, then bound into the container at/datavia adocker-compose.override.yml:services: plato: volumes: !override - /opt/plato/data:/data restart: unless-stopped .envat/opt/plato/app/.env(optional, untracked) — LLM provider keys (OPENROUTER_API_KEY,ANTHROPIC_API_KEY, etc.) andPLANTUML_BASE_URLoverride. Absent by default; LLM features stay greyed out, PlantUML defaults to the public service.- Run / update:
cd /opt/plato/app && git pull && docker compose up -d --build.
Fresh-DB bootstrap workaround
The schema constant in backend/internal/views/store.go (as of commit e0542c0) creates the views table without project_id, then immediately runs CREATE UNIQUE INDEX … ON views(project_id, lower(title)). On a fresh DB this fails (no such column) and Plato crash-loops with open views store: SQL logic error: no such column: project_id. ensureProjectIDColumn() adds the column on subsequent migrations, but the schema apply happens first.
Until the upstream fix lands, pre-seed the DB before first start:
docker compose stop
rm -f /opt/plato/data/plato.db
python3 - <<'PY'
import sqlite3
c = sqlite3.connect('/opt/plato/data/plato.db')
c.executescript("""
CREATE TABLE views (
id TEXT PRIMARY KEY,
type TEXT NOT NULL DEFAULT 'document',
title TEXT NOT NULL,
aliases TEXT NOT NULL DEFAULT '[]',
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX views_project_title_lower ON views(project_id, lower(title));
CREATE INDEX views_project_id ON views(project_id);
""")
c.commit()
PY
chown 65532:65532 /opt/plato/data/plato.db
docker compose up -d
Once the column exists, every subsequent boot's IF NOT EXISTS clauses no-op. Once Plato is fixed upstream (remove the CREATE UNIQUE INDEX line from the boot schema constant — ensureTitleIndexPerProject() already re-creates it after the migration), this preseed becomes unnecessary.
Why privileged
Matches the docker-host convention used by 120 mule-images and 122 arriman. Distroless nonroot's UID 65532 on the host bind mount maps directly through; unprivileged would shift the UID by the idmap offset and the container couldn't write /data without extra plumbing.
Caddy
plato.hubris.network {
tls {
dns ionos {env.IONOS_AUTH_API_TOKEN}
}
reverse_proxy 192.168.8.190:8080
}
No Authentik forward-auth — Plato has no auth model yet; access control is "be on the LAN or the mesh".
DNS
dnsmasq entry on 124-authentik:
address=/plato.hubris.network/192.168.8.175
Related
Changelog
2026-05-13 — container created, Plato deployed
LXC 126 stood up on Debian 12 standard, privileged, docker-ce installed. Plato cloned from dtoro/Plato, built and started. Caddy site and dnsmasq split-horizon entry added. Recycled the IP/ID slot freed earlier the same day by the decommissioned Seafile experiment (LXC 125). Hit the fresh-DB bootstrap bug on first boot; worked around by pre-seeding the SQLite schema.