feat: support Docker volume-based JAR mounting for Docker-in-Docker
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m27s
CI / docker (push) Successful in 1m8s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

When CAMELEER_JAR_DOCKER_VOLUME is set, the orchestrator mounts the
named volume at the jar storage path instead of using a host bind mount.
This solves the path translation issue in Docker-in-Docker setups where
the server runs inside a container and manages sibling containers.

The entrypoint is overridden to use the volume-mounted JAR path via
the CAMELEER_APP_JAR env var.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-08 21:38:34 +02:00
parent c923d8233b
commit 20f3dfe59d
4 changed files with 45 additions and 4 deletions

View File

@@ -58,6 +58,12 @@ public class DeploymentExecutor {
@Value("${cameleer.runtime.server-url:}")
private String globalServerUrl;
@Value("${cameleer.runtime.jar-docker-volume:}")
private String jarDockerVolume;
@Value("${cameleer.runtime.jar-storage-path:/data/jars}")
private String jarStoragePath;
public DeploymentExecutor(RuntimeOrchestrator orchestrator,
DeploymentService deploymentService,
AppService appService,
@@ -122,8 +128,11 @@ public class DeploymentExecutor {
String containerName = env.slug() + "-" + app.slug() + "-" + i;
Long cpuQuota = config.cpuLimit() != null ? (long) (config.cpuLimit() * 100_000) : null;
String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null;
ContainerRequest request = new ContainerRequest(
containerName, baseImage, jarPath, primaryNetwork,
containerName, baseImage, jarPath,
volumeName, jarStoragePath,
primaryNetwork,
envNet != null ? List.of(envNet) : List.of(),
baseEnvVars, labels,
config.memoryLimitBytes(), config.memoryReserveBytes(),

View File

@@ -60,16 +60,23 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
List<String> envList = request.envVars().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()).toList();
Bind jarBind = new Bind(request.jarPath(), new Volume("/app/app.jar"), AccessMode.ro);
HostConfig hostConfig = HostConfig.newHostConfig()
.withMemory(request.memoryLimitBytes())
.withMemorySwap(request.memoryLimitBytes())
.withCpuShares(request.cpuShares())
.withNetworkMode(request.network())
.withBinds(jarBind)
.withRestartPolicy(RestartPolicy.onFailureRestart(request.restartPolicyMaxRetries()));
// JAR mounting: volume mount (Docker-in-Docker) or bind mount (host path)
if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
// Mount the named volume at the jar storage base path
Bind volumeBind = new Bind(request.jarVolumeName(), new Volume(request.jarVolumeMountPath()), AccessMode.ro);
hostConfig.withBinds(volumeBind);
} else {
Bind jarBind = new Bind(request.jarPath(), new Volume("/app/app.jar"), AccessMode.ro);
hostConfig.withBinds(jarBind);
}
if (request.memoryReserveBytes() != null) {
hostConfig.withMemoryReservation(request.memoryReserveBytes());
}
@@ -77,6 +84,12 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
hostConfig.withCpuQuota(request.cpuQuota());
}
// When using volume mount, the JAR is at the original path, not /app/app.jar
if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
envList = new ArrayList<>(envList);
envList.add("CAMELEER_APP_JAR=" + request.jarPath());
}
var createCmd = dockerClient.createContainerCmd(request.baseImage())
.withName(request.containerName())
.withEnv(envList)
@@ -90,6 +103,22 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
.withRetries(3)
.withStartPeriod(30_000_000_000L));
// Override entrypoint to use the volume-mounted JAR path
if (request.jarVolumeName() != null && !request.jarVolumeName().isBlank()) {
createCmd.withEntrypoint("sh", "-c",
"exec java -javaagent:/app/agent.jar " +
"-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} " +
"-Dcameleer.export.endpoint=${CAMELEER_EXPORT_ENDPOINT} " +
"-Dcameleer.agent.name=${HOSTNAME} " +
"-Dcameleer.agent.application=${CAMELEER_APPLICATION_ID:-default} " +
"-Dcameleer.agent.environment=${CAMELEER_ENVIRONMENT_ID:-default} " +
"-Dcameleer.routeControl.enabled=${CAMELEER_ROUTE_CONTROL_ENABLED:-false} " +
"-Dcameleer.replay.enabled=${CAMELEER_REPLAY_ENABLED:-false} " +
"-Dcameleer.health.enabled=true " +
"-Dcameleer.health.port=9464 " +
"-jar ${CAMELEER_APP_JAR}");
}
if (request.exposedPorts() != null && !request.exposedPorts().isEmpty()) {
var ports = request.exposedPorts().stream()
.map(p -> com.github.dockerjava.api.model.ExposedPort.tcp(p))

View File

@@ -52,6 +52,7 @@ cameleer:
routing-mode: ${CAMELEER_ROUTING_MODE:path}
routing-domain: ${CAMELEER_ROUTING_DOMAIN:localhost}
server-url: ${CAMELEER_SERVER_URL:}
jar-docker-volume: ${CAMELEER_JAR_DOCKER_VOLUME:}
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}
indexer:
debounce-ms: ${CAMELEER_INDEXER_DEBOUNCE_MS:2000}

View File

@@ -7,6 +7,8 @@ public record ContainerRequest(
String containerName,
String baseImage,
String jarPath,
String jarVolumeName,
String jarVolumeMountPath,
String network,
List<String> additionalNetworks,
Map<String, String> envVars,