diff --git a/make.rs b/make.rs
index 4a3cf7621a22e3f83eba00bd20aa76f5299e17d9..ec5dba9008f24259bbe35c61d89f6356c8aa94a9 100755
--- a/make.rs
+++ b/make.rs
@@ -8,10 +8,16 @@
 //! [dependencies]
 //! nob = { git = "https://gitlab.isae-supaero.fr/a.stevan/nob.rs", rev = "7ea6be855cf5600558440def6e59a83f78b8b543" }
 //! clap = { version = "4.5.17", features = ["derive"] }
+//!
+//! # for `container --list`
+//! serde = { version = "1.0", features = ["derive"] }
+//! serde_json = "1.0"
+//! prettytable = "0.10.0"
 //! ```
-extern crate clap;
 
 use clap::{Parser, Subcommand};
+use prettytable::{format, Cell, Row, Table};
+use serde_json::Value;
 
 const REGISTRY: &str = "gitlab-registry.isae-supaero.fr";
 const MIRROR_REGISTRY: &str = "ghcr.io/dragoon-rs";
@@ -61,17 +67,26 @@ enum Commands {
         features: bool,
     },
     /// Builds the container.
-    Container {
-        /// Log into the registry instead of building.
-        #[arg(short, long)]
-        login: bool,
-        /// Push to the registry instead of building.
-        #[arg(short, long)]
-        push: bool,
+    #[command(subcommand)]
+    Container(ContainerCommands),
+}
+
+#[derive(Subcommand)]
+enum ContainerCommands {
+    /// Build the current dockerfile.
+    Build,
+    /// List the local images.
+    List {
+        /// Print the output table as NDJSON instead of pretty table.
+        #[arg(long)]
+        json: bool,
     },
+    /// Log into the registry instead of building.
+    Login,
+    /// Push to the registry instead of building.
+    Push,
 }
 
-#[rustfmt::skip]
 fn main() {
     let cli = Cli::parse();
 
@@ -84,10 +99,11 @@ fn main() {
             }
         }
         Some(Commands::Check) => {
-            nob::run_cmd_and_fail!("cargo", "check", "--workspace", "--all-targets");
-            nob::run_cmd_and_fail!("cargo", "check", "--workspace", "--all-targets", "--features", "kzg");
-            nob::run_cmd_and_fail!("cargo", "check", "--workspace", "--all-targets", "--features", "aplonk");
-            nob::run_cmd_and_fail!("cargo", "check", "--workspace", "--all-targets", "--all-features");
+            let cmd = vec!["cargo", "check", "--workspace", "--all-targets"];
+            extend_and_run(&cmd, &[]);
+            extend_and_run(&cmd, &["--features", "kzg"]);
+            extend_and_run(&cmd, &["--features", "aplonk"]);
+            extend_and_run(&cmd, &["--all-features"]);
         }
         Some(Commands::Clippy) => {
             nob::run_cmd_and_fail!(
@@ -104,7 +120,9 @@ fn main() {
         Some(Commands::Test { verbose, examples }) => {
             let mut cmd = vec!["cargo", "test"];
 
-            if *verbose { cmd.push("--verbose") }
+            if *verbose {
+                cmd.push("--verbose")
+            }
             if *examples {
                 cmd.push("--examples");
             } else {
@@ -127,40 +145,96 @@ fn main() {
             features,
         }) => {
             let mut cmd = vec!["cargo", "doc", "--no-deps"];
-            if *open { cmd.push("--open") }
-            if *private { cmd.push("--document-private-items") }
-            if *features { cmd.push("--all-features") }
+            if *open {
+                cmd.push("--open")
+            }
+            if *private {
+                cmd.push("--document-private-items")
+            }
+            if *features {
+                cmd.push("--all-features")
+            }
             nob::run_cmd_as_vec_and_fail!(cmd ; "RUSTDOCFLAGS" => "--html-in-header katex.html");
         }
-        Some(Commands::Container { login, push }) => {
+        Some(Commands::Container(container_cmd)) => {
             let res = nob::run_cmd_and_fail!(@+"git", "rev-parse", "HEAD");
             let sha = String::from_utf8(res.stdout).expect("Invalid UTF-8 string");
-            let image = format!("{}/{}:{}", REGISTRY, IMAGE, sha.trim());
-            let mirror_image = format!("{}/{}:{}", MIRROR_REGISTRY, IMAGE, sha.trim());
-
-            if *login {
-                nob::run_cmd_and_fail!("docker", "login", REGISTRY);
-                nob::run_cmd_and_fail!("docker", "login", MIRROR_REGISTRY);
-            } else if *push {
-                nob::run_cmd_and_fail!("docker", "push", &image);
-                nob::run_cmd_and_fail!("docker", "push", &mirror_image);
-            } else {
-                nob::run_cmd_and_fail!(
-                    "docker",
-                    "build",
-                    "-t", &image,
-                    ".",
-                    "--file", DOCKERFILE
-                );
-                nob::run_cmd_and_fail!(
-                    "docker",
-                    "build",
-                    "-t", &mirror_image,
-                    ".",
-                    "--file", DOCKERFILE
-                );
+
+            let repo = format!("{}/{}", REGISTRY, IMAGE);
+            let image = format!("{}:{}", repo, sha.trim());
+
+            let mirror_repo = format!("{}/{}", MIRROR_REGISTRY, IMAGE);
+            let mirror_image = format!("{}:{}", mirror_repo, sha.trim());
+
+            match container_cmd {
+                ContainerCommands::Login => {
+                    nob::run_cmd_and_fail!("docker", "login", REGISTRY);
+                    nob::run_cmd_and_fail!("docker", "login", MIRROR_REGISTRY);
+                }
+                ContainerCommands::Build => {
+                    let cmd = vec!["docker", "build", ".", "--file", DOCKERFILE];
+                    extend_and_run(&cmd, &["-t", &image]);
+                    extend_and_run(&cmd, &["-t", &mirror_image]);
+                }
+                ContainerCommands::List { json } => {
+                    let cmd = vec!["docker", "image", "list", "--format", "json"];
+                    let images = extend_and_run_and_capture_silent(&cmd, &[&repo])
+                        + &extend_and_run_and_capture_silent(&cmd, &[&mirror_repo]);
+
+                    if *json {
+                        println!("{}", images);
+                    } else {
+                        docker_images_to_table(images).printstd();
+                    }
+                }
+                ContainerCommands::Push => {
+                    nob::run_cmd_and_fail!("docker", "push", &image);
+                    nob::run_cmd_and_fail!("docker", "push", &mirror_image);
+                }
             }
         }
         None => {}
     }
 }
+
+fn docker_images_to_table(lines: String) -> Table {
+    let mut rows: Vec<Vec<String>> = Vec::new();
+    let mut headers: Vec<String> = Vec::new();
+    for line in lines.lines() {
+        let json: Value = serde_json::from_str(&line).unwrap_or_else(|_| Value::Null);
+        if let Value::Object(map) = serde_json::from_str(&line).unwrap_or_else(|_| Value::Null) {
+            if headers.is_empty() {
+                headers = map.keys().cloned().collect();
+            }
+
+            let row: Vec<String> = headers
+                .iter()
+                .map(|key| map.get(key).map_or("".to_string(), |v| v.to_string()))
+                .collect();
+            rows.push(row);
+        }
+    }
+
+    let mut table = Table::new();
+    table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
+    table.set_titles(Row::new(headers.iter().map(|h| Cell::new(h)).collect()));
+    for row in rows {
+        table.add_row(Row::new(row.iter().map(|v| Cell::new(v)).collect()));
+    }
+
+    table
+}
+
+// NOTE: this could be migrated to [`nob.rs`](https://gitlab.isae-supaero.fr/a.stevan/nob.rs)
+fn extend_and_run(cmd: &[&str], args: &[&str]) {
+    let mut cmd = cmd.to_vec();
+    cmd.extend_from_slice(&args);
+    nob::run_cmd_as_vec_and_fail!(cmd);
+}
+
+// NOTE: this could be migrated to [`nob.rs`](https://gitlab.isae-supaero.fr/a.stevan/nob.rs)
+fn extend_and_run_and_capture_silent(cmd: &[&str], args: &[&str]) -> String {
+    let mut cmd = cmd.to_vec();
+    cmd.extend_from_slice(&args);
+    String::from_utf8(nob::run_cmd_as_vec_and_fail!(@+cmd).stdout).expect("Invalid UTF-8 string")
+}