No description
  • TypeScript 77.9%
  • Svelte 19.8%
  • Shell 1.9%
  • CSS 0.2%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Julian Imhof 5778aef2fb
All checks were successful
Build and Deploy / verify (push) Successful in 59s
Build and Deploy / build (push) Successful in 1m15s
fix(docker): reach the engine through the host-root bind
A single-file bind of /var/run/docker.sock pins the socket's inode. dockerd
recreates its socket on every restart, and with live-restore OpsDeck keeps
running across it, so every engine call failed with "fetch failed" until
OpsDeck was restarted by hand (storage measure, stack listing, events).

Prefer <hostRoot>/run/docker.sock when it is a socket: the host-root bind
is a directory and always shows the current one. An explicit unix://
DOCKER_HOST still wins, and the file mount stays as the fallback for
deployments without the host root. Never <hostRoot>/var/run — that is an
absolute symlink resolving to the container's own /run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:58:44 +01:00
.clinerules fix(docker): a fetch that starts after the caller, and ALREADY checks the repo 2026-08-10 15:24:31 +02:00
.cursor/rules docs: CLAUDE.md + per-agent caveman rule files 2026-07-11 22:02:53 +02:00
.forgejo deps: denoland/deno 2.9.6 -> 2.9.8, docker actions to latest 2026-10-28 00:11:32 +01:00
.github fix(docker): a fetch that starts after the caller, and ALREADY checks the repo 2026-08-10 15:24:31 +02:00
.opencode fix(docker): a fetch that starts after the caller, and ALREADY checks the repo 2026-08-10 15:24:31 +02:00
.windsurf/rules fix(docker): a fetch that starts after the caller, and ALREADY checks the repo 2026-08-10 15:24:31 +02:00
docker fix(docker): reach the engine through the host-root bind 2026-10-29 18:58:44 +01:00
docs Merge pull request 'fix(docker): skip locally built images on update; auto-update switch moves to env' (#74) from fix/docker-buildable-and-autoupdate-ui into main 2026-10-27 23:40:35 +01:00
packages fix(docker): reach the engine through the host-root bind 2026-10-29 18:58:44 +01:00
tools Merge pull request 'feat(docker): the docker module roadmap, end to end' (#66) from feat/docker-storage into main 2026-09-23 14:07:54 +02:00
.dockerignore fix(dev-rig): stop the rig misreporting the build, the tester and itself 2026-08-09 19:00:55 +02:00
.env.example build: typecheck the browser, verify it in CI, and version the singletons 2026-08-10 15:37:05 +02:00
.gitignore chore: ignore vite's .vite/ dep cache 2026-10-28 00:46:40 +01:00
AGENTS.md fix(docker): a fetch that starts after the caller, and ALREADY checks the repo 2026-08-10 15:24:31 +02:00
CLAUDE.md fix(docker): reach the engine through the host-root bind 2026-10-29 18:58:44 +01:00
deno.json deps: vite 7.3.7 -> 8.3.5, @sveltejs/vite-plugin-svelte 6.2.4 -> 7.3.1 2026-10-28 00:46:31 +01:00
deno.lock deps: vite 7.3.7 -> 8.3.5, @sveltejs/vite-plugin-svelte 6.2.4 -> 7.3.1 2026-10-28 00:46:31 +01:00
README.md feat(docker): update OpsDeck from OpsDeck through a sidecar container 2026-09-21 18:15:25 +02:00
tsconfig.svelte.json build: typecheck the browser, verify it in CI, and version the singletons 2026-08-10 15:37:05 +02:00

OpsDeck

Modular, self-hosted server dashboard. One Docker container; frontend and backend are separate components talking over a versioned API; functionality ships as modules that are toggled with environment variables.

Stack: Deno 2 + Hono (backend) · Svelte 5 (frontend) · DuckDB (metrics history) · OIDC (auth) · uPlot (charts).

Built-in modules

Module Shows
hardware specs, CPU/RAM/network/disk-IO/temperature history, filesystem usage, GPU utilization/VRAM/temp/power (NVIDIA via nvidia-smi, Intel/AMD via sysfs + DRM fdinfo)
zfs pools, capacity/fragmentation, vdev tree with errors, scrub status, SMART disk health
docker compose stacks, container state/config (env values redacted), image update checks against registries

Enable with OPSDECK_MODULES=hardware,zfs,docker.

External modules come from git: OPSDECK_EXTERNAL_MODULES=https://git.example.com/foo.git#<commit-sha> — cloned, validated and built at container startup, cached in the data volume. Pin by commit SHA; a module runs with the server's privileges (see docs/security.md).

Configuration

Every environment variable, with defaults and what happens when it is unset: docs/configuration.md. Module knobs take an OPSDECK_MOD_<MODULE>_ prefix — the docker module's measuring cadence is OPSDECK_MOD_DOCKER_STORAGE_INTERVAL_MS.

Deploy

cp .env.example .env   # fill in OIDC settings
docker compose -f docker/docker-compose.yml up -d --build

The compose file documents the required mounts (writable host root, docker.sock, data volume), pid: host, and the granular capability set — plus when privileged: true is the saner choice (docs/security.md has the threat model: the socket and host mount are root-equivalent either way).

Deployed this way, OpsDeck can update itself from its own stack page: a sidecar container recreates it with the host's docker compose, checks that the new container answers, and restores the previous image if it does not. That needs the stack to be compose-managed (labels on the container), the compose plugin on the host, and an image with a HEALTHCHECK or curl (the shipped one has both). A rollback leaves the compose file naming the version that failed; the notification says so.

Development (any OS)

deno task build        # shell + shared singleton bundles + module frontends
deno task start        # serve on :8080
deno task test packages/server/tests/

Set OPSDECK_AUTH=disabled and OPSDECK_FAKE_DATA=1 to develop without an IdP or a Linux host — every module has a synthetic data source. For the docker module on Windows, set DOCKER_HOST=tcp://localhost:2375 (Docker Desktop → "Expose daemon") and OPSDECK_HOST_ROOT=/ — the host-root prefix defaults to /host/root, a container mount that does not exist on a checkout, and drive-letter compose paths are refused under it rather than joined into a path that cannot exist. That fixes the paths, and reads genuinely work after it; whether anything can run a COMMAND on the host is a second question. A host root of / says the process is already there, so host exec tries only its direct candidate — no chroot, no nsenter — and proves it by running an sh -c test through it. With a sh.exe on PATH (Git for Windows with the Unix tools, MSYS2) that test passes and host commands run against the Windows docker CLI; with no sh at all it fails and the strategy stays unset. What that costs differs by feature: a stack update never fails over it — the updater probes first and degrades to the engine-API recreate, logging compose unavailable … — using the engine API — while a pin's compose validation and the git commit it makes run through host exec directly and fail with host command execution unavailable. Browsing stacks, files and logs never shells out and works either way; a pin without a shell fails at its validate step and the compensator rolls the edit back. Frontend hot-reload: deno task dev (proxies /api to :8080, so run deno task dev:server too). Note: module frontends are loaded from built files even in dev — rerun deno task build:modules after editing them.

Architecture

packages/
  sdk/             @opsdeck/sdk — the module contract (backend ctx + frontend types)
  server/          Hono app: module host, OIDC sessions, DuckDB metrics, SSE bus
  ui/              @opsdeck/ui — Svelte 5 component library + design tokens
  shell/           SPA shell: router, nav, dynamic module loader
  module-builder/  compiles module frontends against the shared singletons
  modules/         built-in modules (hardware, zfs, docker, demo)

Module frontends never bundle Svelte or the UI library. One pinned Svelte version inside the container compiles everything; builds externalize svelte, @opsdeck/ui and @opsdeck/sdk/client, and a browser import map resolves them to shared singleton bundles under /shared/. That keeps styling consistent and reactivity working across independently-built modules.

Metrics flow: module collectors → buffered writer → DuckDB samples (raw, 48 h) with 5-minute (30 d) and hourly (2 y) rollups → /api/core/metrics/query for charts, plus live values over SSE (/api/core/events).

Writing your own module: docs/modules.md. Mobile alert pushes (privacy-preserving FCM): docs/push-notifications.md. How CI actions and base images are kept current: docs/dependencies.md.