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:}")
|
@Value("${cameleer.runtime.server-url:}")
|
||||||
private String globalServerUrl;
|
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,
|
public DeploymentExecutor(RuntimeOrchestrator orchestrator,
|
||||||
DeploymentService deploymentService,
|
DeploymentService deploymentService,
|
||||||
AppService appService,
|
AppService appService,
|
||||||
@@ -122,8 +128,11 @@ public class DeploymentExecutor {
|
|||||||
String containerName = env.slug() + "-" + app.slug() + "-" + i;
|
String containerName = env.slug() + "-" + app.slug() + "-" + i;
|
||||||
Long cpuQuota = config.cpuLimit() != null ? (long) (config.cpuLimit() * 100_000) : null;
|
Long cpuQuota = config.cpuLimit() != null ? (long) (config.cpuLimit() * 100_000) : null;
|
||||||
|
|
||||||
|
String volumeName = jarDockerVolume != null && !jarDockerVolume.isBlank() ? jarDockerVolume : null;
|
||||||
ContainerRequest request = new ContainerRequest(
|
ContainerRequest request = new ContainerRequest(
|
||||||
containerName, baseImage, jarPath, primaryNetwork,
|
containerName, baseImage, jarPath,
|
||||||
|
volumeName, jarStoragePath,
|
||||||
|
primaryNetwork,
|
||||||
envNet != null ? List.of(envNet) : List.of(),
|
envNet != null ? List.of(envNet) : List.of(),
|
||||||
baseEnvVars, labels,
|
baseEnvVars, labels,
|
||||||
config.memoryLimitBytes(), config.memoryReserveBytes(),
|
config.memoryLimitBytes(), config.memoryReserveBytes(),
|
||||||
|
|||||||
@@ -60,16 +60,23 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
|
|||||||
List<String> envList = request.envVars().entrySet().stream()
|
List<String> envList = request.envVars().entrySet().stream()
|
||||||
.map(e -> e.getKey() + "=" + e.getValue()).toList();
|
.map(e -> e.getKey() + "=" + e.getValue()).toList();
|
||||||
|
|
||||||
Bind jarBind = new Bind(request.jarPath(), new Volume("/app/app.jar"), AccessMode.ro);
|
|
||||||
|
|
||||||
HostConfig hostConfig = HostConfig.newHostConfig()
|
HostConfig hostConfig = HostConfig.newHostConfig()
|
||||||
.withMemory(request.memoryLimitBytes())
|
.withMemory(request.memoryLimitBytes())
|
||||||
.withMemorySwap(request.memoryLimitBytes())
|
.withMemorySwap(request.memoryLimitBytes())
|
||||||
.withCpuShares(request.cpuShares())
|
.withCpuShares(request.cpuShares())
|
||||||
.withNetworkMode(request.network())
|
.withNetworkMode(request.network())
|
||||||
.withBinds(jarBind)
|
|
||||||
.withRestartPolicy(RestartPolicy.onFailureRestart(request.restartPolicyMaxRetries()));
|
.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) {
|
if (request.memoryReserveBytes() != null) {
|
||||||
hostConfig.withMemoryReservation(request.memoryReserveBytes());
|
hostConfig.withMemoryReservation(request.memoryReserveBytes());
|
||||||
}
|
}
|
||||||
@@ -77,6 +84,12 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
|
|||||||
hostConfig.withCpuQuota(request.cpuQuota());
|
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())
|
var createCmd = dockerClient.createContainerCmd(request.baseImage())
|
||||||
.withName(request.containerName())
|
.withName(request.containerName())
|
||||||
.withEnv(envList)
|
.withEnv(envList)
|
||||||
@@ -90,6 +103,22 @@ public class DockerRuntimeOrchestrator implements RuntimeOrchestrator {
|
|||||||
.withRetries(3)
|
.withRetries(3)
|
||||||
.withStartPeriod(30_000_000_000L));
|
.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()) {
|
if (request.exposedPorts() != null && !request.exposedPorts().isEmpty()) {
|
||||||
var ports = request.exposedPorts().stream()
|
var ports = request.exposedPorts().stream()
|
||||||
.map(p -> com.github.dockerjava.api.model.ExposedPort.tcp(p))
|
.map(p -> com.github.dockerjava.api.model.ExposedPort.tcp(p))
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ cameleer:
|
|||||||
routing-mode: ${CAMELEER_ROUTING_MODE:path}
|
routing-mode: ${CAMELEER_ROUTING_MODE:path}
|
||||||
routing-domain: ${CAMELEER_ROUTING_DOMAIN:localhost}
|
routing-domain: ${CAMELEER_ROUTING_DOMAIN:localhost}
|
||||||
server-url: ${CAMELEER_SERVER_URL:}
|
server-url: ${CAMELEER_SERVER_URL:}
|
||||||
|
jar-docker-volume: ${CAMELEER_JAR_DOCKER_VOLUME:}
|
||||||
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}
|
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}
|
||||||
indexer:
|
indexer:
|
||||||
debounce-ms: ${CAMELEER_INDEXER_DEBOUNCE_MS:2000}
|
debounce-ms: ${CAMELEER_INDEXER_DEBOUNCE_MS:2000}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ public record ContainerRequest(
|
|||||||
String containerName,
|
String containerName,
|
||||||
String baseImage,
|
String baseImage,
|
||||||
String jarPath,
|
String jarPath,
|
||||||
|
String jarVolumeName,
|
||||||
|
String jarVolumeMountPath,
|
||||||
String network,
|
String network,
|
||||||
List<String> additionalNetworks,
|
List<String> additionalNetworks,
|
||||||
Map<String, String> envVars,
|
Map<String, String> envVars,
|
||||||
|
|||||||
Reference in New Issue
Block a user