# 126 — `plato` Docker host for [Plato](https://git.hubris.network/dtoro/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`](../infrastructure/dns.md) → [caddy (121)](121-caddy.md) → `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)](121-caddy.md)). - **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`: ```yaml 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](../infrastructure/auto-deploy.md). 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](120-mule-images.md) and [122 arriman](122-arriman.md). 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](124-authentik.md): ``` address=/plato.hubris.network/192.168.8.175 ``` ## Related - [Caddy (121)](121-caddy.md) - [DNS (split-horizon)](../infrastructure/dns.md) - [Media permissions](../infrastructure/media-permissions.md) ## 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](../infrastructure/auto-deploy.md#plato). ### 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)](index.md#recently-destroyed-kept-for-archaeology). Hit the [fresh-DB bootstrap bug](#fresh-db-bootstrap-workaround) on first boot; worked around by pre-seeding the SQLite schema.