feat: support Docker volume-based JAR mounting for Docker-in-Docker
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:
@@ -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(),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user