diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9af54e066bcbdf9aa5a8aba05d0f2cd502082577..e1e2d6e59ed4f01e6ed38125939d7539fd339335 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,6 @@
 ---
 stages:
+  - build
   - unit-tests
   - deploy-part1
   - moderator
@@ -35,6 +36,7 @@ variables:
   RECOVER_CONTROL_PLANE_TEST_GROUPS: "etcd[2:],kube_control_plane[1:]"
   TERRAFORM_VERSION: 1.0.8
   ANSIBLE_MAJOR_VERSION: "2.11"
+  PIPELINE_IMAGE: "$CI_REGISTRY_IMAGE/pipeline:${CI_PIPELINE_ID}-${CI_COMMIT_SHORT_SHA}"
 
 before_script:
   - ./tests/scripts/rebase.sh
@@ -46,7 +48,7 @@ before_script:
 .job: &job
   tags:
     - packet
-  image: quay.io/kubespray/kubespray:$KUBESPRAY_VERSION
+  image: $PIPELINE_IMAGE
   artifacts:
     when: always
     paths:
@@ -76,6 +78,7 @@ ci-authorized:
   only: []
 
 include:
+  - .gitlab-ci/build.yml
   - .gitlab-ci/lint.yml
   - .gitlab-ci/shellcheck.yml
   - .gitlab-ci/terraform.yml
diff --git a/.gitlab-ci/build.yml b/.gitlab-ci/build.yml
new file mode 100644
index 0000000000000000000000000000000000000000..34179c859ca97d2af12b642350ba062308b678b2
--- /dev/null
+++ b/.gitlab-ci/build.yml
@@ -0,0 +1,17 @@
+---
+pipeline image:
+  stage: build
+  image: docker:20.10.22-cli
+  variables:
+    DOCKER_TLS_CERTDIR: ""
+  services:
+    - name: docker:20.10.22-dind
+      # See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27300 for why this is required
+      command: ["--tls=false"]
+  before_script:
+    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
+  script:
+    # DOCKER_HOST is overwritten if we set it as a GitLab variable
+    - DOCKER_HOST=tcp://docker:2375; docker build --network host --file pipeline.Dockerfile --tag $PIPELINE_IMAGE .
+    - docker push $PIPELINE_IMAGE
+  except: ['triggers', 'master']
diff --git a/.gitlab-ci/molecule.yml b/.gitlab-ci/molecule.yml
index 346bf18ce9c0008319b6bfaea50d7c6e88e1fc62..736c0ffd755164ac91ff6164d311d5fc6658f6b7 100644
--- a/.gitlab-ci/molecule.yml
+++ b/.gitlab-ci/molecule.yml
@@ -4,7 +4,7 @@
   tags: [c3.small.x86]
   only: [/^pr-.*$/]
   except: ['triggers']
-  image: quay.io/kubespray/vagrant:$KUBESPRAY_VERSION
+  image: $PIPELINE_IMAGE
   services: []
   stage: deploy-part1
   before_script:
diff --git a/.gitlab-ci/vagrant.yml b/.gitlab-ci/vagrant.yml
index 4f7bd9e43e0c9542fdb92b0337ca7760e5a0c54d..d2a40749973280b8f0a3a39647272ae560bf7a71 100644
--- a/.gitlab-ci/vagrant.yml
+++ b/.gitlab-ci/vagrant.yml
@@ -10,7 +10,7 @@
   tags: [c3.small.x86]
   only: [/^pr-.*$/]
   except: ['triggers']
-  image: quay.io/kubespray/vagrant:$KUBESPRAY_VERSION
+  image: $PIPELINE_IMAGE
   services: []
   before_script:
     - apt-get update && apt-get install -y python3-pip
diff --git a/Dockerfile b/Dockerfile
index 5645e8d3a4995d5963652e7a5d5d1bbea91226cd..833e24c966dacc2a882a725e31359b463dc53ab9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,15 +7,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 
 RUN apt update -y \
     && apt install -y \
-    libssl-dev python3-dev sshpass apt-transport-https jq moreutils \
-    ca-certificates curl gnupg2 software-properties-common python3-pip unzip rsync git \
-    && rm -rf /var/lib/apt/lists/*
-RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
-    && add-apt-repository \
-    "deb [arch=$ARCH] https://download.docker.com/linux/ubuntu \
-    $(lsb_release -cs) \
-    stable" \
-    && apt update -y && apt-get install --no-install-recommends -y docker-ce \
+    curl python3 python3-pip sshpass \
     && rm -rf /var/lib/apt/lists/*
 
 # Some tools like yamllint need this
@@ -25,13 +17,20 @@ RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
 ENV LANG=C.UTF-8
 
 WORKDIR /kubespray
-COPY . .
-RUN /usr/bin/python3 -m pip install --no-cache-dir pip -U \
-    && /usr/bin/python3 -m pip install --no-cache-dir -r tests/requirements.txt \
-    && python3 -m pip install --no-cache-dir -r requirements.txt \
-    && update-alternatives --install /usr/bin/python python /usr/bin/python3 1
+COPY *yml /kubespray/
+COPY roles /kubespray/roles
+COPY inventory /kubespray/inventory
+COPY library /kubespray/library
+COPY extra_playbooks /kubespray/extra_playbooks
 
-RUN KUBE_VERSION=$(sed -n 's/^kube_version: //p' roles/kubespray-defaults/defaults/main.yaml) \
+RUN python3 -m pip install --no-cache-dir \
+    ansible==5.7.1 \
+    ansible-core==2.12.5 \
+    cryptography==3.4.8 \
+    jinja2==2.11.3 \
+    netaddr==0.7.19 \
+    MarkupSafe==1.1.1 \
+    && KUBE_VERSION=$(sed -n 's/^kube_version: //p' roles/kubespray-defaults/defaults/main.yaml) \
     && curl -LO https://storage.googleapis.com/kubernetes-release/release/$KUBE_VERSION/bin/linux/$ARCH/kubectl \
     && chmod a+x kubectl \
     && mv kubectl /usr/local/bin/kubectl
diff --git a/pipeline.Dockerfile b/pipeline.Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..167aca29b037a7363b2004d942acae42c031fc82
--- /dev/null
+++ b/pipeline.Dockerfile
@@ -0,0 +1,47 @@
+# Use imutable image tags rather than mutable tags (like ubuntu:20.04)
+FROM ubuntu:focal-20220531
+
+ARG ARCH=amd64
+ARG TZ=Etc/UTC
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
+
+ENV VAGRANT_VERSION=2.2.19
+ENV VAGRANT_DEFAULT_PROVIDER=libvirt
+ENV VAGRANT_ANSIBLE_TAGS=facts
+
+RUN apt update -y \
+    && apt install -y \
+    libssl-dev python3-dev sshpass apt-transport-https jq moreutils wget libvirt-dev openssh-client rsync git \
+    ca-certificates curl gnupg2 software-properties-common python3-pip unzip \
+    && rm -rf /var/lib/apt/lists/*
+RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - \
+    && add-apt-repository \
+    "deb [arch=$ARCH] https://download.docker.com/linux/ubuntu \
+    $(lsb_release -cs) \
+    stable" \
+    && apt update -y && apt-get install --no-install-recommends -y docker-ce \
+    && rm -rf /var/lib/apt/lists/*
+
+# Some tools like yamllint need this
+# Pip needs this as well at the moment to install ansible
+# (and potentially other packages)
+# See: https://github.com/pypa/pip/issues/10219
+ENV LANG=C.UTF-8
+
+WORKDIR /kubespray
+COPY . .
+RUN /usr/bin/python3 -m pip install --no-cache-dir pip -U \
+    && /usr/bin/python3 -m pip install --no-cache-dir -r tests/requirements.txt \
+    && python3 -m pip install --no-cache-dir -r requirements.txt \
+    && update-alternatives --install /usr/bin/python python /usr/bin/python3 1
+
+RUN KUBE_VERSION=$(sed -n 's/^kube_version: //p' roles/kubespray-defaults/defaults/main.yaml) \
+    && curl -LO https://storage.googleapis.com/kubernetes-release/release/$KUBE_VERSION/bin/linux/$ARCH/kubectl \
+    && chmod a+x kubectl \
+    && mv kubectl /usr/local/bin/kubectl
+
+# Install Vagrant
+RUN wget https://releases.hashicorp.com/vagrant/${VAGRANT_VERSION}/vagrant_${VAGRANT_VERSION}_x86_64.deb && \
+ dpkg -i vagrant_${VAGRANT_VERSION}_x86_64.deb && \
+ rm vagrant_${VAGRANT_VERSION}_x86_64.deb && \
+ vagrant plugin install vagrant-libvirt