- TypeScript 77.9%
- Svelte 19.8%
- Shell 1.9%
- CSS 0.2%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| .clinerules | ||
| .cursor/rules | ||
| .forgejo | ||
| .github | ||
| .opencode | ||
| .windsurf/rules | ||
| docker | ||
| docs | ||
| packages | ||
| tools | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| deno.json | ||
| deno.lock | ||
| README.md | ||
| tsconfig.svelte.json | ||
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.