fix(deploy): unblock every rebuild on BuildKit's host-network refusal
Gitea Actions Runner Test / test-job (push) Successful in 1s
CI / check (push) Successful in 33s
CI / tests-unit (push) Failing after 2m4s
CI / tests-integration (push) Successful in 2m6s
CI / tests-ui (push) Successful in 2m43s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

Docker 29.1.3 ships BuildKit v0.26, which refuses to grant a build host
networking unless each caller passes --allow=network.host. All three rebuild
paths asked for it, and `docker compose build` has no flag to grant it, so a
rebuild failed immediately with "additional privileges requested". The live
container was never replaced, which is exactly the reported symptom: the site
kept serving the previous release after a rebuild.

Nothing in the build actually needs host networking. It uses the network only
for apk, pnpm and next/font/google — all outbound internet, which the default
bridge provides. Verified by building both the full runner image and the
migrations stage with --no-cache after dropping the flag.

Runtime `network_mode: host` stays: blue/green needs per-release host ports
(3002/3003) and nginx reaches each slot over 127.0.0.1.

The second gap is how a rebuild could still ship the wrong code. ci-deploy.sh
stamped every image with HEAD's revision label, and verify-deployed-release.mjs
only re-checks that same label, so a dirty working tree produced an image that
claimed to be release $sha while containing uncommitted code. docker-update.sh
already refused this; ci-deploy.sh now does too, before any build work.
This commit is contained in:
openhands committed 2026-10-10 13:07:13 +02:00
1 parent 48291ab641
commit fdb7af7ef5
6 files changed
+29 -6

No files matched your search

+8 -1
View File
@@ -3,12 +3,19 @@
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
x-cms: &cms x-cms: &cms
image: epicnext-cms:${CMS_RELEASE:-local} image: epicnext-cms:${CMS_RELEASE:-local}
# No `network: host` on the build. BuildKit (v0.26, Docker 29) refuses to grant
# host networking unless every caller passes --allow=network.host, and
# `docker compose build` has no such flag — so asking for it here turned every
# rebuild into an immediate "additional privileges requested" failure, which
# left the previous release serving traffic. The build only needs outbound
# internet (apk, pnpm, next/font/google), which the default bridge provides.
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args: args:
NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown} NEXT_DEPLOYMENT_ID: ${CMS_RELEASE:-unknown}
network: host # Runtime host networking IS required: blue/green needs per-release host ports
# (3002/3003) and nginx reaches the slot over 127.0.0.1.
network_mode: host network_mode: host
stop_grace_period: 15s stop_grace_period: 15s
restart: unless-stopped restart: unless-stopped
+17 -1
View File
@@ -395,11 +395,27 @@ if ! grep -qs '^DATABASE_URL=' .env; then
exit 1 exit 1
fi fi
# De image krijgt het label van $sha, en `verify-deployed-release.mjs` controleert
# later alleen díe label. Zonder deze check bouwt een vuile werkboom dus een image
# die zegt release $sha te zijn terwijl er ongecommitte code in zit — precies het
# scenario "rebuilden levert geen nieuwe code". `docker-update.sh` deed dit al.
# `--untracked-files=normal` laat gitignored artefacten (.next, coverage,
# build-reports) buiten beschouwing; die worden toch niet meegebouwd.
if [ -n "$(git status --porcelain --untracked-files=normal)" ]; then
echo "Error: de werkboom is niet schoon, dus de image zou een verkeerd release-label krijgen." >&2
echo " Commit of stash de wijzigingen en draai opnieuw." >&2
echo " De live release is niet aangeraakt." >&2
git status --short >&2
exit 1
fi
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm exec playwright install chromium pnpm exec playwright install chromium
echo "Building $image" echo "Building $image"
DOCKER_BUILDKIT=1 docker build --network=host --progress=plain --cache-from epicnext-cms:latest \ # No --network=host: BuildKit only grants it via --allow=network.host, and the
# build needs nothing but outbound internet (apk, pnpm, next/font/google).
DOCKER_BUILDKIT=1 docker build --progress=plain --cache-from epicnext-cms:latest \
--build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" . --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" .
check_current check_current
# Read reports from the already-built image; do not start an extra application. # Read reports from the already-built image; do not start an extra application.
+1 -1
View File
@@ -39,6 +39,6 @@ pnpm exec playwright install chromium
export NEWS_E2E_IMAGE="$image" export NEWS_E2E_IMAGE="$image"
export NEWS_E2E_RELEASE="$sha" export NEWS_E2E_RELEASE="$sha"
build_attempted=1 build_attempted=1
DOCKER_BUILDKIT=1 docker build --network=host --progress=plain \ DOCKER_BUILDKIT=1 docker build --progress=plain \
--build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" . --build-arg NEXT_DEPLOYMENT_ID="$sha" -t "$image" .
NEWS_E2E_IMAGE="$image" NEWS_E2E_RELEASE="$sha" node --import tsx e2e/news-real/run.ts NEWS_E2E_IMAGE="$image" NEWS_E2E_RELEASE="$sha" node --import tsx e2e/news-real/run.ts
+1 -1
View File
@@ -123,7 +123,7 @@ if [[ -n "${CMS_IMAGE_REPOSITORY:-}" ]]; then
docker tag "$app_reference" "epicnext-cms:$CMS_RELEASE" docker tag "$app_reference" "epicnext-cms:$CMS_RELEASE"
docker tag "$remote_migration_image" "$migration_image" docker tag "$remote_migration_image" "$migration_image"
else else
docker build --network=host --target migrations --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" -t "$migration_image" . >>"$LOG_FILE" 2>&1 docker build --target migrations --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" -t "$migration_image" . >>"$LOG_FILE" 2>&1
docker compose build --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" cms >>"$LOG_FILE" 2>&1 docker compose build --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" cms >>"$LOG_FILE" 2>&1
fi fi
expected_image="$(docker image inspect --format '{{.Id}}' "epicnext-cms:$CMS_RELEASE")" expected_image="$(docker image inspect --format '{{.Id}}' "epicnext-cms:$CMS_RELEASE")"
+1 -1
View File
@@ -94,7 +94,7 @@ describe("isolated branch preflight", () => {
expect( expect(
result.calls.split("\n").filter((line) => line.startsWith("docker ")), result.calls.split("\n").filter((line) => line.startsWith("docker ")),
).toEqual([ ).toEqual([
expect.stringContaining("docker build --network=host"), expect.stringContaining("docker build --progress=plain"),
`docker image rm ${image}`, `docker image rm ${image}`,
]); ]);
expect(result.calls).not.toMatch( expect(result.calls).not.toMatch(
+1 -1
View File
@@ -11,7 +11,7 @@ pnpm() {
docker() { docker() {
echo "docker $*" >> "$TEST_DIR/calls" echo "docker $*" >> "$TEST_DIR/calls"
case "$1 $2" in case "$1 $2" in
'build --network=host') [ "$SCENARIO" != build-failure ] ;; 'build --progress=plain') [ "$SCENARIO" != build-failure ] ;;
'image rm') [ "$SCENARIO" != cleanup-failure ] ;; 'image rm') [ "$SCENARIO" != cleanup-failure ] ;;
*) return 92 ;; *) return 92 ;;
esac esac