diff --git a/lib/src/cli.rs b/lib/src/cli.rs
index 15b56bdb1..6c9e6a07d 100644
--- a/lib/src/cli.rs
+++ b/lib/src/cli.rs
@@ -24,7 +24,7 @@ use ostree_ext::ostree;
 use schemars::schema_for;
 use serde::{Deserialize, Serialize};
 
-use crate::deploy::RequiredHostSpec;
+use crate::deploy::{RequiredHostSpec, StageOptions};
 use crate::lints;
 use crate::progress_jsonl::{ProgressWriter, RawProgressFd};
 use crate::spec::Host;
@@ -77,6 +77,12 @@ pub(crate) struct UpgradeOpts {
     #[clap(long, conflicts_with = "check")]
     pub(crate) apply: bool,
 
+    /// Load the new deployment into kexec.
+    ///
+    /// Next time the system is rebooted, it will kexec instead of doing a full reboot
+    #[clap(long, conflicts_with = "check")]
+    pub(crate) kexec: bool,
+
     #[clap(flatten)]
     pub(crate) progress: ProgressOptions,
 }
@@ -96,6 +102,12 @@ pub(crate) struct SwitchOpts {
     #[clap(long)]
     pub(crate) apply: bool,
 
+    /// Load the new deployment into kexec.
+    ///
+    /// Next time the system is rebooted, it will kexec instead of doing a full reboot
+    #[clap(long)]
+    pub(crate) kexec: bool,
+
     /// The transport; e.g. oci, oci-archive, containers-storage.  Defaults to `registry`.
     #[clap(long, default_value = "registry")]
     pub(crate) transport: String,
@@ -792,7 +804,18 @@ async fn upgrade(opts: UpgradeOpts) -> Result<()> {
             println!("No update available.")
         } else {
             let osname = booted_deployment.osname();
-            crate::deploy::stage(sysroot, &osname, &fetched, &spec, prog.clone()).await?;
+            crate::deploy::stage(
+                sysroot,
+                &osname,
+                &fetched,
+                &spec,
+                prog.clone(),
+                StageOptions {
+                    deploy_kexec: opts.kexec,
+                    ..Default::default()
+                },
+            )
+            .await?;
             changed = true;
             if let Some(prev) = booted_image.as_ref() {
                 if let Some(fetched_manifest) = fetched.get_manifest(repo)? {
@@ -877,7 +900,18 @@ async fn switch(opts: SwitchOpts) -> Result<()> {
     }
 
     let stateroot = booted_deployment.osname();
-    crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
+    crate::deploy::stage(
+        sysroot,
+        &stateroot,
+        &fetched,
+        &new_spec,
+        prog.clone(),
+        StageOptions {
+            deploy_kexec: opts.kexec,
+            ..Default::default()
+        },
+    )
+    .await?;
 
     sysroot.update_mtime()?;
 
@@ -934,7 +968,15 @@ async fn edit(opts: EditOpts) -> Result<()> {
     // TODO gc old layers here
 
     let stateroot = booted_deployment.osname();
-    crate::deploy::stage(sysroot, &stateroot, &fetched, &new_spec, prog.clone()).await?;
+    crate::deploy::stage(
+        sysroot,
+        &stateroot,
+        &fetched,
+        &new_spec,
+        prog.clone(),
+        Default::default(),
+    )
+    .await?;
 
     sysroot.update_mtime()?;
 
diff --git a/lib/src/deploy.rs b/lib/src/deploy.rs
index 5d932b8b0..3d4a857fb 100644
--- a/lib/src/deploy.rs
+++ b/lib/src/deploy.rs
@@ -478,6 +478,27 @@ pub(crate) fn get_base_commit(repo: &ostree::Repo, commit: &str) -> Result<Optio
     Ok(r)
 }
 
+#[context("Loading deployment into kexec")]
+async fn kexec_load(sysroot: &Storage, deployment: &Deployment) -> Result<()> {
+    // Clone all the things to move to worker thread
+    let sysroot = sysroot.sysroot.clone();
+    // ostree::Deployment is incorrently !Send 😢 so convert it to an integer
+    let deployment_index = deployment.index() as usize;
+
+    async_task_with_spinner(
+        "Deploying",
+        spawn_blocking_cancellable_flatten(move |cancellable| -> Result<()> {
+            let deployments = sysroot.deployments();
+            let deployment = &deployments[deployment_index];
+
+            sysroot.deployment_kexec_load(&deployment, Some(cancellable))?;
+            Ok(())
+        }),
+    )
+    .await?;
+    Ok(())
+}
+
 #[context("Writing deployment")]
 async fn deploy(
     sysroot: &Storage,
@@ -552,6 +573,12 @@ fn origin_from_imageref(imgref: &ImageReference) -> Result<glib::KeyFile> {
     Ok(origin)
 }
 
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
+pub(crate) struct StageOptions {
+    pub(crate) deploy_kexec: bool,
+}
+
 /// Stage (queue deployment of) a fetched container image.
 #[context("Staging")]
 pub(crate) async fn stage(
@@ -560,7 +587,11 @@ pub(crate) async fn stage(
     image: &ImageState,
     spec: &RequiredHostSpec<'_>,
     prog: ProgressWriter,
+    StageOptions { deploy_kexec }: StageOptions,
 ) -> Result<()> {
+    let steps_total = 4 + (deploy_kexec as u64);
+    let mut steps = 0;
+
     let mut subtask = SubTaskStep {
         subtask: "merging".into(),
         description: "Merging Image".into(),
@@ -574,7 +605,7 @@ pub(crate) async fn stage(
         id: image.manifest_digest.clone().as_ref().into(),
         steps_cached: 0,
         steps: 0,
-        steps_total: 3,
+        steps_total,
         subtasks: subtasks
             .clone()
             .into_iter()
@@ -590,13 +621,14 @@ pub(crate) async fn stage(
     subtask.id = "deploying".into();
     subtask.description = "Deploying Image".into();
     subtask.completed = false;
+    steps += 1;
     prog.send(Event::ProgressSteps {
         task: "staging".into(),
         description: "Deploying Image".into(),
         id: image.manifest_digest.clone().as_ref().into(),
         steps_cached: 0,
-        steps: 1,
-        steps_total: 3,
+        steps,
+        steps_total,
         subtasks: subtasks
             .clone()
             .into_iter()
@@ -620,13 +652,14 @@ pub(crate) async fn stage(
     subtask.id = "bound_images".into();
     subtask.description = "Pulling Bound Images".into();
     subtask.completed = false;
+    steps += 1;
     prog.send(Event::ProgressSteps {
         task: "staging".into(),
         description: "Deploying Image".into(),
         id: image.manifest_digest.clone().as_ref().into(),
         steps_cached: 0,
-        steps: 1,
-        steps_total: 3,
+        steps,
+        steps_total,
         subtasks: subtasks
             .clone()
             .into_iter()
@@ -642,13 +675,14 @@ pub(crate) async fn stage(
     subtask.id = "cleanup".into();
     subtask.description = "Removing old images".into();
     subtask.completed = false;
+    steps += 1;
     prog.send(Event::ProgressSteps {
         task: "staging".into(),
         description: "Deploying Image".into(),
         id: image.manifest_digest.clone().as_ref().into(),
         steps_cached: 0,
-        steps: 2,
-        steps_total: 3,
+        steps,
+        steps_total,
         subtasks: subtasks
             .clone()
             .into_iter()
@@ -663,15 +697,42 @@ pub(crate) async fn stage(
     }
     println!("  Digest: {}", image.manifest_digest);
 
+    if deploy_kexec {
+        subtask.completed = true;
+        subtasks.push(subtask.clone());
+        subtask.subtask = "kexec".into();
+        subtask.id = "kexec".into();
+        subtask.description = "Loading image into kexec".into();
+        subtask.completed = false;
+        steps += 1;
+        prog.send(Event::ProgressSteps {
+            task: "staging".into(),
+            description: "Deploying Image".into(),
+            id: image.manifest_digest.clone().as_ref().into(),
+            steps_cached: 0,
+            steps,
+            steps_total,
+            subtasks: subtasks
+                .clone()
+                .into_iter()
+                .chain([subtask.clone()])
+                .collect(),
+        })
+        .await;
+        crate::deploy::kexec_load(sysroot, &deployment).await?;
+        println!("  Next reboot will kexec into this deployment!");
+    }
+
     subtask.completed = true;
     subtasks.push(subtask.clone());
+    steps += 1;
     prog.send(Event::ProgressSteps {
         task: "staging".into(),
         description: "Deploying Image".into(),
         id: image.manifest_digest.clone().as_ref().into(),
         steps_cached: 0,
-        steps: 3,
-        steps_total: 3,
+        steps,
+        steps_total,
         subtasks: subtasks
             .clone()
             .into_iter()