check / check (push) Waiting to run
The shared files are the sneak/prompts copies at dd4027b, plus this repository's own entries. make lint and make test each build one Dockerfile phase without the cache, both covering the frontend; the builder stage waits on both and takes its version from git describe unless VERSION is given. The test phase keeps Go's module and build caches in memory, out of the image make test tags. golangci-lint moves to v2.14.0 with the new .golangci.yml; one test spells X-Request-ID as canonicalheader asks. prettier formats only JavaScript, CSS, HTML and Markdown, so .golangci.yml stays as fetched. script/fmt and script/fmt-check put ~/.local/bin on PATH. script/bootstrap keeps a Go only if it is exactly GO_VERSION, and re-checks the go on PATH after installing. Model: opus-5-5
155 lines
6.9 KiB
Docker
155 lines
6.9 KiB
Docker
# The one image netwatch ships: nginx serves the built frontend and
|
|
# passes /api/, /.well-known/healthcheck and /metrics to netwatch-server,
|
|
# the Go backend, which runs in the same container on loopback only.
|
|
# bin/entrypoint.sh starts and watches both.
|
|
#
|
|
# The lint and test phases are the gates: `make lint` (script/lint)
|
|
# builds the lint stage alone and `make test` (script/test) the test
|
|
# stage alone, and the builder stage depends on both, so the image
|
|
# cannot be built unless they pass. Each covers the frontend as well,
|
|
# through a copy from a node stage. Inside them each tool is invoked
|
|
# directly, never through make or script/, whose lint and test are
|
|
# themselves docker builds.
|
|
|
|
# Frontend lint stage: eslint with the rules in eslint.config.js. The
|
|
# lint phase below runs it.
|
|
# node:22-alpine as of 2026-02-22
|
|
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS frontend-lint
|
|
WORKDIR /app
|
|
COPY package.json yarn.lock ./
|
|
RUN yarn install --frozen-lockfile
|
|
COPY . .
|
|
RUN yarn eslint .
|
|
|
|
# Lint phase: golangci-lint over the backend with backend/.golangci.yml,
|
|
# and eslint through the copy from frontend-lint at the end. The
|
|
# golangci/golangci-lint image ships Go and the linter.
|
|
# golangci/golangci-lint:v2.14.0, 2026-09-24
|
|
FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
|
WORKDIR /src
|
|
COPY backend/go.mod backend/go.sum ./
|
|
RUN go mod download
|
|
COPY backend/ .
|
|
RUN golangci-lint run --config .golangci.yml ./...
|
|
# Nothing is wanted from frontend-lint; the copy is what makes this
|
|
# phase run it.
|
|
COPY --from=frontend-lint /app/yarn.lock /dev/null
|
|
|
|
# Frontend stage: the unit tests in test/unit/, then the production
|
|
# build into dist/, which the runtime stage serves. The test phase below
|
|
# runs it. The tests print a dot each; if any fails, they run again with
|
|
# every test listed, and the step fails even if that run passes.
|
|
# NODE_OPTIONS chooses the reporter because yarn adds its arguments
|
|
# after the test files, where node would take a reporter option for one
|
|
# more file.
|
|
# node:22-alpine as of 2026-02-22
|
|
FROM node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34 AS frontend
|
|
WORKDIR /app
|
|
COPY package.json yarn.lock ./
|
|
RUN yarn install --frozen-lockfile
|
|
# vite.config.js reads the commit for the page's footer with git.
|
|
RUN apk add --no-cache git
|
|
COPY . .
|
|
RUN NODE_OPTIONS=--test-reporter=dot timeout 90 yarn --silent run test || \
|
|
{ echo "--- Rerunning with every test listed for details ---"; \
|
|
NODE_OPTIONS=--test-reporter=spec timeout 90 yarn --silent run test; \
|
|
exit 1; }
|
|
RUN yarn build
|
|
|
|
# Test phase: the backend's tests with the race detector and coverage,
|
|
# and the frontend's through the copy from the frontend stage at the
|
|
# end. -race needs cgo and so a C compiler, which the Debian Go image
|
|
# ships and the alpine one does not. -timeout 90s is a backstop above
|
|
# the 60-second cap on the suite. The rerun with -v only shows details:
|
|
# the step fails however it ends, because the first run already failed.
|
|
# Go's module and build caches are in memory (tmpfs) for the go test
|
|
# step alone, so the image make test tags does not carry them and the
|
|
# build spends no time writing them into it. They start empty on every
|
|
# build, so no stored test result can stand in for a run.
|
|
# golang:1.25.7-trixie, 2026-10-07
|
|
FROM golang@sha256:2b174ffcf56c7ad0c47d30d2630693265639ddf2a5141149c2da34db921791b4 AS test
|
|
WORKDIR /src
|
|
COPY backend/ .
|
|
RUN --mount=type=tmpfs,target=/go/pkg/mod \
|
|
--mount=type=tmpfs,target=/root/.cache/go-build \
|
|
go test -timeout 90s -race -cover ./... || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
go test -timeout 90s -race -v ./...; exit 1; }
|
|
# Nothing is wanted from the frontend stage; the copy is what makes
|
|
# this phase run its tests.
|
|
COPY --from=frontend /app/yarn.lock /dev/null
|
|
|
|
# Backend build stage. Nothing is wanted from the two phases; the copies
|
|
# are what make BuildKit build them first, so this stage cannot run
|
|
# unless lint and test passed.
|
|
# golang:1.25-alpine (2026-02-27)
|
|
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
RUN apk add --no-cache git
|
|
# A tar-stream context keeps the sender's file owners, which git refuses.
|
|
RUN git config --system --add safe.directory /src
|
|
WORKDIR /src
|
|
COPY backend/go.mod backend/go.sum backend/
|
|
RUN cd backend && go mod download
|
|
COPY . .
|
|
|
|
# backend/script/build is the one definition of the build command:
|
|
# CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=..."
|
|
# It reads VERSION from the environment.
|
|
#
|
|
# The version is the VERSION build argument when one is given, otherwise
|
|
# `git describe --tags --always` on the .git in the build context: the
|
|
# tag on a tagged commit, tag-N-gHASH on a commit after one, the short
|
|
# commit when no tag is reachable. With .git present, a version that is
|
|
# still empty, dev or unknown fails the build: git is missing or could
|
|
# not read the checkout.
|
|
ARG VERSION
|
|
RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|
if [ -e .git ]; then \
|
|
case "$version" in ""|dev|unknown) \
|
|
echo "version is '$version' although .git is present" >&2; \
|
|
exit 1 ;; \
|
|
esac; \
|
|
fi; \
|
|
VERSION="$version" backend/script/build
|
|
|
|
# Runtime stage, and the last one: a plain `docker build .` builds it
|
|
# and the stages it copies from, the two phases included.
|
|
# nginx:stable-alpine as of 2026-02-22
|
|
FROM nginx@sha256:15e96e59aa3b0aada3a121296e3bce117721f42d88f5f64217ef4b18f458c6ab
|
|
|
|
# netwatch-server runs as this user, which owns the report directory.
|
|
# nginx keeps the image's own arrangement: its main process runs as
|
|
# root, its worker processes as the nginx user.
|
|
RUN addgroup -g 1000 -S netwatch && \
|
|
adduser -u 1000 -S netwatch -G netwatch
|
|
|
|
# At start-up the nginx image renders every template here into
|
|
# conf.d; bin/entrypoint.sh says how.
|
|
RUN rm /etc/nginx/conf.d/default.conf
|
|
COPY nginx.conf /etc/nginx/templates/netwatch.conf.template
|
|
COPY security-headers.conf /etc/nginx/security-headers.conf
|
|
COPY --from=frontend /app/dist /usr/share/nginx/html
|
|
COPY --from=builder /src/backend/netwatch-server /usr/local/bin/netwatch-server
|
|
COPY bin/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
|
|
# bin/entrypoint.sh creates DATA_DIR at start and gives it and /data to
|
|
# the netwatch user, whatever is mounted there.
|
|
ENV DATA_DIR=/data/reports
|
|
VOLUME /data
|
|
|
|
# The default public port; PORT changes it.
|
|
EXPOSE 8080
|
|
|
|
# Requests the backend's health check through nginx, on the port from
|
|
# PORT, so it fails unless both answer. upaas reads the result 60
|
|
# seconds after a deploy and fails the deploy unless it is healthy.
|
|
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"
|
|
|
|
# The nginx image stops its container with SIGQUIT; the entrypoint
|
|
# acts on TERM and INT.
|
|
STOPSIGNAL SIGTERM
|
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|