diff --git a/crates/navigator-bootstrap/src/docker.rs b/crates/navigator-bootstrap/src/docker.rs index 391b8ae82..e12af7cd3 100644 --- a/crates/navigator-bootstrap/src/docker.rs +++ b/crates/navigator-bootstrap/src/docker.rs @@ -9,7 +9,7 @@ use bollard::models::{ }; use bollard::query_parameters::{ CreateContainerOptions, CreateImageOptions, InspectContainerOptions, InspectNetworkOptions, - RemoveContainerOptions, RemoveVolumeOptions, StartContainerOptions, + RemoveContainerOptions, RemoveImageOptions, RemoveVolumeOptions, StartContainerOptions, }; use futures::StreamExt; use miette::{IntoDiagnostic, Result, WrapErr}; @@ -427,6 +427,15 @@ pub async fn destroy_cluster_resources( let container_name = container_name(name); let volume_name = volume_name(name); + // Capture the container's image reference before removing the container so + // we can clean it up afterwards. This prevents stale images from being + // re-used on subsequent deploys. + let container_image = docker + .inspect_container(&container_name, None::) + .await + .ok() + .and_then(|info| info.image); + let _ = stop_container(docker, &container_name).await; let remove_container = docker @@ -444,6 +453,28 @@ pub async fn destroy_cluster_resources( return Err(err).into_diagnostic(); } + // Remove the cluster image so the next deploy always pulls the latest + // version from the registry instead of reusing a stale local copy. + if let Some(ref image_id) = container_image { + tracing::debug!("Removing cluster image: {}", image_id); + let result = docker + .remove_image( + image_id, + Some(RemoveImageOptions { + force: true, + noprune: true, + ..Default::default() + }), + None, + ) + .await; + if let Err(err) = result + && !is_not_found(&err) + { + tracing::warn!("Failed to remove cluster image {}: {}", image_id, err); + } + } + let remove_volume = docker .remove_volume(&volume_name, Some(RemoveVolumeOptions { force: true })) .await; diff --git a/crates/navigator-bootstrap/src/image.rs b/crates/navigator-bootstrap/src/image.rs index 150c6e01e..4e2d178ca 100644 --- a/crates/navigator-bootstrap/src/image.rs +++ b/crates/navigator-bootstrap/src/image.rs @@ -252,6 +252,29 @@ pub async fn pull_remote_image( format!("failed to tag {registry_image} as {target_repo}:{target_tag} on remote") })?; + // Verify that the pulled image matches the expected architecture. + // This catches cases where the registry returned the wrong platform + // variant (e.g., amd64 on an arm64 host) which would cause an + // "exec format error" at container start time. + let inspect = remote + .inspect_image(image_ref) + .await + .into_diagnostic() + .wrap_err_with(|| format!("failed to inspect pulled image {image_ref} on remote"))?; + + let actual_arch = inspect.architecture.as_deref().unwrap_or("unknown"); + if actual_arch != remote_platform.arch { + return Err(miette::miette!( + "architecture mismatch: pulled image {image_ref} is {actual_arch} but remote host is {expected}; \ + try removing stale images on the remote host and re-deploying", + expected = remote_platform.arch, + )); + } + info!( + "Verified image architecture: {} matches remote host", + actual_arch + ); + on_progress(format!("[status] Image {image_ref} ready on remote host")); info!("Remote image pull and tag complete: {}", image_ref);