Dockwatch v9.3.1
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 --quietvalidation 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/andconfigs/- 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 execterminal - 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-TokenorX-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/PGIDorUSER_ID/GROUP_IDwhen the image exposes those paired conventions- host owner UID/GID and POSIX mode bits
- static writeability (
w+xfor directories,wfor files) - extended POSIX ACL detection when
getfaclis available - an optional non-mutating runtime
test -wusing 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
healthyrequirement
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-> Servicedown - 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 configurationoperator– stack configuration/logs/terminal, Docker actions, monitor/service operations, Git stacksadmin– 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:
- the raw YAML source of truth
- 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 (
nullcannot 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.