check / check (push) Waiting to run
static/js/alpine.min.js is now committed: package/dist/cdn.min.js from the @alpinejs/csp 3.14.9 npm package, byte for byte, taken once from the package file that was in 3p/. The build no longer unpacks it, so script/assets, the assets make target, its Dockerfile steps and the ignore entries for the file are gone, and so is the package file. Prettier and ESLint both ignore the file, so it stays as published. Comments and the README now describe the committed file. Model: opus-5-5
268 lines
12 KiB
Docker
268 lines
12 KiB
Docker
# Stylesheet stages. static/css/tailwind.css is generated, by this pinned
|
|
# tailwindcss, from static/css/input.css and the files its @source lines
|
|
# name. `make css` (script/css) writes it out from the css-output stage.
|
|
# The css-check stage fails when the committed file differs from what is
|
|
# generated; `make check` runs it, and so does the build stage below.
|
|
#
|
|
# tailwindcss v4.2.1 standalone CLI, released 2026-02-23: one binary per
|
|
# architecture, each pinned by its sha256 from the release's sha256sums.txt.
|
|
# debian:bookworm-slim, 2026-10-02: the binary needs glibc.
|
|
FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 AS tailwind-amd64
|
|
ADD --checksum=sha256:39e8d4e24b3c83b0a6e69e100a972fbc75d5fef8dce47b3ddac3cf92dea81fe3 --chmod=755 \
|
|
https://github.com/tailwindlabs/tailwindcss/releases/download/v4.2.1/tailwindcss-linux-x64 /usr/local/bin/tailwindcss
|
|
|
|
FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2e3687004faa906251 AS tailwind-arm64
|
|
ADD --checksum=sha256:d87e6486bb3f70b04ef1dcaacc4ee6548a5a15fbf521b31bc24d2c774f68a951 --chmod=755 \
|
|
https://github.com/tailwindlabs/tailwindcss/releases/download/v4.2.1/tailwindcss-linux-arm64 /usr/local/bin/tailwindcss
|
|
|
|
# TARGETARCH, set by docker, is the architecture being built for.
|
|
FROM tailwind-${TARGETARCH} AS css
|
|
WORKDIR /src
|
|
COPY . .
|
|
RUN tailwindcss -i static/css/input.css -o /out/tailwind.css --minify
|
|
|
|
FROM scratch AS css-output
|
|
COPY --from=css /out/tailwind.css /
|
|
|
|
# Both files are split after each "}", one rule per line, so that when they
|
|
# differ the diff shows the rules that differ.
|
|
FROM css AS css-check
|
|
RUN sed 's/}/}\n/g' static/css/tailwind.css > /tmp/committed.css \
|
|
&& sed 's/}/}\n/g' /out/tailwind.css > /tmp/generated.css \
|
|
&& diff -U0 /tmp/committed.css /tmp/generated.css || { \
|
|
echo "static/css/tailwind.css is not what make css generates; run make css" >&2; \
|
|
exit 1; \
|
|
}
|
|
|
|
# JavaScript lint stages: ESLint, at the version package.json and yarn.lock
|
|
# pin, checks static/js/ against eslint.config.mjs. js-deps installs it, and
|
|
# prettier for the Markdown stages below. The lint phase below runs js-lint.
|
|
#
|
|
# The image's own corepack runs the yarn that package.json's packageManager
|
|
# field names, yarn 4.18.1 (released 2026-09-24), and checks it against the
|
|
# hash there. The image also ships yarn 1, which `corepack enable yarn`
|
|
# replaces.
|
|
# node:24.21.0-alpine (LTS), 2026-09-18
|
|
FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1 AS js-deps
|
|
WORKDIR /src
|
|
COPY package.json yarn.lock .yarnrc.yml ./
|
|
RUN corepack enable yarn && yarn install --immutable --mode=skip-build
|
|
|
|
FROM js-deps AS js-lint
|
|
COPY . .
|
|
RUN --network=none node_modules/.bin/eslint static/js
|
|
|
|
# Markdown stages: prettier, at the version package.json and yarn.lock pin,
|
|
# formats every Markdown file in the tree with the settings in .prettierrc.
|
|
# `make fmt` (script/fmt) writes the formatted files out from markdown-output.
|
|
# markdown-check fails on any file prettier would change; `make fmt-check`
|
|
# runs it, and so does the build stage below.
|
|
FROM js-deps AS markdown
|
|
COPY . .
|
|
RUN --network=none node_modules/.bin/prettier --write '**/*.md' \
|
|
&& mkdir /out \
|
|
&& find . -name '*.md' ! -path './node_modules/*' -exec cp -p --parents {} /out \;
|
|
|
|
FROM scratch AS markdown-output
|
|
COPY --from=markdown /out /
|
|
|
|
FROM js-deps AS markdown-check
|
|
COPY . .
|
|
RUN --network=none node_modules/.bin/prettier --check '**/*.md'
|
|
|
|
# Lint phase: the Go formatting check and golangci-lint over the Go code,
|
|
# and ESLint over static/js/ through the copy from js-lint at the end.
|
|
# `make lint` (script/lint) builds this stage alone; the build stage below
|
|
# depends on it.
|
|
#
|
|
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-09-24
|
|
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# gofmt and golangci-lint are invoked directly rather than through `make
|
|
# fmt-check` and `make lint`, which are themselves docker builds and would
|
|
# need a docker daemon inside this one. The Markdown half of `make
|
|
# fmt-check` is the markdown-check stage above.
|
|
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
|
|
|
|
# The golangci-lint steps run with --network=none. `golangci-lint config
|
|
# verify` is documented as fetching its JSON schema over HTTPS; this pinned
|
|
# image resolves the schema without any network, and --network=none enforces
|
|
# that. It also proves no linter reaches out at analysis time.
|
|
#
|
|
# `run` silently ignores config keys it does not recognize, so a typo would
|
|
# disable a setting without a word. `config verify` is what catches that.
|
|
RUN --network=none golangci-lint config verify --config .golangci.yml
|
|
# --build-tags browser also lints the browser test, which is built only with
|
|
# that tag (make test-browser).
|
|
RUN --network=none golangci-lint run --config .golangci.yml --build-tags browser ./...
|
|
|
|
# Nothing is wanted from js-lint; the copy is what makes this phase run it.
|
|
COPY --from=js-lint /src/yarn.lock /dev/null
|
|
|
|
# Test phase. The tests run with cgo off and so without Go's race detector,
|
|
# which needs cgo on Linux; sneak ruled the race detector out in
|
|
# https://git.eeqj.de/sneak/webhooker/issues/538#issuecomment-153273.
|
|
# `make test` (script/test) builds this stage alone; the build stage below
|
|
# depends on it.
|
|
#
|
|
# golang:1.26.1-bookworm (Debian-based), 2026-03-17: the builder stage's
|
|
# image, which that stage needs for its git and make. Its test step runs
|
|
# under the image's bash, and the tests in internal/versionscript use its
|
|
# git, make and sh, and skip when they are missing.
|
|
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS test
|
|
|
|
WORKDIR /src
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# -timeout applies to each package on its own, so 90s has only to clear the
|
|
# slowest one. -p 4 -parallel 8 keep the run under 2 GB of memory: at most
|
|
# four test binaries build or run at once, each with at most eight parallel
|
|
# tests. The defaults are one per core, so without these the memory the run
|
|
# needs grows with the host's core count.
|
|
#
|
|
# The first run has no -v: go test then prints one result line per package,
|
|
# with its coverage, and for a package that fails, everything its tests
|
|
# wrote. Verbose output from the whole suite passes the 2 MiB at which the
|
|
# Docker build cuts off a step's log, so on a failure only the tests that
|
|
# failed run again, with -v. go test reports a failed test as a line starting
|
|
# "--- FAIL: TestName" (a failed subtest's line is indented, and reruns with
|
|
# its parent) and a failed package as "FAIL<tab>package/path<tab>...". A
|
|
# failure that names no test, such as a build error or a timeout, is already
|
|
# shown in full, so there is nothing to rerun. The step fails after the rerun
|
|
# whatever its result: the first run already showed the suite is broken.
|
|
#
|
|
# TMPDIR, where the tests keep their SQLite databases, is a tmpfs: SQLite
|
|
# waits for the disk at every commit, and on a busy host that waiting was
|
|
# about 40% of the slowest package's run time. GOTMPDIR keeps go's own
|
|
# build files, the test binaries among them, on disk.
|
|
#
|
|
# bash with pipefail, so that the first run's status is go test's, not tee's.
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
RUN --mount=type=tmpfs,target=/tmp/tests,size=512m \
|
|
export TMPDIR=/tmp/tests GOTMPDIR=/tmp; \
|
|
CGO_ENABLED=0 go test -cover -p 4 -parallel 8 -timeout 90s ./... 2>&1 | tee /tmp/go-test.log && exit 0; \
|
|
tests="$(awk '/^--- FAIL: / { print $3 }' /tmp/go-test.log | paste -s -d '|' -)"; \
|
|
packages="$(awk '/^FAIL\t/ { print $2 }' /tmp/go-test.log)"; \
|
|
if [ -n "$tests" ]; then \
|
|
echo "--- Rerunning the failed tests with -v for details ---"; \
|
|
CGO_ENABLED=0 go test -v -p 4 -parallel 8 -timeout 90s -run "^($tests)\$" $packages; \
|
|
fi; \
|
|
exit 1
|
|
|
|
# Build stage
|
|
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
|
# The image ships git and make, which the version step below uses.
|
|
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
|
|
|
# Nothing is wanted from the lint and test phases or from the stylesheet and
|
|
# Markdown checks; the copies are what make BuildKit build them first, so
|
|
# this stage cannot run unless they all passed.
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=test /src/go.sum /dev/null
|
|
COPY --from=css-check /out/tailwind.css /dev/null
|
|
COPY --from=markdown-check /src/yarn.lock /dev/null
|
|
|
|
# A build context sent as a tar archive keeps its files' owners, and git
|
|
# refuses to read a checkout owned by another user. Trust this one
|
|
# whoever owns it.
|
|
RUN git config --system --add safe.directory /build
|
|
|
|
WORKDIR /build
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
COPY . .
|
|
|
|
# Version stamped into the binary: the VERSION build arg when one is
|
|
# given, otherwise what script/version derives from the .git the build
|
|
# context carries, so any `docker build .` of a clone stamps its commit.
|
|
# With neither, as from a source tarball, it is "unknown".
|
|
ARG VERSION
|
|
|
|
# A context that carries .git must not stamp an empty version, "dev" or
|
|
# "unknown": that means git is missing here or could not read the
|
|
# checkout, and the image could not be traced back to its commit.
|
|
RUN version="$(make version VERSION="$VERSION")"; \
|
|
if [ -e .git ]; then \
|
|
case "$version" in ""|dev|unknown) \
|
|
echo "version is '$version' although .git is present" >&2; \
|
|
exit 1 ;; \
|
|
esac; \
|
|
fi
|
|
|
|
# The Makefile's build target builds with cgo off, so the binary is
|
|
# statically linked and runs on the Alpine runtime image below.
|
|
RUN make build VERSION="$VERSION"
|
|
|
|
# Runtime stage
|
|
# alpine:3.21, 2026-03-17
|
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
|
|
|
# su-exec 0.2-r3 (Alpine 3.21), 2026-09-29: the entrypoint runs the app
|
|
# as webhooker with it.
|
|
RUN apk --no-cache add ca-certificates su-exec=0.2-r3
|
|
|
|
# Create non-root user
|
|
RUN addgroup -g 1000 -S webhooker && \
|
|
adduser -u 1000 -S webhooker -G webhooker
|
|
|
|
WORKDIR /app
|
|
|
|
# Copy binary from builder
|
|
COPY --from=builder /build/bin/webhooker /app/webhooker
|
|
|
|
# Not under /app, which belongs to webhooker: this script runs as root.
|
|
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
|
|
|
# Create data directory for all SQLite databases (main app DB +
|
|
# per-webhook event DBs). DATA_DIR defaults to /var/lib/webhooker.
|
|
RUN mkdir -p /var/lib/webhooker
|
|
|
|
RUN chown -R webhooker:webhooker /app /var/lib/webhooker
|
|
|
|
# No USER: the entrypoint starts as root to make the data directory
|
|
# webhooker's, then runs the app as webhooker.
|
|
|
|
EXPOSE 8080
|
|
|
|
# The binary defaults BIND_ADDRESS to 127.0.0.1, which is right for a
|
|
# bare host: the cleartext listener serves the admin UI and the
|
|
# unauthenticated receiver, so it must not appear on every interface
|
|
# of a machine that configured nothing. A container is the other case.
|
|
# Its network namespace is already the isolation boundary, so binding
|
|
# every address inside it exposes nothing; what decides exposure is
|
|
# the publish flag, and `-p 127.0.0.1:8080:8080` is the operator's
|
|
# control there. Shipping the image on loopback would buy no security
|
|
# and would make the process unreachable through its own published
|
|
# port.
|
|
ENV BIND_ADDRESS=0.0.0.0
|
|
|
|
# The probe requests the container's own address, not loopback, so a
|
|
# listener that answers only on loopback (an empty or loopback
|
|
# BIND_ADDRESS) fails it. On a network with IPv4, hostname -i prints the
|
|
# container's IPv4 address, which 0.0.0.0 and :: both answer. On an
|
|
# IPv6-only network it prints none, and the probe requests the IPv6
|
|
# address /etc/hosts lists under the container's hostname instead, which
|
|
# only :: answers.
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
CMD addr="$(hostname -i 2>/dev/null | cut -d ' ' -f 1)"; \
|
|
[ -n "$addr" ] || addr="[$(getent hosts "$(hostname)" | cut -d ' ' -f 1)]"; \
|
|
wget --no-verbose --tries=1 --spider "http://$addr:8080/.well-known/healthcheck" || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
|
CMD ["/app/webhooker"]
|