diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bc243280dd262647fbfba006d9f5365d6ea9e8a3..89ace984f012d152b6ef2b68e26d31517c3636b8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,7 @@ unit-tests:
   script:
     - "nix-shell --run 'nix-build nixos/unit-tests.nix' && cat result"
 
-morph-builds:
+.morph-build: &MORPH_BUILD
   tags:
     # Run this job in a Docker container so that it won't have the system
     # /nix/store so that it has to build everything.  This is necessary so
@@ -36,7 +36,7 @@ morph-builds:
   image: "nixos/nix:latest"
 
   stage: "test"
-  variables:
+  variables: &MORPH_BUILD_VARIABLES
     # CACHIX_AUTH_TOKEN, which lets us push to cachix, is supplied by GitLab
     # thanks to project-level configuration.
     CACHIX_NAME: "privatestorage-opensource"
@@ -49,16 +49,35 @@ morph-builds:
 
   script:
     - |
-      nix-shell --command '
-      for grid in morph/grid/*/grid.nix; do
-          morph build "${grid}"
-      done
-      '
+      # GRID is set in one of the "instantiations" of this job template.
+      nix-shell --command "morph build morph/grid/${GRID}/grid.nix"
 
   after_script:
     - |
       bash -c "comm -13 <(sort /tmp/store-path-pre-build | grep -v '\.drv$') <(nix path-info --all | grep -v '\.drv$' | sort) | cachix push $CACHIX_NAME"
 
+
+morph-build-localdev:
+  <<: *MORPH_BUILD
+  variables:
+    <<: *MORPH_BUILD_VARIABLES
+    GRID: "local"
+
+
+morph-build-testing:
+  <<: *MORPH_BUILD
+  variables:
+    <<: *MORPH_BUILD_VARIABLES
+    GRID: "testing"
+
+
+morph-build-production:
+  <<: *MORPH_BUILD
+  variables:
+    <<: *MORPH_BUILD_VARIABLES
+    GRID: "production"
+
+
 vulnerability-scan:
   stage: "test"
   script: