diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b17457db642b7eceb4eed0f04724adb973ba7f4e..94c39da72fe00b1d88cabed3e4e24cf5bdb1f488 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,15 +1,14 @@
 default:
-  image: python:3.10  # use latest for building/linting
+  image: $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG
   before_script:
     - python --version # For debugging
-    - python -m pip install --upgrade pip
-    - pip install --upgrade tox twine
   cache:
     paths:
       - .cache/pip
       # Do not cache .tox, to recreate virtualenvs for every step
 
 stages:
+  - prepare
   - lint
   # check if this needs to be a separate step
   # - build_extensions
@@ -18,6 +17,13 @@ stages:
   - integration
   - publish # publish instead of deploy
 
+# Prepare image to run ci on
+trigger_prepare:
+  stage: prepare
+  trigger:
+    strategy: depend
+    include: .prepare.gitlab-ci.yml
+
 # Caching of dependencies to speed up builds
 variables:
   PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
diff --git a/.prepare.gitlab-ci.yml b/.prepare.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..10fee5b307f768fb9438ed13e67333321cbb1f4c
--- /dev/null
+++ b/.prepare.gitlab-ci.yml
@@ -0,0 +1,23 @@
+stages:
+  - build
+
+build_ci_runner_image:
+  stage: build
+  image: docker:stable
+  services:
+    - docker:dind
+  script:
+    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+    - |
+      if docker pull $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG; then
+        docker build --cache-from $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG --tag $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG ci-runner
+      else
+        docker pull $CI_REGISTRY_IMAGE/ci-build-runner:latest || true
+        docker build --cache-from $CI_REGISTRY_IMAGE/ci-build-runner:latest --tag $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG ci-runner
+      fi
+    - docker push $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG  # push the image
+    - |
+      if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
+        docker image tag $CI_REGISTRY_IMAGE/ci-build-runner:$CI_COMMIT_REF_SLUG $CI_REGISTRY_IMAGE/ci-build-runner:latest
+        docker push $CI_REGISTRY_IMAGE/ci-build-runner:latest
+      fi
diff --git a/ci-runner/Dockerfile b/ci-runner/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..51eb91dcdd5ca12d23f18c70c966d53420fb306e
--- /dev/null
+++ b/ci-runner/Dockerfile
@@ -0,0 +1,4 @@
+FROM python:3.10
+
+RUN python -m pip install --upgrade pip
+RUN pip install --upgrade tox twine