diff --git a/.env.dockerfile b/.env.dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..791a1cc0c0fabe1a9c31cb355ea739fe220b987a
--- /dev/null
+++ b/.env.dockerfile
@@ -0,0 +1,14 @@
+FROM rust:latest
+
+# Suppress prompts during package installation
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt update --yes && apt upgrade --yes
+RUN apt install --yes protobuf-compiler
+
+COPY rust-toolchain.toml /
+RUN rustup show && cargo --version
+
+RUN cargo install cargo-script
+
+RUN apt clean && rm -rf /var/lib/apt/lists/*
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 89dd078b92c57d81d8bf323070b4be89a16acd5d..ef20985fe4fef0f40cff769c02046a99e2fece14 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,32 +5,23 @@ on: [push, pull_request, workflow_dispatch]
 jobs:
   fmt:
     runs-on: ubuntu-latest
+    container:
+      image: "ghcr.io/dragoon-rs/dragoon/komodo:bcb0e6b5f73420762f6208700a43291e0066c2c3"
     if: "!contains(github.event.head_commit.message, 'draft:') && !contains(github.event.head_commit.message, 'no-ci:')"
     steps:
       - uses: actions/checkout@v3
-      - name: Set up Rust
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: stable
-      - name: Install dependencies
-        run: |
-          cargo install cargo-script
       - name: Run fmt check
         run: |
           ./make.rs fmt --check
 
   test:
     runs-on: ubuntu-latest
+    container:
+      image: "ghcr.io/dragoon-rs/dragoon/komodo:bcb0e6b5f73420762f6208700a43291e0066c2c3"
     needs: fmt
     if: "!contains(github.event.head_commit.message, 'draft:') && !contains(github.event.head_commit.message, 'no-ci:')"
     steps:
       - uses: actions/checkout@v3
-      - name: Install dependencies
-        run: |
-          sudo apt update --yes
-          sudo apt upgrade --yes
-          sudo apt install protobuf-compiler --yes
-          cargo install cargo-script
       - name: Show configuration
         run: |
           ./make.rs version
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d44462d5e90e9ab805ca2a8f12d319e4bcf3b1c4..fe3fff7b6d3a1b293dfa63c19ff07bf244973b24 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,4 @@
-# WARNING: update `rust-toolchain.toml` as well
-image: "rust:1.78"
+image: "gitlab-registry.isae-supaero.fr/dragoon/komodo:bcb0e6b5f73420762f6208700a43291e0066c2c3"
 
 stages:
   - fmt
@@ -16,8 +15,6 @@ workflow:
 
 fmt:
   stage: fmt
-  before_script:
-    - cargo install cargo-script
   script:
     - ./make.rs fmt --check
 
@@ -25,14 +22,8 @@ test:
   stage: test
   needs:
     - fmt
-  before_script:
-    - apt update --yes
-    - apt upgrade --yes
-    - apt install protobuf-compiler --yes
-    - cargo install cargo-script
-    - ./make.rs version
-
   script:
+    - ./make.rs version
     - ./make.rs check
     - ./make.rs clippy
     - ./make.rs test
diff --git a/README.md b/README.md
index 2f9badd5b57ca7396d96ab17bb2a40bc5c195b3f..eff6db1dc8fdb4bbe5fbb7b0c2771dded8f700e9 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,26 @@ see [`benchmarks/`](benchmarks/README.md)
 
 the results can be found in [`dragoon/komodo-benchmark-results`](https://gitlab.isae-supaero.fr/dragoon/komodo-benchmark-results).
 
+## development
+
+Komodo uses a Docker image as the base of the GitLab pipelines.
+
+That means that there is nothing to build apart from the source code of Komodo itself when running jobs.
+
+When the development environment needs to change, e.g. when the version of Rust is bumped in
+[`rust-toolchain.toml`](./rust-toolchain.toml), one shall run the following commands to push the new
+Docker image to the [_container registry_][gitlab.isae-supaero.fr:dragoon/komodo@containers].
+
+```shell
+./make.rs container --login
+```
+```shell
+./make.rs container
+```
+```shell
+./make.rs container --push
+```
+
 ## contributors
 
 Because the code for this project has been originally extracted from
@@ -35,3 +55,4 @@ note that the following people have contributed to this code base:
 - @j.detchart
 
 [pcs-fec-id]: https://gitlab.isae-supaero.fr/dragoon/pcs-fec-id
+[gitlab.isae-supaero.fr:dragoon/komodo@containers]: https://gitlab.isae-supaero.fr/dragoon/komodo/container_registry/42
diff --git a/make.rs b/make.rs
index baaada364613cd5edd3114de2e54a41092427781..f0b11c900a61f1cd53fa363ca9190f22a915d209 100755
--- a/make.rs
+++ b/make.rs
@@ -6,13 +6,18 @@
 //! edition = "2021"
 //!
 //! [dependencies]
-//! nob = { git = "https://gitlab.isae-supaero.fr/a.stevan/nob.rs", rev = "e4b03cdd4f1ba9daf3095930911b12fb28b6a248" }
+//! nob = { git = "https://gitlab.isae-supaero.fr/a.stevan/nob.rs", rev = "7ea6be855cf5600558440def6e59a83f78b8b543" }
 //! clap = { version = "4.5.17", features = ["derive"] }
 //! ```
 extern crate clap;
 
 use clap::{Parser, Subcommand};
 
+const REGISTRY: &str = "gitlab-registry.isae-supaero.fr";
+const MIRROR_REGISTRY: &str = "ghcr.io/dragoon-rs";
+const IMAGE: &str = "dragoon/komodo";
+const DOCKERFILE: &str = ".env.dockerfile";
+
 #[derive(Parser)]
 #[command(version, about, long_about = None)]
 struct Cli {
@@ -55,6 +60,15 @@ enum Commands {
         #[arg(short, long)]
         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,
+    },
 }
 
 #[rustfmt::skip]
@@ -85,7 +99,7 @@ fn main() {
                 "--",
                 "-D",
                 "warnings"
-            )
+            );
         }
         Some(Commands::Test { verbose, examples }) => {
             let mut cmd = vec!["cargo", "test"];
@@ -118,6 +132,35 @@ fn main() {
             if *features { cmd.push("--all-features") }
             nob::run_cmd_as_vec_and_fail!(cmd);
         },
+        Some(Commands::Container { login, push }) => {
+            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
+                );
+            }
+        }
         None => {}
     }
 }
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index f8eeac88483e38e3a9e6a41bf3a80cb1b6f078ec..2c00fd6a0f52661ba156474e2b0b40e5f432a5ad 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,5 +1,4 @@
 [toolchain]
 profile = "minimal"
-# WARNING: update `.gitlab-ci.yml` as well
 channel = "1.78"
 components = ["rustfmt", "clippy", "rust-analyzer"]