From a13f2d90f4afb9b790635311c901b73e0527be68 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 13 Oct 2026 08:00:28 +0000 Subject: [PATCH] Build the image's static binary with netgo, osusergo and sqlite_omit_load_extension (closes #525) The image's static relink linked glibc's getaddrinfo (from Go's cgo DNS resolver) and dlopen (from SQLite extension loading). A static binary still needs glibc's shared libraries at run time for both, and the Alpine runtime has none. The relink now passes the standard build tags netgo, osusergo and sqlite_omit_load_extension through a GO_TAGS variable, which the Makefile's build target hands to go build as -tags next to GO_LDFLAGS, so the version stamp is composed as before. Model: opus-5-5 --- Dockerfile | 24 ++++++++++++------- Makefile | 10 ++++---- README.md | 14 ++++++----- internal/versionscript/version_script_test.go | 8 +++++-- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index c52265d..e72dee7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -214,16 +214,22 @@ RUN version="$(make version VERSION="$VERSION")"; \ # (Alpine.js, extracted from its tarball in 3p/) first. 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. +# Rebuild with static linking for the Alpine runtime. The cgo binary the +# step above builds is dynamically linked against glibc, which Alpine +# (musl) does not have. # -# 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"' +# A statically linked glibc still loads shared libraries at run time for +# getaddrinfo and dlopen, and Alpine has none to load, so the build tags +# keep both out of the binary: netgo and osusergo select Go's own DNS +# resolver and user lookup instead of glibc's, and +# sqlite_omit_load_extension leaves out SQLite extension loading, which +# webhooker never uses. +# +# The static flags and tags go in through GO_LDFLAGS and GO_TAGS 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_TAGS=netgo,osusergo,sqlite_omit_load_extension GO_LDFLAGS='-extldflags "-static"' # Runtime stage # alpine:3.21, 2026-03-17 diff --git a/Makefile b/Makefile index ea25515..b2f2f36 100644 --- a/Makefile +++ b/Makefile @@ -16,14 +16,16 @@ VERSION ?= $(shell script/version) # exists to correct. override VERSION := $(or $(strip $(VERSION)),$(shell script/version)) -# Extra linker flags for the build target. The static relink in the -# Dockerfile adds -extldflags here rather than passing its own -ldflags, -# so composing flags cannot drop the version stamp. +# Extra linker flags and build tags for the build target. The static +# relink in the Dockerfile passes -extldflags and its build tags through +# these rather than a -ldflags of its own, so composing flags cannot drop +# the version stamp. # # The build target itself always passes -trimpath and -s -w, as the Go # Dockerfile in REPO_POLICIES.md does: no build paths, symbol table or # debug information in the binary. GO_LDFLAGS ?= +GO_TAGS ?= bootstrap: @script/bootstrap @@ -53,7 +55,7 @@ check: @script/check build: assets - go build -trimpath -ldflags '$(strip -s -w -X main.version=$(VERSION) $(GO_LDFLAGS))' -o bin/webhooker ./cmd/webhooker + go build -trimpath -tags '$(GO_TAGS)' -ldflags '$(strip -s -w -X main.version=$(VERSION) $(GO_LDFLAGS))' -o bin/webhooker ./cmd/webhooker run: build ./bin/webhooker diff --git a/README.md b/README.md index cbdcc85..80ed3db 100644 --- a/README.md +++ b/README.md @@ -3419,12 +3419,14 @@ fixed independently of the compiler's: phases and the `css-check` and `markdown-check` stages passing (it copies a file from each), runs `make build` (which extracts Alpine.js from `3p/` first), and then rebuilds the binary with `CGO_ENABLED=1` and static linking - so it runs on musl. Both builds go through `make build`, the relink adding - its `-extldflags` via `GO_LDFLAGS`, so neither can drop the `-X` that stamps - the version. The version is the `VERSION` build arg if one is given, - otherwise derived from the `.git` in the context, and the stage fails if a - context with `.git` would stamp an empty version, `dev` or `unknown` (see - [Version stamping](#version-stamping)). + so it runs on musl, with the build tags `netgo`, `osusergo` and + `sqlite_omit_load_extension` so it needs none of glibc's shared libraries at + run time. Both builds go through `make build`, the relink adding its + `-extldflags` via `GO_LDFLAGS` and its tags via `GO_TAGS`, so neither can + drop the `-X` that stamps the version. The version is the `VERSION` build arg + if one is given, otherwise derived from the `.git` in the context, and the + stage fails if a context with `.git` would stamp an empty version, `dev` or + `unknown` (see [Version stamping](#version-stamping)). 7. **Runtime stage** (`alpine:3.21`) — copies the static binary and `deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker` directory for all SQLite databases, exposes port 8080, and includes a health check against diff --git a/internal/versionscript/version_script_test.go b/internal/versionscript/version_script_test.go index 3e59b17..68841c5 100644 --- a/internal/versionscript/version_script_test.go +++ b/internal/versionscript/version_script_test.go @@ -156,7 +156,8 @@ func TestVersion_IsStableAcrossInvocations(t *testing.T) { // The build target is the only place composing linker flags. If a // future edit drops either half, the binary silently reports "dev" // again (the defect this package exists for) or fails to link on -// Alpine. +// Alpine. If it stops passing GO_TAGS, the image's static binary links +// glibc's getaddrinfo and dlopen again. func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) { t.Parallel() @@ -164,6 +165,7 @@ func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) { require.Contains(t, makefile, "-X main.version=$(VERSION)") require.Contains(t, makefile, "$(GO_LDFLAGS)") + require.Contains(t, makefile, "-tags '$(GO_TAGS)'") require.Contains(t, makefile, "VERSION ?= $(shell script/version)") } @@ -233,7 +235,9 @@ func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) { "a raw go build bypasses the Makefile's -X flag") require.Contains(t, dockerfile, "ARG VERSION") require.Contains(t, dockerfile, - `make build VERSION="$VERSION" GO_LDFLAGS='-extldflags "-static"'`) + `make build VERSION="$VERSION"`+ + ` GO_TAGS=netgo,osusergo,sqlite_omit_load_extension`+ + ` GO_LDFLAGS='-extldflags "-static"'`) } func read(t *testing.T, path string) string {