Files
oikos/containers/126-plato.md
claudio 94d02785f6 wiki: plato auto-deploy (gitea webhook on dtoro/Plato → LXC 126:9799)
- infrastructure/auto-deploy.md: Plato pipeline added (Shape B,
  port 9799, hook id 8); ALLOWED_HOST_LIST gains 192.168.8.190;
  per-pipeline note for the LXC 126 specifics (origin URL, no
  ProtectHome); Changelog updated
- containers/126-plato.md: Auto-deploy section + Changelog entry
2026-05-13 09:00:34 +02:00

5.4 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.networkcaddy (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 of http://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 (owned 65532:65532 to match the distroless nonroot UID) bind-mounted into the LXC at /opt/plato/data, then bound into the container at /data via a docker-compose.override.yml:
    services:
      plato:
        volumes: !override
          - /opt/plato/data:/data
        restart: unless-stopped
    
  • .env at /opt/plato/app/.env (optional, untracked) — LLM provider keys (OPENROUTER_API_KEY, ANTHROPIC_API_KEY, etc.) and PLANTUML_BASE_URL override. Absent by default; LLM features stay greyed out, PlantUML defaults to the public service.
  • Run / update: push to dtoro/Plato (auto-deploys, see below) or cd /opt/plato/app && git pull && docker compose up -d --build for a manual rebuild.

Auto-deploy

Push to dtoro/Plato main triggers a rebuild — same Shape B pattern as Artifacto / mule-image. Webhook receiver at /opt/plato-deploy/, systemd unit plato-deploy-webhook.service, port 9799, gitea hook id 8.

  • Receiver: http://192.168.8.190:9799/deploy, signed payload (HMAC-SHA256, secret in /etc/plato-deploy/secret).
  • Logs: journalctl -u plato-deploy-webhook -f.
  • Health: curl http://127.0.0.1:9799/health.
  • Manual deploy: /opt/plato-deploy/deploy.sh.
  • Gitea's app.ini ALLOWED_HOST_LIST was extended with 192.168.8.190 to allow this delivery.

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

Changelog

2026-05-13 — auto-deploy wired

Shape B pipeline added (/opt/plato-deploy/, port 9799, gitea hook id 8). ALLOWED_HOST_LIST in gitea app.ini extended with 192.168.8.190. See auto-deploy.

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.