Fix SSH keys, git config, and HTTPS token not applied on container restart
All checks were successful
Build App / build-linux (push) Successful in 2m26s
Build App / build-windows (push) Successful in 3m17s

Recreate the container when SSH key path, git name, git email, or git
HTTPS token change — not just when the docker socket toggle changes.
The claude config named volume persists across recreation so no data
is lost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 19:37:06 +00:00
parent 60842befde
commit df3d434877
2 changed files with 76 additions and 23 deletions

View File

@@ -102,20 +102,16 @@ pub async fn start_project_container(
// Check for existing container // Check for existing container
let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? { let container_id = if let Some(existing_id) = docker::find_existing_container(&project).await? {
// Check if docker socket mount matches the current project setting. // Compare the running container's configuration (mounts, env vars)
// If the user toggled "Allow container spawning" after the container was // against the current project settings. If anything changed (SSH key
// created, we need to recreate the container for the mount change to take // path, git config, docker socket, etc.) we recreate the container.
// effect. // Safe to recreate: the claude config named volume is keyed by
let has_socket = docker::container_has_docker_socket(&existing_id).await.unwrap_or(false); // project ID (not container ID) so it persists across recreation.
if has_socket != project.allow_docker_access { let needs_recreation = docker::container_needs_recreation(&existing_id, &project)
log::info!( .await
"Docker socket mismatch (container has_socket={}, project wants={}), recreating container", .unwrap_or(false);
has_socket, project.allow_docker_access if needs_recreation {
); log::info!("Container config changed, recreating container for project {}", project.id);
// Safe to remove and recreate: the claude config named volume is
// keyed by project ID (not container ID) so it persists across
// container recreation. Bind mounts (workspace, SSH, AWS) are
// host paths and are unaffected.
let _ = docker::stop_container(&existing_id).await; let _ = docker::stop_container(&existing_id).await;
docker::remove_container(&existing_id).await?; docker::remove_container(&existing_id).await?;
let new_id = docker::create_container( let new_id = docker::create_container(

View File

@@ -288,25 +288,82 @@ pub async fn remove_container(container_id: &str) -> Result<(), String> {
.map_err(|e| format!("Failed to remove container: {}", e)) .map_err(|e| format!("Failed to remove container: {}", e))
} }
/// Check whether an existing container has docker socket mounted. /// Check whether the existing container's configuration still matches the
pub async fn container_has_docker_socket(container_id: &str) -> Result<bool, String> { /// current project settings. Returns `true` when the container must be
/// recreated (mounts or env vars differ).
pub async fn container_needs_recreation(container_id: &str, project: &Project) -> Result<bool, String> {
let docker = get_docker()?; let docker = get_docker()?;
let info = docker let info = docker
.inspect_container(container_id, None) .inspect_container(container_id, None)
.await .await
.map_err(|e| format!("Failed to inspect container: {}", e))?; .map_err(|e| format!("Failed to inspect container: {}", e))?;
let has_socket = info let mounts = info
.host_config .host_config
.and_then(|hc| hc.mounts) .as_ref()
.map(|mounts| { .and_then(|hc| hc.mounts.as_ref());
mounts.iter().any(|m| {
m.target.as_deref() == Some("/var/run/docker.sock") // ── Docker socket mount ──────────────────────────────────────────────
}) let has_socket = mounts
.map(|m| {
m.iter()
.any(|mount| mount.target.as_deref() == Some("/var/run/docker.sock"))
}) })
.unwrap_or(false); .unwrap_or(false);
if has_socket != project.allow_docker_access {
log::info!("Docker socket mismatch (container={}, project={})", has_socket, project.allow_docker_access);
return Ok(true);
}
Ok(has_socket) // ── SSH key path mount ───────────────────────────────────────────────
let ssh_mount_source = mounts
.and_then(|m| {
m.iter()
.find(|mount| mount.target.as_deref() == Some("/tmp/.host-ssh"))
})
.and_then(|mount| mount.source.as_deref());
let project_ssh = project.ssh_key_path.as_deref();
if ssh_mount_source != project_ssh {
log::info!(
"SSH key path mismatch (container={:?}, project={:?})",
ssh_mount_source,
project_ssh
);
return Ok(true);
}
// ── Git environment variables ────────────────────────────────────────
let env_vars = info
.config
.as_ref()
.and_then(|c| c.env.as_ref());
let get_env = |name: &str| -> Option<String> {
env_vars.and_then(|vars| {
vars.iter()
.find(|v| v.starts_with(&format!("{}=", name)))
.map(|v| v[name.len() + 1..].to_string())
})
};
let container_git_name = get_env("GIT_USER_NAME");
let container_git_email = get_env("GIT_USER_EMAIL");
let container_git_token = get_env("GIT_TOKEN");
if container_git_name.as_deref() != project.git_user_name.as_deref() {
log::info!("GIT_USER_NAME mismatch (container={:?}, project={:?})", container_git_name, project.git_user_name);
return Ok(true);
}
if container_git_email.as_deref() != project.git_user_email.as_deref() {
log::info!("GIT_USER_EMAIL mismatch (container={:?}, project={:?})", container_git_email, project.git_user_email);
return Ok(true);
}
if container_git_token.as_deref() != project.git_token.as_deref() {
log::info!("GIT_TOKEN mismatch");
return Ok(true);
}
Ok(false)
} }
pub async fn get_container_info(project: &Project) -> Result<Option<ContainerInfo>, String> { pub async fn get_container_info(project: &Project) -> Result<Option<ContainerInfo>, String> {