# The one image netwatch ships: nginx serves the built frontend and # passes /api/ and /.well-known/healthcheck to netwatch-server, the Go # backend, which runs in the same container on loopback only. # bin/entrypoint.sh starts and watches both. # Lint stage — fast feedback on formatting and lint issues. The # golangci/golangci-lint image ships Go, gofmt, make and the linter, so # nothing is installed here. The root make lint builds this stage alone. # golangci/golangci-lint:v2.12.2 (2026-08-10) FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint WORKDIR /src COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ . RUN make fmt-check RUN make lint # Backend build stage # golang:1.25-alpine (2026-02-27) FROM golang:1.25-alpine@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder RUN apk add --no-cache make WORKDIR /src # Force BuildKit to run the lint stage before proceeding. BuildKit runs # stages in parallel by default; without this no-op copy a lint failure # would not gate compilation. COPY --from=lint /src/go.sum /dev/null COPY backend/go.mod backend/go.sum ./ RUN go mod download COPY backend/ . RUN make test # make build is a shim around backend/script/build, the one definition # of the build command: # CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.Version=... -X main.Buildarch=..." # That script reads VERSION from the environment, so it is handed over # there rather than as a make variable. ARG VERSION=dev RUN VERSION="${VERSION}" make build # Frontend stage # 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 RUN apk add --no-cache git make COPY . . # make frontend-check is the frontend half of make check (test + lint + # fmt-check); its test step is the production yarn build, so this both # produces dist/ and gates the image on lint/fmt-check/test regressions. # This node stage has neither Go nor Docker; the lint and builder stages # above gate the backend half. RUN make frontend-check # Runtime stage # 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: master process as root, # workers as the nginx user. RUN addgroup -g 1000 -S netwatch && \ adduser -u 1000 -S netwatch -G netwatch RUN rm /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/netwatch.conf COPY --from=frontend /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 ENV DATA_DIR=/data/reports RUN mkdir -p /data/reports && chown -R netwatch:netwatch /data VOLUME /data EXPOSE 8080 # The nginx image stops its container with SIGQUIT; the entrypoint # acts on TERM and INT. STOPSIGNAL SIGTERM ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]