check / check (push) Waiting to run
yarn build moves out of the stage that runs the frontend's unit tests, now named frontend-test, into a frontend-build stage of its own. It starts from frontend-test for its yarn packages and source, installs the git that vite.config.js reads the commit with, and copies from the lint and test phases as builder does, so it cannot run unless they passed. The runtime stage copies dist/ from it. The Go compile, most of make test's time, is unchanged and waits on the ruling asked for in sneak/prompts#113; no build cache is added. Model: opus-5-5
164 lines
7.3 KiB
Docker
164 lines
7.3 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.
|
|
|
|
# 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 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
|
|
COPY . .
|
|
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 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
|
|
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.
|
|
# 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 . .
|
|
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-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
|
|
RUN yarn build
|
|
|
|
# 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 go.mod go.sum ./
|
|
RUN go mod download
|
|
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 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
|
|
|
|
# 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"]
|