Owner ruling on #25: one service, one port, one container. Inbound to nginx; nginx serves the built frontend and proxies a route to the Go backend running in the same container. Today there are two images (Dockerfile, Dockerfile.backend) with no network path between them, and nginx.conf has no proxy_pass.
Definition of done
One root Dockerfile builds the single shipping image; Dockerfile.backend is deleted. Stages follow the REPO_POLICIES Go pattern: a lint stage on the digest-pinned golangci/golangci-lint image (keep the pin already on next) running the backend format check and lint; a Go build stage that depends on the lint stage via COPY --from=lint, runs the backend tests and builds the static binary with ARG VERSION; a node stage that runs the frontend check and produces dist/; a runtime stage on the digest-pinned nginx alpine image already in use, containing nginx, dist/, the Go binary and the entrypoint. Every base image pinned by digest with a version+date comment.
Inside the container the backend listens on loopback only (127.0.0.1) on an internal port that is not the public port. The bind address is a config setting next to PORT in backend/internal/config/config.go (viper SetDefault + AutomaticEnv), documented in backend/README.md.
nginx listens on the public port (8080) and proxies /api/ and /.well-known/healthcheck to the backend, setting Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto. Everything else is served from dist/ as today (try_files for / stays; /assets/ caching stays).
Process supervision is a small POSIX sh entrypoint: start the backend, start nginx, forward SIGTERM/SIGINT to both, and exit non-zero as soon as either process exits so the platform restarts the container instead of leaving it half-alive. Plain trap, background jobs, wait, explicit PIDs. No third-party supervisor unless the script provably cannot be made correct; any added binary is a hash-verified download, never an unpinned package.
Verified and summarised in the PR (no logs): kill the backend inside a running container and the container exits non-zero; same for nginx; docker stop delivers TERM to both, the backend flushes its buffer, exit status 0. Build the image, run it, curl / returns the page, curl -X POST /api/v1/reports with a valid body returns the backend's JSON through nginx, /.well-known/healthcheck answers through nginx.
DATA_DIR defaults to a directory under /data in the image, declared with VOLUME, writable by the user the backend runs as. Say in the PR which user each process runs as.
script/cibuild, script/docker and .gitea/workflows/check.yml build exactly one image. script/docker becomes byte-identical to the org model at https://git.eeqj.de/sneak/prompts/raw/branch/main/script/docker (the divergence #28 lists; consolidation is the moment to fix it).
backend/README.md and the root README.md Deployment section describe the single image: public port, the internal backend port, DATA_DIR and the volume, the proxied paths. Only what changed; #24 does the full rewrite.
Root make check green; docker build . green twice in a row on the unchanged tree. TODO.md updated in the same commit. Commit title ends (closes #N).
Implementation requirements
Base on current next. If #38 has landed, build on its split (node stage runs make frontend-check; lint and Go stages run the backend/script/* entrypoints). If it has not landed, use what next has and do not reimplement it.
Do not add security headers or the PORT-driven nginx listen port here; those are #18 and #26 and land on top. Choose the entrypoint so that #26's envsubst restricted to ${PORT} can be added without redesign, and say how in the PR.
Do not touch src/main.js; the reporting client is its own issue.
The backend's PORT default stays 8080 for standalone runs; the image passes the internal port explicitly.
No scripted edits (sed -i, perl -pi, awk, python rewrites); edit files by hand, format only via make fmt. make targets and script/ entrypoints only. No attribution trailers.
model: claude-fable-5
## Why
Owner ruling on https://git.eeqj.de/sneak/netwatch/issues/25: one service, one port, one container. Inbound to nginx; nginx serves the built frontend and proxies a route to the Go backend running in the same container. Today there are two images (`Dockerfile`, `Dockerfile.backend`) with no network path between them, and `nginx.conf` has no `proxy_pass`.
## Definition of done
- [ ] One root `Dockerfile` builds the single shipping image; `Dockerfile.backend` is deleted. Stages follow the REPO_POLICIES Go pattern: a lint stage on the digest-pinned `golangci/golangci-lint` image (keep the pin already on `next`) running the backend format check and lint; a Go build stage that depends on the lint stage via `COPY --from=lint`, runs the backend tests and builds the static binary with `ARG VERSION`; a node stage that runs the frontend check and produces `dist/`; a runtime stage on the digest-pinned nginx alpine image already in use, containing nginx, `dist/`, the Go binary and the entrypoint. Every base image pinned by digest with a version+date comment.
- [ ] Inside the container the backend listens on loopback only (`127.0.0.1`) on an internal port that is not the public port. The bind address is a config setting next to `PORT` in `backend/internal/config/config.go` (viper `SetDefault` + `AutomaticEnv`), documented in `backend/README.md`.
- [ ] nginx listens on the public port (8080) and proxies `/api/` and `/.well-known/healthcheck` to the backend, setting `Host`, `X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto`. Everything else is served from `dist/` as today (`try_files` for `/` stays; `/assets/` caching stays).
- [ ] Process supervision is a small POSIX `sh` entrypoint: start the backend, start nginx, forward SIGTERM/SIGINT to both, and exit non-zero as soon as either process exits so the platform restarts the container instead of leaving it half-alive. Plain `trap`, background jobs, `wait`, explicit PIDs. No third-party supervisor unless the script provably cannot be made correct; any added binary is a hash-verified download, never an unpinned package.
- [ ] Verified and summarised in the PR (no logs): kill the backend inside a running container and the container exits non-zero; same for nginx; `docker stop` delivers TERM to both, the backend flushes its buffer, exit status 0. Build the image, run it, `curl /` returns the page, `curl -X POST /api/v1/reports` with a valid body returns the backend's JSON through nginx, `/.well-known/healthcheck` answers through nginx.
- [ ] `DATA_DIR` defaults to a directory under `/data` in the image, declared with `VOLUME`, writable by the user the backend runs as. Say in the PR which user each process runs as.
- [ ] `script/cibuild`, `script/docker` and `.gitea/workflows/check.yml` build exactly one image. `script/docker` becomes byte-identical to the org model at https://git.eeqj.de/sneak/prompts/raw/branch/main/script/docker (the divergence https://git.eeqj.de/sneak/netwatch/issues/28 lists; consolidation is the moment to fix it).
- [ ] `backend/README.md` and the root `README.md` Deployment section describe the single image: public port, the internal backend port, `DATA_DIR` and the volume, the proxied paths. Only what changed; https://git.eeqj.de/sneak/netwatch/issues/24 does the full rewrite.
- [ ] Root `make check` green; `docker build .` green twice in a row on the unchanged tree. `TODO.md` updated in the same commit. Commit title ends ` (closes #N)`.
## Implementation requirements
- Base on current `next`. If https://git.eeqj.de/sneak/netwatch/pulls/38 has landed, build on its split (node stage runs `make frontend-check`; lint and Go stages run the `backend/script/*` entrypoints). If it has not landed, use what `next` has and do not reimplement it.
- Do not add security headers or the `PORT`-driven nginx listen port here; those are https://git.eeqj.de/sneak/netwatch/issues/18 and https://git.eeqj.de/sneak/netwatch/issues/26 and land on top. Choose the entrypoint so that #26's `envsubst` restricted to `${PORT}` can be added without redesign, and say how in the PR.
- Do not touch `src/main.js`; the reporting client is its own issue.
- The backend's `PORT` default stays 8080 for standalone runs; the image passes the internal port explicitly.
- No scripted edits (`sed -i`, `perl -pi`, awk, python rewrites); edit files by hand, format only via `make fmt`. `make` targets and `script/` entrypoints only. No attribution trailers.
model: claude-fable-5
clawbot
added this to the 1.0.0 milestone 2026-09-21 09:47:41 +02:00
clawbot
self-assigned this 2026-09-21 09:47:41 +02:00
Brief update: what #38 left on next, which this builds on.
Root make lint runs the Go linter only in Docker, by building the lint stage of Dockerfile.backend (--target lint --no-cache --output type=cacheonly). When Dockerfile.backend is deleted, script/lint builds the lint stage of the root Dockerfile the same way; no host path to golangci-lint returns.
The node stage runs make frontend-check; the Go stages run the backend's make targets, which shim to backend/script/*; backend/script/build reads VERSION from the environment.
script/cibuild and script/docker each build the one image; script/docker byte-identical to the org model.
Verification adds one browser check, done in a headless browser in a container: load the page from the running image over plain HTTP and confirm its report reaches the backend through nginx.
#26 (PORT for nginx) and #59 (health check, settings checked at start, upaas README section) land on top; do not do them here.
Model: opus-5-5
Brief update: what https://git.eeqj.de/sneak/netwatch/pulls/38 left on `next`, which this builds on.
- Root `make lint` runs the Go linter only in Docker, by building the lint stage of `Dockerfile.backend` (`--target lint --no-cache --output type=cacheonly`). When `Dockerfile.backend` is deleted, `script/lint` builds the lint stage of the root `Dockerfile` the same way; no host path to golangci-lint returns.
- The node stage runs `make frontend-check`; the Go stages run the backend's `make` targets, which shim to `backend/script/*`; `backend/script/build` reads `VERSION` from the environment.
- `script/cibuild` and `script/docker` each build the one image; `script/docker` byte-identical to the org model.
- Verification adds one browser check, done in a headless browser in a container: load the page from the running image over plain HTTP and confirm its report reaches the backend through nginx.
- https://git.eeqj.de/sneak/netwatch/issues/26 (`PORT` for nginx) and https://git.eeqj.de/sneak/netwatch/issues/59 (health check, settings checked at start, upaas README section) land on top; do not do them here.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
Owner ruling on #25: one service, one port, one container. Inbound to nginx; nginx serves the built frontend and proxies a route to the Go backend running in the same container. Today there are two images (
Dockerfile,Dockerfile.backend) with no network path between them, andnginx.confhas noproxy_pass.Definition of done
Dockerfilebuilds the single shipping image;Dockerfile.backendis deleted. Stages follow the REPO_POLICIES Go pattern: a lint stage on the digest-pinnedgolangci/golangci-lintimage (keep the pin already onnext) running the backend format check and lint; a Go build stage that depends on the lint stage viaCOPY --from=lint, runs the backend tests and builds the static binary withARG VERSION; a node stage that runs the frontend check and producesdist/; a runtime stage on the digest-pinned nginx alpine image already in use, containing nginx,dist/, the Go binary and the entrypoint. Every base image pinned by digest with a version+date comment.127.0.0.1) on an internal port that is not the public port. The bind address is a config setting next toPORTinbackend/internal/config/config.go(viperSetDefault+AutomaticEnv), documented inbackend/README.md./api/and/.well-known/healthcheckto the backend, settingHost,X-Real-IP,X-Forwarded-ForandX-Forwarded-Proto. Everything else is served fromdist/as today (try_filesfor/stays;/assets/caching stays).shentrypoint: start the backend, start nginx, forward SIGTERM/SIGINT to both, and exit non-zero as soon as either process exits so the platform restarts the container instead of leaving it half-alive. Plaintrap, background jobs,wait, explicit PIDs. No third-party supervisor unless the script provably cannot be made correct; any added binary is a hash-verified download, never an unpinned package.docker stopdelivers TERM to both, the backend flushes its buffer, exit status 0. Build the image, run it,curl /returns the page,curl -X POST /api/v1/reportswith a valid body returns the backend's JSON through nginx,/.well-known/healthcheckanswers through nginx.DATA_DIRdefaults to a directory under/datain the image, declared withVOLUME, writable by the user the backend runs as. Say in the PR which user each process runs as.script/cibuild,script/dockerand.gitea/workflows/check.ymlbuild exactly one image.script/dockerbecomes byte-identical to the org model at https://git.eeqj.de/sneak/prompts/raw/branch/main/script/docker (the divergence #28 lists; consolidation is the moment to fix it).backend/README.mdand the rootREADME.mdDeployment section describe the single image: public port, the internal backend port,DATA_DIRand the volume, the proxied paths. Only what changed; #24 does the full rewrite.make checkgreen;docker build .green twice in a row on the unchanged tree.TODO.mdupdated in the same commit. Commit title ends(closes #N).Implementation requirements
next. If #38 has landed, build on its split (node stage runsmake frontend-check; lint and Go stages run thebackend/script/*entrypoints). If it has not landed, use whatnexthas and do not reimplement it.PORT-driven nginx listen port here; those are #18 and #26 and land on top. Choose the entrypoint so that #26'senvsubstrestricted to${PORT}can be added without redesign, and say how in the PR.src/main.js; the reporting client is its own issue.PORTdefault stays 8080 for standalone runs; the image passes the internal port explicitly.sed -i,perl -pi, awk, python rewrites); edit files by hand, format only viamake fmt.maketargets andscript/entrypoints only. No attribution trailers.model: claude-fable-5
clawbot referenced this issue2026-09-28 19:01:51 +02:00
Brief update: what #38 left on
next, which this builds on.make lintruns the Go linter only in Docker, by building the lint stage ofDockerfile.backend(--target lint --no-cache --output type=cacheonly). WhenDockerfile.backendis deleted,script/lintbuilds the lint stage of the rootDockerfilethe same way; no host path to golangci-lint returns.make frontend-check; the Go stages run the backend'smaketargets, which shim tobackend/script/*;backend/script/buildreadsVERSIONfrom the environment.script/cibuildandscript/dockereach build the one image;script/dockerbyte-identical to the org model.PORTfor nginx) and #59 (health check, settings checked at start, upaas README section) land on top; do not do them here.Model: opus-5-5