Files
dockwatch/README.md
T
jbergner ad54651558
release-tag / release-image (push) Successful in 2m13s
v9.3.3
2026-08-31 22:34:50 +02:00

18 KiB
Raw Blame History

Dockwatch v9.3.2

Go module: git.send.nrw/sendnrw/dockwatch

Dockwatch is a single-binary Go control plane for Docker Compose, Docker resources and uptime monitoring. It combines Dockge/Dockhand-style operational workflows with Uptime-Kuma-style monitoring while keeping its own implementation and UI.

The same binary runs as standalone, master or agent. SQLite uses modernc.org/sqlite, so the application itself builds with CGO_ENABLED=0.

What is included

Compose / stack management

  • multiple Compose stacks per environment
  • local Docker plus remote agents
  • staged docker compose config --quiet validation before a save
  • atomic managed-file writes and rollback on save failure
  • full bidirectional Compose designer backed by a YAML AST
  • recursive editing of all YAML maps/arrays/scalars, including unknown/future Compose fields
  • compose.yaml, root .env, envs/, secrets/ and configs/
  • Up / Start / Stop / Restart / Down / Pull / Update / Force recreate
  • service/container status
  • live SSE logs with pause, follow, filter and download
  • interactive PTY/WebSocket docker compose exec terminal
  • normalized service/dependency/network/volume graph from docker compose config --format json
  • registry digest based image-update checks
  • safe stack deletion: only Dockwatch-managed definition files are removed by default
  • explicit, typed-name confirmation for full stack-folder purge

The editor keeps unsaved drafts in browser storage. Background refresh never overwrites a dirty editor.

Git-backed stacks

Git sources support:

  • local or remote-agent environment
  • repository, branch, workdir and Compose filename
  • manual sync
  • optional deploy after sync
  • signed webhooks (X-Hub-Signature-256, X-Gitlab-Token or X-Webhook-Token)
  • encrypted webhook secret at rest
  • last commit / last sync / last error

A Git sync is now managed-file based. Dockwatch stores a small .dockwatch/git-manifest.json in the stack directory and only replaces/removes files that belong to the Git checkout. Unrelated runtime or bind-mount data beside the Compose project is preserved. Syncs of the same source are serialized and a failed apply rolls back the touched file set.

Symlink stack destinations and symlink paths inside Git-managed writes are rejected.

Docker resources

Containers

  • list/filter
  • start / stop / restart / force-remove
  • operator-only inspect
  • one-shot CPU/memory/network/block stats

Container identity / host UID-GID checks

Container rows include an Identity action. Dockwatch inspects the selected container and reports:

  • configured Compose/image user and the effective runtime UID/GID where resolvable
  • whether the process currently runs as UID 0
  • a conservative root assessment based on privileged, Docker socket mounts, passed-through devices and added Linux capabilities
  • bind-mount sources and their host UID/GID ownership when host access is configured
  • single-container detail checks plus a throttled Identity audit across all containers in the selected environment
  • whether the container UID/GID already maps to a local host account/group

Dockwatch deliberately does not claim that a root container can always be converted to non-root. Application-internal filesystem permissions, entrypoints and image-specific PUID/PGID conventions cannot be proven from Docker metadata alone. It also never rewrites Compose user: automatically.

A matching local host username is not required by Docker. Linux file ownership is numeric; creating a host account can nevertheless make bind-mount ownership, backups and administration easier.

Host inspection is opt-in. For read-only inspection, set:

HOST_ROOT=/host
ALLOW_HOST_USER_MANAGEMENT=false
ALLOW_HOST_PERMISSION_MANAGEMENT=false

and mount the host root read-only at /host. examples/compose-host-identity.override.yml shows this setup.

Admins may optionally create a locked/non-login local host account using the container's server-side re-resolved UID/GID. This requires both a writable host-root mount and:

ALLOW_HOST_USER_MANAGEMENT=true

See examples/compose-host-user-management.override.yml. The browser cannot supply an arbitrary UID/GID: Dockwatch re-inspects the container immediately before the change and derives the IDs itself. Existing numeric users/groups are reused and UID/name collisions are refused. Docker rootless/user-namespace remapping is detected where possible; automatic same-numbered host-account creation is refused when IDs are remapped. This operation is intentionally admin-only and disabled by default.

Identity & Bind Mount Permissions

Dockwatch can now diagnose the actual bind-mount permission problem instead of stopping at "container runs as UID X". The container Identity dialog and each stack's Permissions tab show, per bind mount:

  • effective PID 1 UID/GID and a separate expected bind UID/GID
  • PUID/PGID or USER_ID/GROUP_ID when the image exposes those paired conventions
  • host owner UID/GID and POSIX mode bits
  • static writeability (w+x for directories, w for files)
  • extended POSIX ACL detection when getfacl is available
  • an optional non-mutating runtime test -w using the expected numeric identity
  • read-only mounts, rootless/userns remapping and unsafe symlinked host paths as hard repair blockers

The repair flow is Analyze → Preview → Repair → Verify. For recursive ownership repair Dockwatch scans the tree first and shows how many files/directories differ. Automatic recursive repair is refused above 200,000 entries. Symlinks are never followed or chowned. :ro mounts are never repaired automatically.

Permission repair is a separate high-trust opt-in from host-user creation:

HOST_ROOT=/host
ALLOW_HOST_PERMISSION_MANAGEMENT=true

and / must intentionally be mounted read-write at /host. See examples/compose-host-permissions.override.yml. The browser supplies only the container and its mount destination (for example /config); the backend re-reads docker inspect and resolves the real host source itself. Arbitrary host paths and arbitrary UID/GID values cannot be submitted for repair.

Ownership repair can operate on only the bind root or recursively. chmod is separate, optional, explicit, and only applies to the bind root; Dockwatch never automatically applies chmod 777 and never recursively rewrites modes. After a repair Dockwatch re-runs the ownership/writeability analysis and reports the result.

Images

  • list/filter
  • pull / remove / prune
  • registry login/logout
  • persistent Docker auth via DOCKER_CONFIG=/data/docker-config

Volumes

  • list/filter
  • create / remove / prune
  • driver and labels

Networks

  • list/filter
  • create / remove / prune
  • driver, labels, internal, attachable

Docker positional arguments and Compose service names are validated before invoking the CLI so option-like values cannot be interpreted as Docker CLI flags.

Monitoring

Probe types:

  • HTTP(S)
  • TCP
  • DNS
  • Docker container running state
  • Docker container running + optional healthy requirement

Monitor lifecycle:

  • create / edit / delete
  • pause / resume
  • manual or timed maintenance
  • maintenance notes
  • manual Check now
  • interval and timeout
  • HTTP method and accepted status range
  • request headers/body
  • keyword / inverted keyword assertion
  • optional HTTP TLS verification bypass
  • local or remote-agent execution
  • heartbeat history
  • last latency/status message
  • 24-hour uptime
  • retention cleanup

The scheduler uses a lightweight monitor query and does not recompute 24-hour uptime statistics every two seconds. Manual checks are serialized with scheduled checks and do not accidentally resume paused/maintenance monitors. Shutdown-cancelled checks are not persisted as false outages.

Services and public status pages

Probes can be grouped into user-facing Services. Aggregation is strict:

  • any probe down -> Service down
  • otherwise maintenance -> Service maintenance
  • pending/paused states are retained
  • otherwise Service up

Service dialogs can assign/unassign probes directly.

Admins can publish selected Services on public pages:

/status/<slug>
/public/api/status/<slug>

The public JSON API uses a dedicated DTO and exposes only public status information (name, status, uptime, latency and last-check timestamp), not targets, node IDs, request headers, bodies or credentials. HTML/JSON status responses are not cached.

Notifications

Providers:

  • generic JSON webhook
  • ntfy
  • Gotify
  • SMTP

SMTP supports:

  • STARTTLS
  • implicit SSL/TLS
  • None
  • authentication on/off independently of TLS
  • optional certificate-verification bypass for private/self-signed infrastructure

SMTP connections inherit the notification deadline after connect as well, so a stalled server cannot leave a delivery goroutine hanging indefinitely. SMTP/password/token/secret values are AES-GCM encrypted at rest. Notification configuration is admin-only in both API and UI.

PocketID / OIDC and roles

Roles are mapped from OIDC/PocketID groups:

  • viewer – dashboards/status/inventory, sanitized monitor configuration
  • operator – stack configuration/logs/terminal, Docker actions, monitor/service operations, Git stacks
  • admin – environments, notifications, status-page administration and audit log

Viewer API responses do not expose monitor request headers/body/keywords. Stack definition detail and live logs require Operator access because .env, secrets and runtime logs may contain sensitive data.

Required OIDC setup:

AUTH_DISABLED=false
BASE_URL=https://dockwatch.example.com
OIDC_ISSUER=https://id.example.com
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
APP_SECRET=replace-with-at-least-32-random-characters
OIDC_ADMIN_GROUP=dockwatch-admins
OIDC_OPERATOR_GROUP=dockwatch-operators

Redirect URI:

https://dockwatch.example.com/auth/callback

OIDC discovery, token exchange and verification are bounded by HTTP_TIMEOUT_SECONDS. Session/state cookies are HttpOnly, SameSite=Lax and become Secure when BASE_URL uses HTTPS.

Master / agent

Modes:

APP_MODE=standalone
APP_MODE=master
APP_MODE=agent

An agent exposes only /agent/v1/* and requires an AGENT_TOKEN of at least 24 characters. Agent tokens are AES-GCM encrypted in the master's SQLite database.

Remote-capable features include:

  • stacks and actions
  • logs and PTY terminal
  • Compose graph
  • image update checks
  • containers/images/networks/volumes
  • container identity analysis, bind-mount permission repair and optional host-account creation on the agent host
  • monitoring probes
  • Git clone/sync/deploy

See examples/compose-master.yml and examples/compose-agent.yml.

Full Compose designer

The Compose tab uses two synchronized representations:

  1. the raw YAML source of truth
  2. a recursive visual designer

YAML changes are parsed after a short debounce. Visual changes patch the selected YAML AST path instead of reserializing a simplified Compose model. Unknown keys, x-* extensions and comments are therefore preserved where possible. Invalid/incomplete YAML pauses visual synchronization and never overwrites the source editor.

The designer is not limited to a hard-coded subset: arbitrary maps, arrays and scalar types remain editable, including current/future Compose fields and long syntax objects.

Server-side save validation still uses Docker Compose itself after all related .env, secret, env-file and config files have been staged.

Reliability and security work in v9 / v9.2

The v9.2 identity extension is opt-in, admin-gated and preserves the existing least-surprise rule: diagnostics are read-only by default and no container user or host account is changed automatically.

The v9 review includes, among other changes:

  • fixed fresh-database migration ordering before monitor indexes are created
  • SQLite WAL/busy-timeout/foreign-key configuration with a small connection pool
  • strict environment parsing instead of silently accepting invalid numeric/boolean values
  • graceful HTTP shutdown and sensible server timeouts
  • security headers and same-origin checks for browser mutations
  • request JSON size limit and single-value decoding
  • admin-only audit activity and notification configuration
  • audit retention cleanup
  • public-status DTO that does not leak internal monitor configuration
  • safe IP parsing for audit entries
  • sensitive viewer responses reduced
  • Compose save snapshot/rollback
  • safe stack delete vs. explicit purge
  • symlink protection on stack/Git file paths
  • atomic managed-file writes
  • Git sync serialization and managed-file manifest
  • Git copy file-descriptor leak fixed
  • Node URL validation rejects credentials/query/fragment
  • remote terminal WebSocket URL/query handling fixed
  • registry/Docker CLI positional-argument validation
  • scheduler query and shutdown behaviour improved
  • defensive frontend collection handling (null cannot break list pages)
  • admin-only navigation entries hidden for non-admin users
  • persisted dark/light theme and collapsible/mobile sidebar
  • API connectivity indicator and manual refresh
  • denser Docker/monitor/service tables and better empty/error states

Configuration

.env.example contains the defaults. Important values:

APP_MODE=standalone
LISTEN_ADDR=:8080
BASE_URL=http://localhost:8080
DATA_DIR=/data
STACKS_DIR=/stacks
AUTH_DISABLED=true
APP_SECRET=change-me-change-me-change-me-32bytes
CHECK_CONCURRENCY=8
CHECK_RETENTION_DAYS=30
HTTP_TIMEOUT_SECONDS=10
AUDIT_RETENTION_DAYS=180
HOST_ROOT=
ALLOW_HOST_USER_MANAGEMENT=false
ALLOW_HOST_PERMISSION_MANAGEMENT=false

AUTH_DISABLED=true is for local development only. Do not expose that configuration publicly.

Run with Docker Compose

cp .env.example .env
docker compose up -d --build

Runtime mounts normally include:

/data                       SQLite, Docker registry config
/stacks                     Compose projects
/var/run/docker.sock        Docker Engine access

Giving Dockwatch access to the Docker socket grants highly privileged control of that Docker host. Protect the UI and agent endpoint accordingly.

v9.3.1 build-context fix

v9.3 accidentally used the broad ignore pattern dockwatch in both .gitignore and .dockerignore. Because patterns without a slash match path components recursively, that could hide cmd/dockwatch/ from Git and from the Docker build context. v9.3.1 removes that pattern, writes local Makefile builds to bin/dockwatch, ignores only bin//dist/, and makes the Dockerfile fail early with a clear message if cmd/dockwatch/main.go is ever missing from the build context.

Build from source

The pinned OIDC/OAuth2 releases require Go 1.25. The Docker build uses golang:1.25-alpine.

go mod tidy
go test ./...
CGO_ENABLED=0 go build ./cmd/dockwatch

Build metadata can be injected through the supplied Makefile/Dockerfile (VERSION, COMMIT, BUILD_DATE) and is visible in /healthz and the UI.

Database

SQLite database:

/data/dockwatch.db

modernc.org/sqlite is used, so no SQLite CGO binding is required. Migrations are additive and include users/sessions, nodes, monitors/checks/services, public status pages, audit events, notification channels and Git sources.

Verification for this archive

The artifact-building environment cannot reach proxy.golang.org, so it cannot download the real external modules or generate a trustworthy go.sum here. The repository intentionally does not ship fake checksums or test stubs.

For quality control, the project is copied into a temporary test workspace where API-compatible local stubs replace only the external dependencies. Those stubs are not included in the ZIP. The checks used for v9 include:

# all packages type/compile checked with temporary external-module stubs
go test -run='^$' ./...

go test ./internal/config ./internal/gitops ./internal/httpapi \
        ./internal/monitor ./internal/nodes ./internal/notify ./internal/stacks

go vet ./...
go test -race ./internal/config ./internal/gitops ./internal/httpapi ./internal/monitor ./internal/nodes ./internal/notify ./internal/stacks
node --check web/app.js

The SQLite migration SQL is additionally smoke-tested against Python's SQLite engine because the temporary modernc.org/sqlite stub does not implement a real SQL driver.

On a normal networked development machine or during docker build, run go mod tidy && go test ./... once against the real pinned dependencies.

Module metadata / go mod tidy

The Docker builder runs go mod tidy after the full source tree has been copied and before compiling. This is intentional: Go 1.17+ module graph pruning may require transitive dependencies to be recorded as indirect requirements once the actual imported package graph is known. A checkout with an older/incomplete go.mod or without go.sum therefore still builds reproducibly in CI instead of failing late with updates to go.mod needed.

For repository maintenance, run:

go mod tidy
git add go.mod go.sum
git commit -m "chore: tidy Go modules"

CI can use make verify to fail when go.mod/go.sum are not committed in tidy form.

v9.3.2 module-build fix

v9.3.2 fixes Docker/CI builds that stopped at go: updates to go.mod needed; to update it: go mod tidy. The builder now runs go mod tidy after the complete source tree has been copied and before go build, so indirect requirements required by Go module graph pruning are materialized in the build stage. The Makefile also includes tidy and verify targets for maintaining committed go.mod/go.sum files.

Build-context note (v9.3.3)

Runtime directories in .dockerignore and .gitignore are root-anchored (/stacks/, /data/, /bin/, /dist/). This is intentional: unanchored patterns such as stacks/ also match the source package internal/stacks/ and can make Go try to resolve the project's own internal package as a remote module during go mod tidy.