check / check (push) Successful in 3m37s
yarn 1, which the node image ships and which is no longer developed, printed node's url.parse() deprecation warning during the js-deps install. package.json now pins yarn 4.18.1 by version and hash in its packageManager field; the js-deps stage enables it with the node image's own corepack and runs `yarn install --immutable`. yarn.lock is regenerated in yarn 4's format from the old lockfile, so every package keeps the version it had. The new .yarnrc.yml keeps the install in node_modules/, where the lint and Markdown stages run the tools from. Model: opus-5-5
221 lines
9.6 KiB
Docker
221 lines
9.6 KiB
Docker
# Lint stage
|
|
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
|
|
# Using Debian-based image because mattn/go-sqlite3 (CGO) does not
|
|
# compile on Alpine musl (off64_t is a glibc type).
|
|
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
# Copy source code. In CI the context also carries .ci-fingerprint, which
|
|
# holds the hash of the commit being checked (see
|
|
# .gitea/workflows/check.yml). That invalidates this layer, so the checks
|
|
# below cannot report success by replaying a cached pass. Do not add it to
|
|
# .dockerignore.
|
|
COPY . .
|
|
|
|
# Run the Go formatting check and the linter. gofmt and golangci-lint are
|
|
# invoked directly rather than through `make fmt-check` and `make lint`: this
|
|
# stage is already the pinned linter image, and both scripts build docker
|
|
# stages, so calling them here would need a docker daemon inside the build.
|
|
# The Markdown half of `make fmt-check` is the markdown-check stage below.
|
|
# Keep the golangci-lint steps in step with Dockerfile.lint, including
|
|
# --network=none (see its header for why).
|
|
RUN if [ -n "$(gofmt -s -l .)" ]; then echo "gofmt needed on:"; gofmt -s -l .; exit 1; fi
|
|
RUN script/assets
|
|
RUN --network=none golangci-lint config verify --config .golangci.yml
|
|
RUN --network=none golangci-lint run --config .golangci.yml --build-tags browser ./...
|
|
|
|
# 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, and stays cached until package.json,
|
|
# yarn.lock or .yarnrc.yml changes. script/lint forces only js-lint to re-run,
|
|
# and the build stage below runs it too. COPY . . brings in the CI cache
|
|
# barrier described in the lint stage above.
|
|
#
|
|
# 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'
|
|
|
|
# Build stage
|
|
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
|
# Using Debian-based image because gorm.io/driver/sqlite pulls in
|
|
# mattn/go-sqlite3 (CGO), which does not compile on Alpine musl.
|
|
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
|
|
|
# Depend on the lint, stylesheet check, JavaScript lint and Markdown check
|
|
# stages passing
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
COPY --from=css-check /out/tailwind.css /dev/null
|
|
COPY --from=js-lint /src/yarn.lock /dev/null
|
|
COPY --from=markdown-check /src/yarn.lock /dev/null
|
|
|
|
# jq is a runtime dependency of script/ci-mark-superseded, which the test
|
|
# suite executes. git is what script/version derives the version with.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends make curl ca-certificates jq git && rm -rf /var/lib/apt/lists/*
|
|
|
|
# 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 source code, including the .ci-fingerprint cache barrier described in
|
|
# the lint stage above.
|
|
COPY . .
|
|
|
|
# Run tests and build. Both first run script/assets, which extracts Alpine.js
|
|
# from its tarball in 3p/.
|
|
RUN make test
|
|
|
|
# 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".
|
|
#
|
|
# Declared here, below the test step, so a changed version does not
|
|
# invalidate its cached layer.
|
|
ARG VERSION
|
|
|
|
# A context that carries .git must not stamp "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 if [ -d .git ] && [ "$(make version VERSION="$VERSION")" = unknown ]; then \
|
|
echo "version is unknown although the build context carries .git" >&2; \
|
|
exit 1; \
|
|
fi
|
|
|
|
RUN make build VERSION="$VERSION"
|
|
|
|
# Rebuild with static linking for Alpine runtime.
|
|
# make build already verified compilation.
|
|
# The CGO binary from `make build` is dynamically linked against glibc,
|
|
# which doesn't exist on Alpine (musl). Rebuild with static linking so
|
|
# the binary runs on Alpine without glibc.
|
|
#
|
|
# The static flags go in through GO_LDFLAGS rather than a -ldflags of
|
|
# their own: the build target composes them with the -X that stamps the
|
|
# version, so this relink cannot silently drop the stamp.
|
|
RUN CGO_ENABLED=1 make build VERSION="$VERSION" GO_LDFLAGS='-extldflags "-static"'
|
|
|
|
# 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
|
|
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/.well-known/healthcheck || exit 1
|
|
|
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
|
CMD ["/app/webhooker"]
|