# 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 --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: ```env 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: ```env 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: ```env 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: ```text /status/ /public/api/status/ ``` 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: ```env 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: ```text 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: ```text 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: ```env 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 ```bash cp .env.example .env docker compose up -d --build ``` Runtime mounts normally include: ```text /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`. ```bash 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: ```text /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: ```text # 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.