From 843e7823400c4470e66aabad8ba0281b1e00e804 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:47:28 +0200 Subject: [PATCH] chore(runtime): point shipped image defaults to registry.cameleer.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Customers running this server with no overrides reach the public registry alias, not the internal hostname. registry.cameleer.io and gitea.siegeln.net resolve to the same registry — buildtime CI keeps pushing to gitea.siegeln.net, runtime defaults pull via the public alias. - application.yml: baseimage, loaderimage defaults - DeploymentExecutor.java: matching @Value defaults - docker-orchestration.md: updates the documented default and notes the buildtime/public split so future changes don't "fix" the asymmetry Out of scope (intentionally still on gitea.siegeln.net): - LoaderHardeningIT and the two DockerRuntimeOrchestrator unit tests. Tests are buildtime artifacts; LoaderHardeningIT pulls the real image via CI's pre-authenticated docker login to gitea.siegeln.net. - deploy/base/*.yaml and deploy/overlays/main/*.yaml (internal k3s, customers don't use these manifests). - pom.xml, .npmrc, ui/Dockerfile (build dependency sources). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules/docker-orchestration.md | 2 +- .../io/cameleer/server/app/runtime/DeploymentExecutor.java | 4 ++-- cameleer-server-app/src/main/resources/application.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/rules/docker-orchestration.md b/.claude/rules/docker-orchestration.md index 37b147dc..da22d9c8 100644 --- a/.claude/rules/docker-orchestration.md +++ b/.claude/rules/docker-orchestration.md @@ -41,7 +41,7 @@ When deployed via the cameleer-saas platform, this server orchestrates customer `startContainer` is now a two-phase op per replica: 1. **Volume create** — `cameleer-jars-{containerName}` named volume (per-replica, deterministic so cleanup in `removeContainer` can derive it). -2. **Loader container** — `loaderImage` (default `gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest`, **built and published by the cameleer-saas repo** at `docker/runtime-loader/`), name `{containerName}-loader`, mount the volume **RW at `/app/jars`**, env vars `ARTIFACT_URL` + `ARTIFACT_EXPECTED_SIZE`. Loader downloads the JAR from the signed URL into the volume and exits 0. Orchestrator blocks on `waitContainerCmd().exec(WaitContainerResultCallback).awaitStatusCode(120, SECONDS)`. Loader container is removed in a `finally` block; on non-zero exit the volume is also removed and `RuntimeException` propagates so `DeploymentExecutor` marks the deployment FAILED. **Loader logs are captured before removal** (`captureLoaderLogs` — `logContainerCmd` with `withTail(50)`, capped at 4096 chars, 5s timeout) and appended to the thrown `RuntimeException` message as `". loader output: "`. Best-effort: log-capture failures are swallowed and don't mask the original exit. The loader image's Dockerfile pre-creates `/app/jars` owned by `loader:loader` (UID 1000) so the orchestrator's fresh named volume initialises with that ownership — without it the empty volume comes up as `root:root 0755` and wget exits 1 with "Permission denied". `LoaderHardeningIT` is the cross-repo contract test (pulls the published `:latest` and asserts exit 0 under the orchestrator's hardening shape). +2. **Loader container** — `loaderImage` (default `registry.cameleer.io/cameleer/cameleer-runtime-loader:latest`, **built and published by the cameleer-saas repo** at `docker/runtime-loader/`; CI pushes to `gitea.siegeln.net` under the same path — both names resolve to the same registry), name `{containerName}-loader`, mount the volume **RW at `/app/jars`**, env vars `ARTIFACT_URL` + `ARTIFACT_EXPECTED_SIZE`. Loader downloads the JAR from the signed URL into the volume and exits 0. Orchestrator blocks on `waitContainerCmd().exec(WaitContainerResultCallback).awaitStatusCode(120, SECONDS)`. Loader container is removed in a `finally` block; on non-zero exit the volume is also removed and `RuntimeException` propagates so `DeploymentExecutor` marks the deployment FAILED. **Loader logs are captured before removal** (`captureLoaderLogs` — `logContainerCmd` with `withTail(50)`, capped at 4096 chars, 5s timeout) and appended to the thrown `RuntimeException` message as `". loader output: "`. Best-effort: log-capture failures are swallowed and don't mask the original exit. The loader image's Dockerfile pre-creates `/app/jars` owned by `loader:loader` (UID 1000) so the orchestrator's fresh named volume initialises with that ownership — without it the empty volume comes up as `root:root 0755` and wget exits 1 with "Permission denied". `LoaderHardeningIT` is the cross-repo contract test (pulls the published `:latest` and asserts exit 0 under the orchestrator's hardening shape). 3. **Main container** — same hardening contract, mount the same volume **RO at `/app/jars`**, entrypoint reads `/app/jars/app.jar` (Spring Boot/Quarkus: `-jar /app/jars/app.jar`; plain Java: `-cp /app/jars/app.jar `; native: `exec /app/jars/app.jar`). `removeContainer(id)` derives the volume name from the inspected container name (Docker prefixes it with `/`) and removes the volume after the container removes — blue/green doesn't leak volumes. diff --git a/cameleer-server-app/src/main/java/io/cameleer/server/app/runtime/DeploymentExecutor.java b/cameleer-server-app/src/main/java/io/cameleer/server/app/runtime/DeploymentExecutor.java index 88c34e33..5d2c054a 100644 --- a/cameleer-server-app/src/main/java/io/cameleer/server/app/runtime/DeploymentExecutor.java +++ b/cameleer-server-app/src/main/java/io/cameleer/server/app/runtime/DeploymentExecutor.java @@ -36,7 +36,7 @@ public class DeploymentExecutor { @Autowired(required = false) private DockerNetworkManager networkManager; - @Value("${cameleer.server.runtime.baseimage:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest}") + @Value("${cameleer.server.runtime.baseimage:registry.cameleer.io/cameleer/cameleer-runtime-base:latest}") private String baseImage; @Value("${cameleer.server.runtime.dockernetwork:cameleer}") @@ -69,7 +69,7 @@ public class DeploymentExecutor { @Value("${cameleer.server.runtime.certresolver:}") private String globalCertResolver; - @Value("${cameleer.server.runtime.loaderimage:gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest}") + @Value("${cameleer.server.runtime.loaderimage:registry.cameleer.io/cameleer/cameleer-runtime-loader:latest}") private String loaderImage; @Value("${cameleer.server.runtime.artifacttokenttlseconds:600}") diff --git a/cameleer-server-app/src/main/resources/application.yml b/cameleer-server-app/src/main/resources/application.yml index 91d72085..88cca4b0 100644 --- a/cameleer-server-app/src/main/resources/application.yml +++ b/cameleer-server-app/src/main/resources/application.yml @@ -45,7 +45,7 @@ cameleer: runtime: enabled: ${CAMELEER_SERVER_RUNTIME_ENABLED:true} jarstoragepath: ${CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH:/data/jars} - baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-base:latest} + baseimage: ${CAMELEER_SERVER_RUNTIME_BASEIMAGE:registry.cameleer.io/cameleer/cameleer-runtime-base:latest} dockernetwork: ${CAMELEER_SERVER_RUNTIME_DOCKERNETWORK:cameleer} # Container runtime override. Empty (default) auto-detects: uses runsc # (gVisor) if the daemon has it registered, otherwise the daemon default @@ -65,7 +65,7 @@ cameleer: # short-lived sidecar that downloads the JAR from a signed URL into a # per-replica named volume, which the main container then mounts RO at # /app/jars. See issue #152 close-out + .claude/rules/docker-orchestration.md. - loaderimage: ${CAMELEER_SERVER_RUNTIME_LOADERIMAGE:gitea.siegeln.net/cameleer/cameleer-runtime-loader:latest} + loaderimage: ${CAMELEER_SERVER_RUNTIME_LOADERIMAGE:registry.cameleer.io/cameleer/cameleer-runtime-loader:latest} artifacttokenttlseconds: ${CAMELEER_SERVER_RUNTIME_ARTIFACTTOKENTTLSECONDS:600} artifactbaseurl: ${CAMELEER_SERVER_RUNTIME_ARTIFACTBASEURL:} indexer: