This document is the shared contract between the server and the client (and between work packages). Any change here should be coordinated across both.
All request/response bodies are JSON unless noted otherwise.
- Auth is a single shared password (
ADMIN_PASSWORD), compared in constant time, no user accounts/database. - On successful login, the server sets a signed, httpOnly cookie named
dockpull_session(SameSite=Lax,Securewhen served over HTTPS,Max-Age=SESSION_TTLseconds). - Protected routes (everything except
/api/auth/loginand/api/health) require a validdockpull_sessioncookie. If it is missing, invalid, or expired, the server responds401 Unauthorizedwith{ "error": "unauthorized" }.
- Auth: none.
- Body:
{ "password": "string" } - Response:
200 { "ok": true }+Set-Cookie: dockpull_session=...on success.401 { "error": "invalid_password" }on bad password.429 { "error": "too_many_attempts" }after too many failed attempts from one client IP (temporary lockout).
- Auth: cookie.
- Body: none.
- Response:
200 { "ok": true }, clears thedockpull_sessioncookie.
- Auth: cookie (optional — never errors, reports status).
- Response:
200 { "authenticated": boolean }
- Auth: cookie.
- Response:
200— array of container items (shape below). Each item carries acomposeFileMissingflag the dashboard uses to warn when a stack's compose file isn't reachable inside the container (mount misconfigured).
- Auth: cookie.
- Body: none.
- Actively queries the registry for each running image's current digest and records/clears update events accordingly.
- Response:
200 { "total": n, "checked": n, "updatesFound": n, "errors": n }503 { "error": "docker_unavailable" }if the Docker daemon is unreachable.
- Auth: cookie.
- Response:
text/event-stream(SSE). Emitsdata: {"type":"containers-changed"}whenever server state changes (a check ran, an update finished, or a pin changed) so dashboards can refresh without a manual reload. Comment lines (: ...) are sent as keepalives.
- Auth: cookie.
- Path param:
name— container name. - Body: none.
- Response:
200 { "streamId": "string" }— starts a pull + recreate operation for that container; use the returnedstreamIdto subscribe to progress via the SSE endpoint below. - Errors:
404if no such container;409if an update is already in progress for that container. - Note: after
up -d, the container is health-checked; if it doesn't come up healthy the result is reported assuccess:falsewith an actionable message (and a rollback point is recorded).
- Auth: cookie.
- Path param:
name— container name. - Recreates the container from the image it ran before its last update (the
rollback point), then starts it. Same SSE streaming + result shape as an
update; subscribe via
GET /api/update/:name/stream. - Response:
200 { "streamId": "string" }. - Errors:
404 no_rollbackif there's nothing to revert to;404 not_foundif no such container;409if an update/revert is already in progress.
- Auth: cookie.
- Path param:
name— container name (same as used to start the update). - Response:
text/event-stream(SSE). Events:data: {"type":"log","line":"..."}— zero or more, streamed as the update runs (docker compose pull/up -doutput).data: {"type":"result","success":boolean,"message":"..."}— exactly one, final event; the stream closes after this.
- Auth: cookie.
- Query params:
container(optional, filter by container name),limit(default50),offset(default0). - Response:
200— array of update history rows, newest first:[ { "id": 1, "container_name": "nginx", "image": "nginx:latest", "old_digest": "sha256:...", "new_digest": "sha256:...", "status": "success", "message": "Updated successfully", "created_at": "2026-06-22 12:00:00" } ]
- Auth: cookie.
- Path param:
name— container name. - Query params:
limit(default50),offset(default0). - Response: same shape as
GET /api/history, filtered to that container.
- Auth: cookie.
- Deletes all update-history rows.
- Response:
200—{ "ok": true }.
- Auth: cookie.
- Response:
200— array of pinned refs, e.g.["nginx:latest", "redis:7"].
- Auth: cookie.
- Body:
{ "ref": "string" } - Response:
200 { "ok": true }. Idempotent.
- Auth: cookie.
- Path param:
ref— the image ref to unpin (URL-encoded). - Response:
200 { "ok": true }. Idempotent.
Note: refs passed to POST /api/pin and DELETE /api/pin/:ref are
normalized server-side (via the same normalizeRef used elsewhere) before
being stored/looked up, so e.g. raw nginx and
docker.io/library/nginx:latest are equivalent and GET /api/pinned
always returns normalized refs. Pinning ("Pin Version") holds a container at
its current version: it's never flagged for updates and is grouped into a
separate section, but can still be updated by hand.
- Auth: cookie.
- Response:
200— current settings, fully populated with defaults:{ "defaultFilter": "updates", "autoCheckOnOpen": true, "backgroundCheckEnabled": true, "scheduledCheckTime": "09:00", "discordEnabled": false, "discordWebhookUrl": "" }defaultFilter—"updates"or"all"; the view the dashboard opens in.autoCheckOnOpen— whether the dashboard runs a check automatically on first open.backgroundCheckEnabled— whether the server runs a scheduled check.scheduledCheckTime— daily local time (HH:MM) for the scheduled scan.discordEnabled— whether to send Discord notifications on new updates.discordWebhookUrl— Discord (or compatible) webhook URL, or"".
- Auth: cookie.
- Body: a partial patch of the settings object, e.g.
{ "defaultFilter": "all" }. Unknown keys are ignored; invalid values for known keys return400 { "error": "invalid_value" }. Changing the time/enable re-arms the background scheduler immediately. - Response:
200— the full, updated settings object.
- Auth: cookie.
- Body:
{ "url": "string" }(optional) — a webhook URL to test; falls back to the configureddiscordWebhookUrl. - Sends a one-off test message to the webhook.
- Response:
200 { "ok": true }on success;400 { "error": "no_webhook" }if no URL is configured;502 { "error": "webhook_failed" }if the webhook rejected the message.
- Auth: none.
- Response:
200 { "ok": true }.
{
"name": "nginx",
"project": "web",
"service": "nginx",
"image": "nginx:latest",
"tag": "latest",
"currentVersion": "1.27.3",
"sourceUrl": "https://github.com/nginx/nginx",
"currentDigest": "sha256:...",
"updateAvailable": true,
"availableDigest": "sha256:...",
"availableVersion": "1.27.4",
"pinned": false,
"state": "running",
"composeFile": "/opt/stacks/web/compose.yaml",
"composeFileMissing": false,
"workingDir": "/opt/stacks/web"
}Field notes:
name— Docker container name.project/service— derived from thecom.docker.compose.project/com.docker.compose.servicelabels.image— image ref as configured (tag, not digest).tag— the tag portion ofimage(e.g.latest,1.27), ornullif the ref is digest-pinned.currentVersion— human-readable version from the running image'sorg.opencontainers.image.versionlabel, if it sets one (elsenull).sourceUrl— source/project URL from the image'sorg.opencontainers.image.source(or.url) label, normalized to an https web URL (elsenull); used for the per-card changelog/source link.currentDigest— digest of the image the running container was created from.updateAvailable—trueif the most recent unresolved update event (from the registry check) for this image's normalized ref reports a digest different fromcurrentDigest.availableDigest— the digest from that unresolved event, if any (elsenull).availableVersion— a human version for the AVAILABLE (remote) image, resolved when the update was found (best-effort, elsenull). Prefers the image'sorg.opencontainers.image.versionlabel; when that isn't a usable version (e.g.main/latest/a sha) but the image declares a GitHub source, falls back to that repo's latest release tag.pinned—trueif the image ref is in thepinnedtable ("Pin Version": update indicator is suppressed and the container is grouped separately, but a manual update is still allowed).canRevert—trueif a rollback point exists (the container was updated and its previous image is remembered), so the UI can offer a one-click revert.rollbackVersion— the previous version label for that rollback point, ornull.state— Docker container state (running,exited, etc.).composeFile/workingDir— derived fromcom.docker.compose.project.config_files/com.docker.compose.project.working_dirlabels; used to rundocker composecommands for that container.composeFileMissing—truewhencomposeFileis set but not present inside the updater container (the same-path stacks mount is missing/wrong), so a compose update would fail; the dashboard surfaces a warning banner.
update_events rows are produced solely by the active registry check
(POST /api/check and the background scheduler). Each records the
normalized_ref and the registry-reported digest; a row is resolved once
the running container's digest matches it (the update was applied). There is
no external notifier — the app queries registries directly.