check / check (push) Waiting to run
The shared files are the sneak/prompts copies at dd4027b, with this repository's own entries after them. make lint and make test each build one Dockerfile phase without the cache, both covering the frontend through a node stage; the builder stage waits on both and takes its version from git describe unless VERSION is given. 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, which the shared workflow no longer does. 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
151 lines
6.6 KiB
Docker
151 lines
6.6 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.
|
|
# golang:1.25.7-trixie, 2026-10-07
|
|
FROM golang@sha256:2b174ffcf56c7ad0c47d30d2630693265639ddf2a5141149c2da34db921791b4 AS test
|
|
WORKDIR /src
|
|
COPY backend/go.mod backend/go.sum ./
|
|
RUN go mod download
|
|
COPY backend/ .
|
|
RUN 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"]
|