Skip to content

Commit f1def40

Browse files
fix(docker): repair the Coolify preview deployment
docker/preview/* was never updated for the native-auth cutover (#1317) or the Java webhook receiver (#1300, #1306), and had drifted far enough that no stack could boot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9ab3bd6 commit f1def40

2 files changed

Lines changed: 209 additions & 67 deletions

File tree

docker/preview/compose.app.yaml

Lines changed: 156 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@
1313
# 5. Configure environment variables (see .env.example)
1414
# 6. Configure domains in Coolify UI (General tab) on the MAIN application:
1515
# - webapp: https://hephaestus.example.com
16-
# - application-server: https://api.hephaestus.example.com
16+
# - appserver: https://api.hephaestus.example.com
1717
#
1818
# Preview deployments will AUTOMATICALLY get domains like:
1919
# - webapp: https://{{pr_id}}.hephaestus.example.com
20-
# - application-server: https://{{pr_id}}.api.hephaestus.example.com
20+
# - appserver: https://{{pr_id}}.api.hephaestus.example.com
2121
#
2222
# Coolify Auto-Generated Variables:
2323
# - SERVICE_FQDN_WEBAPP: Domain for webapp (host only, no scheme)
2424
# - SERVICE_URL_WEBAPP: Full URL for webapp (with https://)
25-
# - SERVICE_FQDN_APPLICATION_SERVER: Domain for API (host only)
26-
# - SERVICE_URL_APPLICATION_SERVER: Full URL for API (with https://)
25+
# - SERVICE_FQDN_APPSERVER: Domain for API (host only)
26+
# - SERVICE_URL_APPSERVER: Full URL for API (with https://)
2727
# - SERVICE_NAME_<SERVICE>: Container name (e.g., postgres-wg44k0c-pr-557)
2828
# - COOLIFY_CONTAINER_NAME: Unique container identifier
2929
#
@@ -44,9 +44,13 @@ services:
4444
# SEEDING HACK: Uses init container to populate via psql commands
4545
# after postgres starts, rather than relying on /docker-entrypoint-initdb.d
4646
# ---------------------------------------------------------------------------
47+
# No `hostname:` override. Coolify attaches every stack to the shared `coolify`
48+
# network, where a fixed hostname would make each preview's postgres answer to the
49+
# same `postgres` name — an app-server could then reach another PR's database.
50+
# The compose service key is already a per-deploy-unique alias (`postgres` on the
51+
# base deploy, `postgres-pr-<id>` on previews); reference it via SERVICE_NAME_POSTGRES.
4752
postgres:
4853
image: ghcr.io/ls1intum/hephaestus/postgres:${IMAGE_TAG:-latest}
49-
hostname: postgres
5054
restart: unless-stopped
5155
environment:
5256
POSTGRES_DB: hephaestus
@@ -81,14 +85,14 @@ services:
8185
environment:
8286
PGPASSWORD: ${POSTGRES_PASSWORD:-hephaestus-preview}
8387
SERVICE_NAME_POSTGRES: ${SERVICE_NAME_POSTGRES:-}
84-
SERVICE_FQDN_APPLICATION_SERVER: ${SERVICE_FQDN_APPLICATION_SERVER:-}
88+
SERVICE_FQDN_APPSERVER: ${SERVICE_FQDN_APPSERVER:-}
8589
SERVICE_FQDN_WEBAPP: ${SERVICE_FQDN_WEBAPP:-}
8690
command: |
8791
sh -c '
8892
set -e
8993
apk add --no-cache docker-cli >/tmp/apk.log 2>&1
9094
91-
APP_FQDN="${SERVICE_FQDN_APPLICATION_SERVER:-${SERVICE_FQDN_WEBAPP:-}}"
95+
APP_FQDN="${SERVICE_FQDN_APPSERVER:-${SERVICE_FQDN_WEBAPP:-}}"
9296
if ! echo "$$APP_FQDN" | grep -Eq "^pr[0-9]+"; then
9397
echo "Skipping seed-loader: non-PR deployment detected"
9498
exit 0
@@ -183,36 +187,104 @@ services:
183187
# labels from docker/compose.app.yaml are intentionally omitted here — they would be a
184188
# no-op. See docs/contributor/unified-pi-runtime.mdx.
185189
# ---------------------------------------------------------------------------
186-
application-server:
187-
build:
188-
context: ./server
189-
dockerfile: Dockerfile
190-
hostname: application-server
190+
# Docker creates named volumes owned by root:root, but the buildpack image runs as the
191+
# unprivileged `cnb` user (1002:1001). Without this, GitRepositoryManager cannot mkdir under
192+
# /data/git-repos ("Failed to ensure repository" on every checkout) and Tomcat's AccessLogValve
193+
# cannot open /var/log/hephaestus/access. Previews get fresh root-owned volumes on every deploy,
194+
# so this cannot be a one-off fix applied by hand.
195+
volume-init:
196+
image: alpine:3
197+
command:
198+
- sh
199+
- -c
200+
- chown -R 1002:1001 /data/git-repos /var/log/hephaestus && chmod 0755 /data/git-repos /var/log/hephaestus
201+
volumes:
202+
- git-repos:/data/git-repos
203+
- appserver-logs:/var/log/hephaestus
204+
restart: "no"
205+
206+
# Never use `${VAR:?message}` in this file. Coolify's compose parser seeds its own environment
207+
# store from these placeholders and keeps the *message* as the value, so `:?Required` becomes the
208+
# literal string "Required" — which then fails the boot far from the cause (a base64 "Required" is
209+
# 6 bytes, not the 32 the state-cookie key needs). Leave required vars as a bare `${VAR}` and let
210+
# the app's own fail-fast report them.
211+
#
212+
# There is no server/Dockerfile: the image comes from Paketo buildpacks (spring-boot:build-image,
213+
# server/pom.xml), which also supplies the /workspace/health-check binary the healthcheck invokes.
214+
# ci-docker-build.yml publishes `latest` (main), `pr-<id>` (PRs), `<sha>`, `<branch>`. The webapp
215+
# below builds from source, so a preview always exercises the PR's frontend; set IMAGE_TAG=pr-<id>
216+
# to exercise the PR's server too (that tag exists only if the PR touched server/).
217+
#
218+
# The service name is `appserver`, not `application-server`, and it is load-bearing. Coolify stores
219+
# a service's configured domain under the service name but resolves it at deploy time with dashes
220+
# replaced by underscores; with a dashed name it did not find the entry and replaced the domain
221+
# with an auto-generated <service>-<uuid>.<wildcard> host on every deploy. A dash-free name makes
222+
# the two forms identical and the configured api.<domain> sticks.
223+
appserver:
224+
image: ghcr.io/ls1intum/hephaestus/application-server:${IMAGE_TAG:-latest}
191225
restart: unless-stopped
226+
expose:
227+
- "8080"
192228
environment:
193229
SPRING_PROFILES_ACTIVE: prod
194230
# prod runs forward-headers=native, so ProxyTrustGuard aborts the boot unless this is set.
195231
# Coolify fronts previews with its own proxy — set its ingress regex, not staging's range.
196-
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES:?Required}
232+
#
233+
# Write this Tomcat internal-proxies regex WITHOUT backslashes. Coolify re-escapes them when
234+
# it writes the stack's .env, so "172\.18\." arrives in the container as "172\\.18\\." — which
235+
# as a Java regex matches a literal backslash and therefore never matches an IP. The valve then
236+
# silently distrusts Traefik, X-Forwarded-Proto is ignored, and every OAuth redirect_uri is
237+
# built as http:// (which the IdP rejects). An unescaped '.' matches any char, which is
238+
# harmless here: e.g. 172.(1[6-9]|2[0-9]|3[01]).[0-9]{1,3}.[0-9]{1,3}
239+
HEPHAESTUS_TRUSTED_PROXIES: ${HEPHAESTUS_TRUSTED_PROXIES}
197240
# Previews run non-release images with no release-pin, so don't require a digest-pinned agent image.
198241
HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST: ${HEPHAESTUS_AGENT_IMAGE_REQUIRE_DIGEST:-false}
199-
# URL - Coolify provides SERVICE_FQDN_WEBAPP
242+
# The SPA's origin — NOT this server's. It feeds hephaestus.cors.allowed-origins,
243+
# hephaestus.webapp.url and the links the app hands to users.
200244
APPLICATION_HOST_URL: https://${SERVICE_FQDN_WEBAPP}
245+
# The API is served at the ROOT of its own host here (Coolify cannot route a compose service
246+
# on a path — it regenerates a host-only domain — so the SPA and the API get sibling hosts).
247+
# application-prod.yml pins api-base-path=/api for the production stack, where Traefik strips
248+
# that prefix. Nothing strips it here, so it must be cleared or every OAuth callback is built
249+
# as https://<api-host>/api/login/oauth2/code/... and 404s. An env var outranks the profile
250+
# YAML, and the callback's {baseUrl} comes from the request.
251+
HEPHAESTUS_AUTH_API_BASE_PATH: ""
201252
# Internal, trusted docker-network DB: skip TLS. pgJDBC doesn't validate Postgres' self-signed cert
202253
# so it adds no authentication, only the SSL handshake's NIO direct buffers that OOM the JVM off-heap
203254
# default; confidentiality is fine to drop here (creds already sit in plaintext compose env).
204-
DATABASE_URL: postgresql://postgres:5432/hephaestus?sslmode=disable
255+
# SERVICE_NAME_POSTGRES is Coolify's per-deploy service key, so a preview can only ever
256+
# reach its own database (see the note on the postgres service).
257+
DATABASE_URL: postgresql://${SERVICE_NAME_POSTGRES:-postgres}:5432/hephaestus?sslmode=disable
205258
DATABASE_USERNAME: hephaestus
206259
DATABASE_PASSWORD: ${POSTGRES_PASSWORD:-hephaestus-preview}
207-
# Shared infrastructure (must match shared-infra service names with UUID suffix)
208-
# When "Connect to Predefined Network" is enabled, use the full container names
260+
# Shared infrastructure, reached over Coolify's predefined `coolify` network.
209261
NATS_SERVER: nats://nats-server:4222
262+
# hephaestus.sync.nats.server and hephaestus.agent.nats.server BOTH default to ${NATS_SERVER},
263+
# so they must be pinned separately — otherwise aiming the integration consumer at another
264+
# environment's NATS would also submit this stack's agent jobs into that environment's AGENT
265+
# queue, where its workers would execute them.
266+
#
267+
# The integration consumer may point at an upstream NATS (e.g. staging's) to consume webhook
268+
# events that a shared GitHub App delivers there, instead of receiving deliveries locally.
269+
# durable-consumer-name is per-deploy (below), so this stack cannot steal that environment's
270+
# messages — it gets its own JetStream consumers.
271+
HEPHAESTUS_SYNC_NATS_SERVER: ${HEPHAESTUS_SYNC_NATS_SERVER:-nats://nats-server:4222}
272+
# Agent job queue stays local, always.
273+
HEPHAESTUS_AGENT_NATS_SERVER: nats://nats-server:4222
210274
NATS_ENABLED: ${NATS_ENABLED:-true}
211-
NATS_DURABLE_CONSUMER_NAME: ${COOLIFY_CONTAINER_NAME:-preview}-consumer
212-
# Auth (Hephaestus-native; ADR 0017).
213-
# Public URL for issuer validation (must match token's iss claim).
214-
HEPHAESTUS_AUTH_ISSUER: https://${PREVIEW_DOMAIN:?Required}
215-
HEPHAESTUS_AUTH_STATE_COOKIE_KEY: ${HEPHAESTUS_AUTH_STATE_COOKIE_KEY:?Required}
275+
# Per-deploy durable name: a shared one would make base and every preview compete for the
276+
# same JetStream consumer and steal each other's messages.
277+
NATS_DURABLE_CONSUMER_NAME: ${SERVICE_NAME_APPSERVER:-appserver}-consumer
278+
# Auth (Hephaestus-native; ADR 0017). Issuer is this deploy's own origin, so a preview's
279+
# tokens validate against the preview it was issued by.
280+
HEPHAESTUS_AUTH_ISSUER: https://${SERVICE_FQDN_WEBAPP}
281+
HEPHAESTUS_AUTH_STATE_COOKIE_KEY: ${HEPHAESTUS_AUTH_STATE_COOKIE_KEY}
282+
# First super-admin: the admin UI is admin-gated, so it must come from operator config.
283+
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS: ${HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS:-}
284+
# AES-256-GCM key (exactly 32 chars) for credentials encrypted at rest in `connection`.
285+
# Required in prod by EncryptedStringConverter + JwtSigningKeySealer, and the Liquibase
286+
# backfill re-encrypts on boot — so it MUST be present wherever Liquibase runs.
287+
HEPHAESTUS_SECURITY_ENCRYPTION_KEY: ${HEPHAESTUS_SECURITY_ENCRYPTION_KEY}
216288
# Sourced from GH_OAUTH_* — GitHub Actions reserves the GITHUB_ prefix for secret/variable names.
217289
GITHUB_OAUTH_CLIENT_ID: ${GH_OAUTH_CLIENT_ID:-}
218290
GITHUB_OAUTH_CLIENT_SECRET: ${GH_OAUTH_CLIENT_SECRET:-}
@@ -221,10 +293,52 @@ services:
221293
GITLAB_OAUTH_BASE_URL: ${GITLAB_OAUTH_BASE_URL:-https://gitlab.com}
222294
GITLAB_OAUTH_DISPLAY_NAME: ${GITLAB_OAUTH_DISPLAY_NAME:-GitLab}
223295
# GitHub integration
224-
GH_APP_ID: ${GH_APP_ID:?Required}
225-
GH_APP_PRIVATE_KEY: ${GH_APP_PRIVATE_KEY:?Required}
296+
GH_APP_ID: ${GH_APP_ID}
297+
GH_APP_PRIVATE_KEY: ${GH_APP_PRIVATE_KEY}
226298
GH_APP_INSTALLATION_URL: ${GH_APP_INSTALLATION_URL:-}
227-
GH_AUTH_TOKEN: ${GH_AUTH_TOKEN:?Required}
299+
GH_AUTH_TOKEN: ${GH_AUTH_TOKEN}
300+
# GitLab integration
301+
GITLAB_ENABLED: ${GITLAB_ENABLED:-false}
302+
GITLAB_DEFAULT_SERVER_URL: ${GITLAB_DEFAULT_SERVER_URL:-https://gitlab.lrz.de}
303+
HEPHAESTUS_FEATURES_FLAGS_GITLAB_WORKSPACE_CREATION: ${GITLAB_WORKSPACE_CREATION:-false}
304+
# Slack. OAuth/admin and mentor replies run here; inbound Events API + interactivity are
305+
# verified on webhook-server (shared infra), which needs the same signing secret.
306+
HEPHAESTUS_INTEGRATION_SLACK_ENABLED: ${HEPHAESTUS_INTEGRATION_SLACK_ENABLED:-false}
307+
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_ID: ${HEPHAESTUS_INTEGRATION_SLACK_CLIENT_ID:-}
308+
HEPHAESTUS_INTEGRATION_SLACK_CLIENT_SECRET: ${HEPHAESTUS_INTEGRATION_SLACK_CLIENT_SECRET:-}
309+
HEPHAESTUS_INTEGRATION_SLACK_SIGNING_SECRET: ${HEPHAESTUS_INTEGRATION_SLACK_SIGNING_SECRET:-}
310+
HEPHAESTUS_INTEGRATION_SLACK_REDIRECT_URI: ${HEPHAESTUS_INTEGRATION_SLACK_REDIRECT_URI:-}
311+
# Practice review + Pi mentor. Execution happens on application-worker; these gate it.
312+
PRACTICE_REVIEW_FOR_ALL: ${PRACTICE_REVIEW_FOR_ALL:-false}
313+
HEPHAESTUS_FEATURES_FLAGS_PRACTICE_REVIEW_FOR_ALL: ${PRACTICE_REVIEW_FOR_ALL:-false}
314+
AGENT_NATS_ENABLED: ${AGENT_NATS_ENABLED:-false}
315+
GIT_CHECKOUT_ENABLED: ${GIT_CHECKOUT_ENABLED:-false}
316+
GIT_STORAGE_PATH: /data/git-repos
317+
# Agent sandbox. There is no separate application-worker container here on purpose:
318+
# hephaestus.runtime.worker.enabled defaults to true, so this pod runs the worker role
319+
# in-process and executes practice reviews / Pi mentor turns itself. That only works if it
320+
# can reach the host Docker daemon — hence the docker.sock mount below (prod does the same).
321+
SANDBOX_DOCKER_HOST: ${SANDBOX_DOCKER_HOST:-unix:///var/run/docker.sock}
322+
# Previews share one host — keep the sandbox footprint small.
323+
SANDBOX_MAX_CONCURRENT: ${SANDBOX_MAX_CONCURRENT:-2}
324+
SANDBOX_MEMORY_BYTES: ${SANDBOX_MEMORY_BYTES:-4294967296}
325+
SANDBOX_CPUS: ${SANDBOX_CPUS:-2.0}
326+
# LLM proxy (OpenAI-compatible route). This is a verbatim path passthrough — the sandbox's
327+
# request path is appended to the base as-is, so give it the API ROOT and let the client add
328+
# /v1/chat/completions (same convention as the https://api.openai.com default).
329+
LLM_PROXY_OPENAI_URL: ${LLM_PROXY_OPENAI_URL:-https://api.openai.com}
330+
LLM_PROXY_OPENAI_AUTH_HEADER: ${LLM_PROXY_OPENAI_AUTH_HEADER:-Authorization}
331+
LLM_PROXY_OPENAI_USE_BEARER: ${LLM_PROXY_OPENAI_USE_BEARER:-true}
332+
# The LLM key + model are per-workspace (AgentConfig, encrypted at rest), not env-based.
333+
# A preview starts from an empty/seeded DB with no admin to fill that in, so seed a default
334+
# AgentConfig at boot — without a model name the Pi provider is never registered and the
335+
# mentor silently has no LLM.
336+
AGENT_DEFAULT_CONFIG_ENABLED: ${AGENT_DEFAULT_CONFIG_ENABLED:-false}
337+
AGENT_DEFAULT_CONFIG_NAME: ${AGENT_DEFAULT_CONFIG_NAME:-Default}
338+
AGENT_DEFAULT_CONFIG_PROVIDER: ${AGENT_DEFAULT_CONFIG_PROVIDER:-OPENAI}
339+
AGENT_DEFAULT_CONFIG_MODEL_NAME: ${AGENT_DEFAULT_CONFIG_MODEL_NAME:-}
340+
AGENT_DEFAULT_CONFIG_API_KEY: ${AGENT_DEFAULT_CONFIG_API_KEY:-}
341+
AGENT_DEFAULT_CONFIG_BASE_URL: ${AGENT_DEFAULT_CONFIG_BASE_URL:-}
228342
# Monitoring (relaxed for previews)
229343
MONITORING_TIMEFRAME: ${MONITORING_TIMEFRAME:-14}
230344
MONITORING_RUN_ON_STARTUP: ${MONITORING_RUN_ON_STARTUP:-false}
@@ -234,19 +348,25 @@ services:
234348
LEADERBOARD_NOTIFICATION_ENABLED: "false"
235349
# Observability (optional)
236350
SENTRY_DSN: ${SENTRY_DSN:-}
237-
# Webhook auto-registration. Override (not the APPLICATION_HOST_URL default) because previews
238-
# route webhooks via PREVIEW_DOMAIN, not the webapp's SERVICE_FQDN_WEBAPP. Bare origin:
239-
# GitLabWebhookService appends /webhooks/gitlab itself.
351+
# Webhook auto-registration. Points at the shared-infra receiver's own host, not this
352+
# deploy's origin: webhook-server lives in the shared-infra stack and is routed there.
353+
# Bare origin — GitLabWebhookService appends /webhooks/gitlab itself.
240354
WEBHOOK_SECRET: ${WEBHOOK_SECRET:-}
241-
WEBHOOK_EXTERNAL_URL: https://${PREVIEW_DOMAIN:?Required}
355+
WEBHOOK_EXTERNAL_URL: ${WEBHOOK_EXTERNAL_URL:-https://${PREVIEW_DOMAIN}}
242356
# thc healthcheck target: actuator liveness on the management port.
243357
THC_PORT: "8080"
244358
THC_PATH: /actuator/health/liveness
359+
volumes:
360+
- /var/run/docker.sock:/var/run/docker.sock
361+
- git-repos:/data/git-repos
362+
- appserver-logs:/var/log/hephaestus
245363
depends_on:
246364
postgres:
247365
condition: service_healthy
248366
seed-loader:
249367
condition: service_completed_successfully
368+
volume-init:
369+
condition: service_completed_successfully
250370
# thc healthcheck (see docker/compose.app.yaml).
251371
healthcheck:
252372
test: ["CMD", "/workspace/health-check"]
@@ -271,23 +391,24 @@ services:
271391
args:
272392
COOLIFY_BRANCH: ${COOLIFY_BRANCH:-}
273393
SOURCE_COMMIT: ${SOURCE_COMMIT:-}
274-
hostname: webapp
275394
restart: unless-stopped
276395
environment:
277396
APPLICATION_VERSION: preview
278397
# Coolify provides SERVICE_FQDN_WEBAPP automatically
279398
APPLICATION_CLIENT_URL: https://${SERVICE_FQDN_WEBAPP}
280399
GIT_BRANCH: ${COOLIFY_BRANCH:-}
281400
GIT_COMMIT: ${SOURCE_COMMIT:-}
282-
# Coolify provides SERVICE_FQDN_APPLICATION_SERVER (e.g., {{pr_id}}.api.example.com)
283-
APPLICATION_SERVER_URL: https://${SERVICE_FQDN_APPLICATION_SERVER}
401+
# The API's own host (Coolify assigns it per deploy). It is a sibling of the SPA host under
402+
# the same registrable domain, so the session cookie is same-site and rides XHR normally;
403+
# CORS allows this SPA origin via hephaestus.cors.allowed-origins=${APPLICATION_HOST_URL}.
404+
APPLICATION_SERVER_URL: https://${SERVICE_FQDN_APPSERVER}
284405
TANSTACK_DEVTOOLS_ENABLED: ${TANSTACK_DEVTOOLS_ENABLED:-true}
285406
# Optional
286407
SENTRY_ENVIRONMENT: preview
287408
SENTRY_DSN: ${SENTRY_DSN:-}
288409
POSTHOG_ENABLED: "false"
289410
depends_on:
290-
application-server:
411+
appserver:
291412
# Gate on app-server readiness so the SPA isn't served before the API answers.
292413
condition: service_healthy
293414
healthcheck:
@@ -307,3 +428,5 @@ services:
307428
# =============================================================================
308429
volumes:
309430
postgres-data:
431+
git-repos:
432+
appserver-logs:

0 commit comments

Comments
 (0)