check / check (push) Waiting to run
On sneak's rulings in sneak/prompts#113 and sneak/prompts#130. REPO_POLICIES.md and script/test, script/lint, script/docker and script/cibuild are the sneak/prompts copies at 0653579: the scripts pass a new CACHEBUST value instead of --no-cache, and the lint and test builds write no image. In each Dockerfile stage the system packages and the Go module download come above ARG CACHEBUST; every step that lints, tests or compiles, and the node stages' yarn install, comes after it. Go's build cache and golangci-lint's are cache mounts, and both go test runs carry -count=1. README.md and TODO.md say the same. Model: opus-5-5
183 lines
8.0 KiB
Docker
183 lines
8.0 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 two build stages, frontend-build and builder,
|
|
# depend on both, so the image cannot be built unless they pass. Each
|
|
# phase 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.
|
|
#
|
|
# In every stage ARG CACHEBUST comes right after the steps that install
|
|
# system packages and download Go's modules, or right after FROM in a
|
|
# stage with neither, and every step that lints, tests or compiles
|
|
# comes after it, as does yarn install in the node stages. script/
|
|
# passes a new value on every build, which reruns every RUN step after
|
|
# it. Docker keeps the layers above it until the base image, their own
|
|
# lines or go.mod and go.sum change. The steps that compile mount Go's
|
|
# build cache, and golangci-lint run also mounts golangci-lint's; both
|
|
# survive from build to build.
|
|
|
|
# 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
|
|
ARG CACHEBUST
|
|
WORKDIR /app
|
|
COPY package.json yarn.lock ./
|
|
RUN yarn install --frozen-lockfile
|
|
COPY . .
|
|
RUN yarn eslint .
|
|
|
|
# Lint phase: golangci-lint over the Go packages with .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 go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY . .
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
--mount=type=cache,target=/root/.cache/golangci-lint \
|
|
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 test stage: the unit tests in test/unit/. The test phase
|
|
# below runs it, and the frontend-build stage starts from 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-test
|
|
ARG CACHEBUST
|
|
WORKDIR /app
|
|
COPY package.json yarn.lock ./
|
|
RUN yarn install --frozen-lockfile
|
|
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; }
|
|
|
|
# Test phase: the backend's tests with the race detector and coverage,
|
|
# and the frontend's through the copy from the frontend-test 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.
|
|
# -count=1 because the mounted cache also holds test results, which go
|
|
# test would otherwise replay.
|
|
# golang:1.25.7-trixie, 2026-10-07
|
|
FROM golang@sha256:2b174ffcf56c7ad0c47d30d2630693265639ddf2a5141149c2da34db921791b4 AS test
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY . .
|
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
|
go test -count=1 -timeout 90s -race -cover ./... || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
go test -count=1 -timeout 90s -race -v ./...; exit 1; }
|
|
# Nothing is wanted from the frontend-test stage; the copy is what makes
|
|
# this phase run its tests.
|
|
COPY --from=frontend-test /app/yarn.lock /dev/null
|
|
|
|
# Frontend build stage: the production build into dist/, which the
|
|
# runtime stage serves, on the yarn packages and source of the
|
|
# frontend-test 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.
|
|
FROM frontend-test AS frontend-build
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
# vite.config.js reads the commit for the page's footer with git.
|
|
RUN apk add --no-cache git
|
|
ARG CACHEBUST
|
|
RUN yarn build
|
|
|
|
# Backend build stage. Nothing is wanted from the two phases; the copies
|
|
# are what make BuildKit build them first, so nothing after them can
|
|
# run unless lint and test passed.
|
|
# golang:1.25-alpine (2026-02-27)
|
|
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
|
|
RUN apk add --no-cache git
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
ARG CACHEBUST
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
# A tar-stream context keeps the sender's file owners, which git refuses.
|
|
RUN git config --system --add safe.directory /src
|
|
COPY . .
|
|
|
|
# The same go build as in script/build, which builds the binary on the
|
|
# host.
|
|
#
|
|
# 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 --mount=type=cache,target=/root/.cache/go-build \
|
|
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; \
|
|
CGO_ENABLED=0 go build -trimpath \
|
|
-ldflags "-s -w -X main.Version=$version" \
|
|
-o netwatch-server ./cmd/netwatch-server/
|
|
|
|
# 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
|
|
ARG CACHEBUST
|
|
|
|
# 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 configs/nginx.conf /etc/nginx/templates/netwatch.conf.template
|
|
COPY configs/security-headers.conf /etc/nginx/security-headers.conf
|
|
COPY --from=frontend-build /app/dist /usr/share/nginx/html
|
|
COPY --from=builder /src/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"]
|