Dockwatch v9.5
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.
v9.5 firewall-provider layer
- Host Security firewall management now uses a provider model: Auto detect / UFW / firewalld / native nftables
- Auto detect adopts an already-active UFW or firewalld frontend instead of refusing all changes simply because one exists
- multiple truly competing active frontends are still treated as a hard conflict
- UFW rules are added with deterministic
dockwatch:comments and Dockwatch removes only those managed rules; it never runsufw reset - firewalld uses the selected native zone and managed rich rules while preserving unrelated services, ports, sources and rich rules
- native nftables keeps the isolated
table inet dockwatchmodel and never flushes the global ruleset - existing provider state/rules are displayed read-only in the firewall editor before changes are applied
- provider selection, default inbound management, firewalld zone selection, ALLOW/DENY/REJECT/LIMIT rules and rule reordering are available in the unified UI
- provider changes retain the timed rollback/explicit commit safety model; rollback restores the previous Dockwatch-managed policy and provider default snapshot where applicable
- package installation/upgrade now installs the selected firewall frontend (
ufw,firewalldornftables) rather than always installing nftables
v9.4.1 fixes
- fixed monitor creation when no monitor is selected (
state.monitor == null) - static UI assets are served with
Cache-Control: no-store, preventing stale frontend code after upgrades - Compose visual-editor patches now use a real queue so rapid edits on different fields are not lost
- the Volumes section now reconciles every service mount with top-level
volumes:declarations - named volumes missing a top-level declaration can be added individually or with Add missing declarations
- bind mounts, anonymous mounts and dynamic mounts are shown in the reconciliation view but are never silently converted into named volumes
- unused top-level volume declarations are highlighted and can be removed explicitly without deleting the Docker volume itself
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.
Host Security layer
Dockwatch can optionally act as a host-security control plane for the selected local or remote-agent environment. The feature is disabled by default and uses three independent capabilities so that audit, configuration changes and package management can be granted separately.
The System → Host Security page provides a posture overview and managed workflows for:
Firewall (Auto / UFW / firewalld / nftables)
- Auto detect adopts the already-active host firewall frontend: UFW first when it is the sole active frontend, firewalld when it is the sole active frontend, otherwise native nftables is used when available
- an explicit provider can be selected when you intentionally want to migrate or standardize a host
- only genuinely competing active frontends are treated as conflicts; for example UFW + firewalld, or UFW + an already-loaded Dockwatch nftables table
- the editor shows installed/active frontends plus the provider's existing runtime rules/state before any mutation
- common managed policy supports trusted IPv4/IPv6 CIDRs and ordered TCP/UDP ALLOW / DENY / REJECT / LIMIT rules
- provider-global default inbound management is an explicit opt-in for UFW/firewalld; nftables keeps its default policy inside Dockwatch's own table
- UFW: Dockwatch uses native
ufwcommands, tags its rules with deterministicdockwatch:comments and removes only those tagged rules; it never runsufw resetand preserves foreign UFW rules - firewalld: Dockwatch targets a selected zone, uses native rich rules, and preserves unrelated zone services/ports/sources/rich rules; default zone target changes are opt-in
- nftables: Dockwatch continues to own only
table inet dockwatch, validates withnft -c, never runsflush ruleset, and does not alter Docker NAT/FORWARD chains - when an enabled policy is applied to an inactive UFW/firewalld provider, Dockwatch activates the selected frontend inside the rollback window; rollback restores the previous active state
- timed rollback (30–600 seconds, UI default 90 seconds) after apply; changes must be explicitly kept after management connectivity is verified
- package installation/upgrade uses the selected provider package instead of always installing nftables
- browser clients never submit arbitrary firewall command text; the backend renders and executes only the typed policy model
Fail2Ban
- package/runtime/boot status
- package installation/upgrade when separately enabled
- managed global ban/find/max-retry/backend settings
- ignore IP/CIDR list
- configurable jails with filter, port, backend, log path and per-jail retry threshold
- server-side
fail2ban-client -tvalidation before activation/reload - Dockwatch owns only
/etc/fail2ban/jail.d/dockwatch.local; distro and administrator configuration is preserved
Linux Audit (auditd)
- package/runtime/boot status
- managed watches for identity files, sudoers, SSH configuration, Docker socket/configuration, systemd units and kernel-module configuration
- additional explicitly configured file watches
- path/key/permission validation
augenrules --checkvalidation andaugenrules --loadactivation- Dockwatch owns only
/etc/audit/rules.d/90-dockwatch.rules; unrelated audit rules remain untouched
Installation and maintenance
- install or upgrade the selected firewall provider (
ufw,firewalldornftables),fail2banandauditd/auditusing the detected host package manager - supported package-manager families: apt, dnf, yum, zypper, apk and pacman
- enable, disable, restart and (where meaningful) reload the corresponding host services
- current package/service/config-drift information in the UI
- backups of Dockwatch-managed host configuration before replacement
- all mutation endpoints are administrator-only and flow through Dockwatch's existing origin/CSRF guard and activity audit log
Dockwatch intentionally manages its own drop-in/configuration scope rather than rewriting the host's entire security configuration. It is a focused administration layer, not a replacement for a complete CIS/STIG benchmark, SELinux/AppArmor policy management, an enterprise EDR, or distro-specific security tooling.
Capability modes
Read-only file/configuration audit:
services:
dockwatch:
environment:
HOST_ROOT: /host
HOST_SECURITY_ENABLED: "true"
ALLOW_HOST_SECURITY_CHANGES: "false"
ALLOW_HOST_PACKAGE_MANAGEMENT: "false"
volumes:
- /:/host:ro
A ready-to-use example is examples/compose-host-security-audit.override.yml. In this mode Dockwatch cannot mutate the host. Runtime/service checks that require entering the host namespaces may be unavailable and are reported as such.
Full host-security management is intentionally high privilege:
services:
dockwatch:
pid: host
privileged: true
environment:
HOST_ROOT: /host
HOST_SECURITY_ENABLED: "true"
ALLOW_HOST_SECURITY_CHANGES: "true"
ALLOW_HOST_PACKAGE_MANAGEMENT: "true"
HOST_SECURITY_HOST_PID: "1"
volumes:
- /:/host:rw
See examples/compose-host-security.override.yml. Dockwatch verifies that HOST_ROOT and /proc/<HOST_SECURITY_HOST_PID>/root refer to the same host before enabling its namespace executor. Host commands are executed through nsenter in the target host namespaces; the browser can select only predefined operations and cannot submit arbitrary shell commands.
Because this mode grants Dockwatch broad host-administration capability, use OIDC, restrict the admin role carefully, protect the Dockwatch database/APP_SECRET, and do not expose an agent token or the UI to untrusted networks.
The same security APIs are available through the existing master/agent relay, so each agent can audit/manage its own host when that agent has been configured with the appropriate host-security capability. A master does not silently inherit host privileges on an agent.
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
- host-security audit/management when explicitly enabled on that agent
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
HOST_SECURITY_ENABLED=false
ALLOW_HOST_SECURITY_CHANGES=false
ALLOW_HOST_PACKAGE_MANAGEMENT=false
HOST_SECURITY_HOST_PID=1
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 this source snapshot 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 ./internal/hostsecurity
go vet ./...
go test -race ./internal/config ./internal/gitops ./internal/httpapi ./internal/monitor ./internal/nodes ./internal/notify ./internal/stacks ./internal/hostsecurity
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.