Patch notes — first production deploy, fixes & Projects (2026-06-10 session 3)
Branch: main. This session took skui.io from “builds but never deployed” to live in
production at https://skui.io, fixed the bugs that surfaced once it was actually running,
and moved the Projects page into the admin/DB.
1 — Compose-based production deploy ✅
Problem: build.yml’s deploy job did a single docker run of only the web container
with a named volume — no collector, no socket-proxy, and not the compose stack.
Fix (.gitea/workflows/build.yml): rewrote the deploy job to be compose-based:
scp compose.yml→/storage/skui.io/compose.yml- SSH to
deploy.skui.ioasgitea-nextcloud:mkdir -p data, auto-detect the nginx network fromnc-nginx-1→ export asNC_NETWORK,docker compose pull && up -d --remove-orphans,docker image prune -f.
Data volume → bind mount (compose.yml): web and collector now mount ./data:/data
(resolves to /storage/skui.io/data) instead of the named skui-data volume, so the SQLite DB
- uploads live in a host folder. Container runs as root → writes through regardless of host
ownership (
gitea-nextcloud:users).
No server .env needed: init_db.py seeds admin/skuiio; COLLECTOR_TOKEN defaults to
skui-internal; NC_NETWORK is auto-detected.
2 — nginx routing + reload (in the nc repo) ✅
Problem: the live site 502’d. Two causes:
nc/nginx/nginx.confstill proxiedskui.io→ the deadskui.io_site:80container.- After fixing the config, nginx kept serving the old in-memory config:
nginx.confis bind-mounted, sodocker compose up -d nginxis a no-op when only the file changed.
Fixes:
- Point the vhost at
skui_web:8099with lazy Docker DNS so nginx still boots if the upstream is briefly down:resolver 127.0.0.11 valid=30s; set $skui_upstream skui_web; proxy_pass http://$skui_upstream:8099; nc/.gitea/workflows/deploy.ymlnow runsnginx -t && nginx -s reloadafter copying the config (theup -dno-op was the silent-502 culprit).
3 — Change-password 403 / 502 ✅ (two layered bugs)
Reported: changing the admin password returned 403 bad csrf, then 502.
- Missing CSRF token —
password_pagewas the only admin form rendered without the hidden<input name=csrf>, so every POST failed the generic CSRF check. Added the token. - Latent crash, masked by #1 — once CSRF passed, the handler ran for the first time and
crashed:
require_user()returns the username string, but the handler indexed it asuser["username"]→TypeError→ 502. Use the string directly.
Verified non-destructively: a wrong current password now returns “Current password is incorrect” (200), not 403/502.
4 — Container dismiss (no auto re-add) ✅
Problem: the collector INSERT OR IGNOREs every live container every ~30 s, so deleting one
in admin just got re-added on the next push.
Fix: new tracked_containers.dismissed column (idempotent ALTER TABLE migration).
“Remove” now sets dismissed=1, enabled=0, public=0, auto=0 — a tombstone the push can’t
resurrect. Dismissed rows are filtered out of the admin list, status items, /api/*, and the
dashboard count. Verified across repeated pushes on a temp DB.
5 — Projects moved to the DB ✅
Goal: add/remove projects from admin instead of editing data/projects.yaml + rebuilding.
- New
projectstable (name, tagline, description, status, tags, url, sort, enabled), seeded from the former yaml (idempotent). GET /api/projects(public) +q_projects()helper (tags split from comma-separated → list).- Admin CRUD at
/admin/projects, placed between Notes and Working On (server nav + the public admin dropdown insidebar-hydrate.js). /projects/hydrates its cards from/api/projectsviasidebar-hydrate.js; the build-time yaml render stays as a no-JS fallback (DB is the source of truth).
Files touched
| Repo | Files |
|---|---|
skui.io | .gitea/workflows/build.yml, compose.yml, server.py, scripts/init_db.py, layouts/projects/list.html, assets/js/sidebar-hydrate.js |
nc | nginx/nginx.conf, .gitea/workflows/deploy.yml |
Follow-ups / notes
data/projects.yamlis now only a no-JS fallback; it will drift from the DB. Option to drop it and render an empty grid that JS fills.- Default admin password is still
skuiiountil changed via/admin/password.