From df80bc1fb685e43bb1727eeaadfc5057e3c5f158 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 07:56:04 +0000 Subject: [PATCH] docker: run as non-root, add HEALTHCHECK, document upaas deploy (closes #147) The image had never been run. A trial run (fresh named volume, port 8080, real targets, no notification endpoints) showed it exited at once: Viper searches the working directory and, with a YAML config type, also matches an extension-less file named dnswatcher, so the binary at /app/dnswatcher was parsed as a config file. The binary now lives in /usr/local/bin and the working directory is the data dir. The runtime stage also gains an unprivileged dnswatcher user (uid 10001) that owns /var/lib/dnswatcher, so a fresh named volume inherits writable ownership, and a Docker HEALTHCHECK that probes /.well-known/healthcheck with busybox wget. README gains a "Deploying with upaas" section. Model: opus-4-8 --- Dockerfile | 28 ++++++++++++++++++++++------ README.md | 29 +++++++++++++++++++++++++++++ TODO.md | 2 ++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 94b5b57..140ef3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,15 +41,31 @@ FROM alpine@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4 RUN apk add --no-cache ca-certificates tzdata -WORKDIR /app +# The binary lives in /usr/local/bin, not the working directory: config +# loading searches the working directory, and with a YAML config type Viper +# also matches an extension-less file named "dnswatcher" there, so a binary +# named "dnswatcher" in that directory would be parsed as a config file. +COPY --from=builder /src/bin/dnswatcher /usr/local/bin/dnswatcher -COPY --from=builder /src/bin/dnswatcher /app/dnswatcher - -# Create data directory -RUN mkdir -p /var/lib/dnswatcher +# Run as an unprivileged user. The data directory is owned by that user so +# writes succeed both on a bind mount and when Docker seeds a fresh named +# volume from the image (a fresh volume inherits this directory's ownership). +RUN addgroup -S dnswatcher \ + && adduser -S -G dnswatcher -u 10001 dnswatcher \ + && mkdir -p /var/lib/dnswatcher \ + && chown dnswatcher:dnswatcher /var/lib/dnswatcher ENV DNSWATCHER_DATA_DIR=/var/lib/dnswatcher +WORKDIR /var/lib/dnswatcher + +USER dnswatcher + EXPOSE 8080 -ENTRYPOINT ["/app/dnswatcher"] +# busybox wget (already in alpine) probes the health endpoint. PORT defaults +# to 8080 and is honoured if the operator overrides it. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null "http://127.0.0.1:${PORT:-8080}/.well-known/healthcheck" || exit 1 + +ENTRYPOINT ["/usr/local/bin/dnswatcher"] diff --git a/README.md b/README.md index c849041..a44dca1 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,35 @@ docker run -d \ --- +## Deploying with upaas + +upaas builds this repo's `Dockerfile` and runs the resulting image with the +volume, port, and environment variables the operator enters. Configure: + +- **Branch:** `main`. This is the recommended branch on the deployment + decision issue and is not yet confirmed by the owner. +- **Volume:** mount persistent storage at `/var/lib/dnswatcher`. The image runs + as an unprivileged user (`dnswatcher`, uid 10001) that owns this directory; a + fresh named volume inherits that ownership, and a bind-mounted host directory + must be writable by uid 10001. +- **Port:** the container listens on `8080`. Override with `PORT` if needed; + the healthcheck honours it. +- **Required environment:** `DNSWATCHER_TARGETS`, a comma-separated list of the + domains and hostnames to watch. The process refuses to start without it. +- **Recommended environment:** the notification endpoints + `DNSWATCHER_SLACK_WEBHOOK`, `DNSWATCHER_MATTERMOST_WEBHOOK`, and + `DNSWATCHER_NTFY_TOPIC` (without at least one, changes are only visible on the + dashboard); and `DNSWATCHER_METRICS_USERNAME` / `DNSWATCHER_METRICS_PASSWORD` + to enable the basic-auth-protected `/metrics` endpoint. +- **Healthcheck:** the image already declares a Docker `HEALTHCHECK` against + `/.well-known/healthcheck`; no operator configuration is needed. The same + path is available for an external probe. + +The dashboard is unauthenticated and shows every watched name and recent alert; +decide deliberately whether to expose it publicly. + +--- + ## Monitoring Lifecycle 1. **Startup**: Load state from disk. If no state file exists, start diff --git a/TODO.md b/TODO.md index b00f161..25aa29a 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,8 @@ Rationale, Design, TODO, License, Author) if any are still missing. # Completed Steps +- 2026-09-21: upaas deploy readiness — runtime image runs as unprivileged + `dnswatcher`, Docker `HEALTHCHECK`, README "Deploying with upaas" (closes #147). - 2026-09-21: `go mod tidy` dropped the redundant `golang.org/x/sync` `// indirect` line so `script/bootstrap` leaves a clean tree (#132) - 2026-08-10: comment-only corrections to `script/bootstrap`,