diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 197c96a9e9e2f82b27bfa679270f676d1b43e60a..ad94191fe16b94234fd9fd169e179a7da9e67619 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,12 +6,30 @@ stages:
 variables:
   GIT_SUBMODULE_STRATEGY: recursive
   GIT_DEPTH: 0
+  CACHIX_CACHE_NAME: flow3r
+  BUILD_IMAGE_NAME: ${CI_REGISTRY_IMAGE}/flow3r-build:${CI_COMMIT_SHA}
 
 default:
   # built via:
   #     docker load < $(nix-build nix/docker-image.nix)
   image: registry.gitlab.com/flow3r-badge/flow3r-build:kfjixcricw2358zp5vg15b784l9jnpzz
 
+nix-deps:
+  image: docker.nix-community.org/nixpkgs/nix-flakes
+  stage: .pre
+  script:
+    - set +e +o pipefail
+    # cache development environment
+    - nix run .#build-and-cache -- devShell
+    # build docker image and cache if not already in cache
+    - nix run .#build-and-cache -- dockerImage
+    # push docker image
+    # FIXME: disabled because the gitlab registry is broken
+    #- |
+    #  echo "${CI_REGISTRY_PASSWORD}" \
+    #    | skopeo login --username="${CI_REGISTRY_USER}" --password-stdin "${CI_REGISTRY}"
+    #- skopeo copy --tmpdir /tmp --insecure-policy "docker-archive://${PWD}/result" "docker://${BUILD_IMAGE_NAME}"
+
 clang-tidy:
   stage: check
   script:
diff --git a/flake.nix b/flake.nix
index 66ecd503b892de68dc34bed1ea62336442921df6..baac093990ccbff6e2fbd1a045a86bffcbe53a90 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,6 +1,17 @@
 {
   description = "flow3r badge flake";
 
+  nixConfig = {
+    substituters = [
+      "https://cache.nixos.org"
+      "https://flow3r.cachix.org"
+    ];
+    trusted-public-keys = [
+      "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
+      "flow3r.cachix.org-1:/v8059Hm6UdEVNKE15uxltpYM0z+pulaTpobjIvFM5A="
+    ];
+  };
+
   inputs = {
     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
     flake-compat = {
@@ -69,6 +80,8 @@
     {
       overlays.default = import ./nix/overlay;
 
+      legacyPackages = forAllPkgs (pkgs: pkgs);
+
       packages = forAllPkgs (pkgs:
         {
           dockerImage = pkgs.dockerTools.buildImage {
@@ -84,9 +97,8 @@
               pathsToLink = [ "/bin" ];
             };
 
-            runAsRoot = ''
-              #!${pkgs.runtimeShell}
-              mkdir -p /tmp
+            extraCommands = ''
+              mkdir -m 1777 tmp
             '';
 
             config = {
@@ -102,6 +114,44 @@
           };
         });
 
+      hydraJobs = let inherit (nixpkgs.lib) hydraJob; in {
+        dockerImage = forAllSystems (system: hydraJob self.packages.${system}.dockerImage);
+        devShell = forAllSystems (system: hydraJob self.devShells.${system}.default);
+      };
+
+      apps = forAllPkgs (pkgs: {
+        cache-flake-inputs = {
+          type = "app";
+          program = toString (pkgs.writers.writeBash "cache-flake-inputs" ''
+            set +e +o pipefail
+            nix flake archive --json \
+              | ${pkgs.jq}/bin/jq -r '.path,(.inputs|to_entries[].value.path)' \
+              | ${pkgs.cachix}/bin/cachix push flow3r
+          '');
+        };
+        build-and-cache = {
+          type = "app";
+          program = toString (pkgs.writers.writeBash "build-and-cache" ''
+            set +e +o pipefail
+            PACKAGE="$1"
+
+            IS_CACHED=$(
+              ${pkgs.nix-eval-jobs}/bin/nix-eval-jobs \
+                --gc-roots-dir gcroot \
+                --flake ".#hydraJobs.$PACKAGE" \
+                --check-cache-status \
+                | ${pkgs.jq}/bin/jq '.isCached'
+            )
+
+            if [ "$IS_CACHED" == "false" ]; then
+              nix build -L --json ".#hydraJobs.$PACKAGE.${pkgs.system}" \
+                | ${pkgs.jq}/bin/jq -r '.[].outputs | to_entries[].value' \
+                | ${pkgs.cachix}/bin/cachix push flow3r
+            fi
+          '');
+        };
+      });
+
       devShells = forAllPkgs (pkgs:
         {
           default = pkgs.mkShell {