Skip to content

Commit b451268

Browse files
fix(docker): unbreak prod deploy — Traefik 3.7, distroless healthchecks, agent-pin override (#1328)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6c59898 commit b451268

7 files changed

Lines changed: 87 additions & 29 deletions

File tree

docker/compose.app.yaml

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ services:
1818
ports:
1919
- "80"
2020
depends_on:
21-
- application-server
21+
# Gate on app-server readiness so the SPA isn't served before the API answers.
22+
application-server:
23+
condition: service_healthy
2224
restart: unless-stopped
2325
networks:
2426
- shared-network
@@ -167,6 +169,10 @@ services:
167169
GITLAB_DEFAULT_SERVER_URL: ${GITLAB_DEFAULT_SERVER_URL:-https://gitlab.lrz.de}
168170
# Agent sandbox (practice review + Pi mentor chat). Activated by the worker role
169171
# (hephaestus.runtime.worker.enabled), then gated per-workspace via WorkspaceFeatures.
172+
# AgentImagePinGuard requires a digest-pinned agent image in prod; the release-pin-fetcher
173+
# supplies it for release tags. Set false for non-release (e.g. main/latest) deploys that skip
174+
# the pin, otherwise the boot fails on agent-pi:<tag>. Keep true for release deploys.
175+
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-true}
170176
SANDBOX_DOCKER_HOST: ${SANDBOX_DOCKER_HOST:-unix:///var/run/docker.sock}
171177
SANDBOX_CONTAINER_RUNTIME: ${SANDBOX_CONTAINER_RUNTIME:-}
172178
SANDBOX_MAX_CONCURRENT: ${SANDBOX_MAX_CONCURRENT:-5}
@@ -196,6 +202,9 @@ services:
196202
LLM_PROXY_AZURE_OPENAI_USE_BEARER: ${LLM_PROXY_AZURE_OPENAI_USE_BEARER:-false}
197203
# Worker control channel: same env var feeds the worker pod's registration-token claim.
198204
HEPHAESTUS_WORKER_HUB_TOKEN_REGISTRATION_TOKEN: ${HEPHAESTUS_WORKER_REGISTRATION_TOKEN:-}
205+
# thc healthcheck target: actuator liveness on the management port.
206+
THC_PORT: "8080"
207+
THC_PATH: /actuator/health/liveness
199208
depends_on:
200209
postgres:
201210
condition: service_started
@@ -243,12 +252,14 @@ services:
243252
- "traefik.http.services.https-application-server.loadbalancer.healthcheck.path=/actuator/health/liveness"
244253
- "traefik.http.services.https-application-server.loadbalancer.healthcheck.interval=10s"
245254
- "traefik.http.services.https-application-server.loadbalancer.healthcheck.timeout=3s"
255+
# The distroless image has no shell for a wget probe; the probe is the static thc binary the
256+
# health-checker buildpack adds at /workspace/health-check (server/pom.xml).
246257
healthcheck:
247-
test: "wget -qO- http://localhost:8080/actuator/health || exit 1"
248-
interval: 5s
249-
timeout: 10s
250-
retries: 10
251-
start_period: 10s
258+
test: ["CMD", "/workspace/health-check"]
259+
interval: 15s
260+
timeout: 5s
261+
retries: 3
262+
start_period: 90s
252263
# Matches application.yml's SHUTDOWN_TIMEOUT default (20s) plus headroom so SIGTERM has time
253264
# to drain in-flight requests before Docker SIGKILLs the launcher.
254265
stop_grace_period: 30s
@@ -265,6 +276,7 @@ services:
265276
APP_VERSION: ${IMAGE_TAG}
266277
SPRING_PROFILES_ACTIVE: prod,worker
267278
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:-}
279+
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-true}
268280
HEPHAESTUS_HUB_URL: ws://application-server:8080/api/workers/connect
269281
HEPHAESTUS_WORKER_REGISTRATION_TOKEN: ${HEPHAESTUS_WORKER_REGISTRATION_TOKEN:-}
270282
NATS_SERVER: nats://nats-server:4222
@@ -282,6 +294,9 @@ services:
282294
HEPHAESTUS_WORKER_CAPACITY_MENTOR_MAX: ${HEPHAESTUS_WORKER_MENTOR_MAX:-auto}
283295
HEPHAESTUS_WORKER_DRAIN_TIMEOUT: ${HEPHAESTUS_WORKER_DRAIN_TIMEOUT:-5m}
284296
SENTRY_DSN: ${SENTRY_DSN}
297+
# thc healthcheck target: actuator liveness on the management port (still :8080 though server.port=-1).
298+
THC_PORT: "8080"
299+
THC_PATH: /actuator/health/liveness
285300
depends_on:
286301
application-server:
287302
condition: service_started
@@ -296,12 +311,14 @@ services:
296311
- shared-network
297312
# 5m drain budget + 1m headroom; matches spring.lifecycle.timeout-per-shutdown-phase.
298313
stop_grace_period: 6m
314+
# thc healthcheck (see application-server). The worker fronts no Traefik router, so this is its
315+
# only health signal.
299316
healthcheck:
300-
test: "wget -qO- http://localhost:8080/actuator/health/liveness || exit 1"
301-
interval: 10s
317+
test: ["CMD", "/workspace/health-check"]
318+
interval: 15s
302319
timeout: 5s
303-
retries: 6
304-
start_period: 30s
320+
retries: 3
321+
start_period: 90s
305322
logging:
306323
driver: "json-file"
307324
options:

docker/compose.core.yaml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ services:
3232
SENTRY_DSN: ${SENTRY_DSN}
3333
# forward-headers=native (application-prod.yml) → ProxyTrustGuard fails the boot unless pinned.
3434
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:-}
35+
# AgentImagePinGuard loads in any prod pod; set false for non-release deploys that skip the pin.
36+
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-true}
37+
# thc healthcheck target: readiness (reports unhealthy while NATS/JetStream is unavailable).
38+
THC_PORT: "8080"
39+
THC_PATH: /actuator/health/readiness
3540
depends_on:
3641
nats-server:
3742
condition: service_healthy
@@ -58,12 +63,17 @@ services:
5863
- "traefik.http.routers.http-webhook-server.priority=100"
5964
- "traefik.http.routers.https-webhook-server.priority=100"
6065
- "traefik.http.services.https-webhook-server.loadbalancer.server.port=8080"
66+
# Routing-layer probe: drops a bad backend from the LB pool.
67+
- "traefik.http.services.https-webhook-server.loadbalancer.healthcheck.path=/actuator/health/readiness"
68+
- "traefik.http.services.https-webhook-server.loadbalancer.healthcheck.interval=15s"
69+
- "traefik.http.services.https-webhook-server.loadbalancer.healthcheck.timeout=5s"
70+
# thc healthcheck (see docker/compose.app.yaml).
6171
healthcheck:
62-
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health/readiness"]
72+
test: ["CMD", "/workspace/health-check"]
6373
interval: 15s
6474
timeout: 5s
65-
retries: 5
66-
start_period: 30s
75+
retries: 3
76+
start_period: 90s
6777
logging:
6878
driver: "json-file"
6979
options:

docker/compose.proxy.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
services:
22
reverse-proxy:
3-
image: traefik:v3.4
3+
# Keep at >=3.3: below it the app-server sticky.cookie.path label (workspace replica affinity)
4+
# fails to parse and Traefik silently drops the whole router. Pin a current 3.x.
5+
image: traefik:v3.7.4
46
restart: unless-stopped
57
networks:
68
- shared-network

docker/preview/compose.app.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ services:
194194
# prod runs forward-headers=native, so ProxyTrustGuard aborts the boot unless this is set.
195195
# Coolify fronts previews with its own proxy — set its ingress regex, not staging's range.
196196
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:?Required}
197+
# Previews run non-release images with no release-pin, so don't require a digest-pinned agent image.
198+
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-false}
197199
# URL - Coolify provides SERVICE_FQDN_WEBAPP
198200
APPLICATION_HOST_URL: https://${SERVICE_FQDN_WEBAPP}
199201
DATABASE_URL: postgresql://postgres:5432/hephaestus
@@ -234,23 +236,20 @@ services:
234236
# GitLabWebhookService appends /webhooks/gitlab itself.
235237
WEBHOOK_SECRET: ${WEBHOOK_SECRET:-}
236238
WEBHOOK_EXTERNAL_URL: https://${PREVIEW_DOMAIN:?Required}
239+
# thc healthcheck target: actuator liveness on the management port.
240+
THC_PORT: "8080"
241+
THC_PATH: /actuator/health/liveness
237242
depends_on:
238243
postgres:
239244
condition: service_healthy
240245
seed-loader:
241246
condition: service_completed_successfully
247+
# thc healthcheck (see docker/compose.app.yaml).
242248
healthcheck:
243-
test:
244-
[
245-
"CMD",
246-
"wget",
247-
"--spider",
248-
"--quiet",
249-
"http://127.0.0.1:8080/actuator/health",
250-
]
251-
interval: 10s
252-
timeout: 10s
253-
retries: 20
249+
test: ["CMD", "/workspace/health-check"]
250+
interval: 15s
251+
timeout: 5s
252+
retries: 3
254253
start_period: 90s
255254
logging:
256255
driver: "json-file"
@@ -286,6 +285,7 @@ services:
286285
POSTHOG_ENABLED: "false"
287286
depends_on:
288287
application-server:
288+
# Gate on app-server readiness so the SPA isn't served before the API answers.
289289
condition: service_healthy
290290
healthcheck:
291291
test: ["CMD", "curl", "-f", "http://127.0.0.1:80/"]

docker/preview/compose.shared-infra.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,24 @@ services:
7979
WEBHOOK_SECRET: ${WEBHOOK_SECRET:?Required}
8080
# prod runs forward-headers=native → ProxyTrustGuard aborts the boot unless set (Coolify ingress regex).
8181
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:?Required}
82+
# Previews run non-release images with no release-pin, so don't require a digest-pinned agent image.
83+
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-false}
84+
# thc healthcheck target: actuator readiness on the management port.
85+
THC_PORT: "8080"
86+
THC_PATH: /actuator/health/readiness
8287
depends_on:
8388
nats-server:
8489
condition: service_healthy
8590
# Spring drains HTTP (20s) THEN WebhookGracefulShutdown drains NATS (up to 15s).
8691
# Docker must allow ≥35s before SIGKILL, or the JVM is cut mid-drain.
8792
stop_grace_period: 40s
93+
# thc healthcheck (see docker/compose.app.yaml).
8894
healthcheck:
89-
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/actuator/health/readiness"]
95+
test: ["CMD", "/workspace/health-check"]
9096
interval: 15s
9197
timeout: 5s
92-
retries: 5
93-
start_period: 30s
98+
retries: 3
99+
start_period: 90s
94100
logging:
95101
driver: "json-file"
96102
options:

docs/admin/buildpacks-cds-decision.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,29 @@ AOT processing evaluates `@Conditional` at **build time**, baking the build-time
2727

2828
## Builder pinning
2929

30-
`pom.xml` pins `builder-noble-java-tiny` + `ubuntu-noble-run-tiny` by sha256 digest. Refresh:
30+
`pom.xml` pins `builder-noble-java-tiny` + `ubuntu-noble-run-tiny` + the `health-checker` buildpack by
31+
sha256 digest. Refresh:
3132

3233
```
3334
docker buildx imagetools inspect paketobuildpacks/builder-noble-java-tiny:latest --format '{{.Manifest.Digest}}'
3435
docker buildx imagetools inspect paketobuildpacks/ubuntu-noble-run-tiny:latest --format '{{.Manifest.Digest}}'
36+
docker buildx imagetools inspect paketobuildpacks/health-checker:latest --format '{{.Manifest.Digest}}'
3537
```
3638

3739
Bump as part of release cycles; the digest is the source of truth.
3840

41+
## Container healthcheck on the distroless run image
42+
43+
On Docker Compose the container `HEALTHCHECK` is the only container-level health signal —
44+
`service_healthy` gating and the `docker compose ps` column both depend on it. `run-tiny` has no
45+
shell/wget and `builder-noble-java-tiny` bundles no probe, so `pom.xml` adds an explicit `<buildpacks>`
46+
order. Specifying `<buildpacks>` **replaces** the builder's default order, so the `java` composite must
47+
be re-listed (`urn:cnb:builder:paketo-buildpacks/java`) before appending
48+
`docker://paketobuildpacks/health-checker`; `BP_HEALTH_CHECKER_ENABLED=true` opts it in. It contributes
49+
the static, shell-free `thc` binary at `/workspace/health-check`, which the compose services invoke as an
50+
exec-form `HEALTHCHECK` (`THC_PORT`/`THC_PATH` → actuator liveness/readiness). No `health-check` process
51+
type is added, so the JVM-spawn-per-probe issue (health-checker#87) does not apply.
52+
3953
## git CLI in the runtime image
4054

4155
`GitDiffOperations` previously shelled out to `git`; it was ported to JGit in the prerequisite commit, eliminating the runtime `git` dependency. The Paketo run image is used unmodified.

server/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,9 +922,18 @@
922922
<image>
923923
<builder>paketobuildpacks/builder-noble-java-tiny@sha256:c8b5f936e6d9af492af4a493b4a03eaf496fe51f206c5d53ccddab7871ed4ef5</builder>
924924
<runImage>paketobuildpacks/ubuntu-noble-run-tiny@sha256:b5a167cf9a8021e289e5701a82f7c6b430ba85bb8360e9ddc767340e3f8824c3</runImage>
925+
<!-- Explicit order: the builder's `java` composite, then the health-checker buildpack
926+
(not bundled in builder-noble-java-tiny) so the distroless run image gets the static,
927+
shell-free `thc` binary at /workspace/health-check for a real Docker HEALTHCHECK.
928+
See docs/admin/buildpacks-cds-decision.md and docker/compose.app.yaml. -->
929+
<buildpacks>
930+
<buildpack>urn:cnb:builder:paketo-buildpacks/java</buildpack>
931+
<buildpack>docker://paketobuildpacks/health-checker@sha256:c95183658c32d47d8902bc59b9950f5bfa57cd4b7f655d2d5ba4d902051a0964</buildpack>
932+
</buildpacks>
925933
<env>
926934
<BP_JVM_VERSION>21</BP_JVM_VERSION>
927935
<BP_JVM_CDS_ENABLED>true</BP_JVM_CDS_ENABLED>
936+
<BP_HEALTH_CHECKER_ENABLED>true</BP_HEALTH_CHECKER_ENABLED>
928937
<!-- Activates application-cds-training.yml during the build-time training run.
929938
Coolify's runtime SPRING_PROFILES_ACTIVE=prod overrides this. -->
930939
<SPRING_PROFILES_ACTIVE>cds-training</SPRING_PROFILES_ACTIVE>

0 commit comments

Comments
 (0)