diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e05905571ad45e620acb14163279e620691a3427..703c0ab0cb1621909b1640b6499b54b6d7006382 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,8 @@
 ---
+include:
+  - /.gitlab/ci_templates/kaniko.yml
+  - /.gitlab/ci_templates/ssh_setup.yml
+  - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
 
 # Global templates and YAML anchors
 # =================================
@@ -20,13 +24,37 @@
     echo "CLUSTER_DIR:               $CLUSTER_DIR"
     echo "ANSIBLE_HOST_KEY_CHECKING: $ANSIBLE_HOST_KEY_CHECKING"
     echo "KANIKO_BUILD_IMAGENAME:    $KANIKO_BUILD_IMAGENAME"
-    echo "KANIKO build image ref:    ${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_COMMIT_REF_SLUG}"
+    echo "KANIKO build image ref:    ${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_CONTAINER_TAG}"
     echo "SSH_KEY_ID:                $SSH_KEY_ID"
     echo
     [ -d $CLUSTER_DIR ] && find $CLUSTER_DIR || echo "directory ${CLUSTER_DIR} not found"
     echo
     echo
 
+# The dotenv report requires us to report the artifacts in every job that is
+# required with a `needs:` from another job.
+.report_artifacts:
+  artifacts:
+    paths:
+      - clusters
+      - ./enabled_apps/$APP
+    expire_in: 1 month
+    when: always
+    reports:
+      dotenv:
+        $CLUSTER_DIR/.cluster.env
+
+# Rules that enable the cluster to be built and are applied to most steps
+# (except for application-specific steps)
+.general_rules:
+  rules:
+    - changes:
+        - .gitlab-ci.yml
+        - .gitlab/ci_scripts/*
+        - ansible/**/*
+        - flux/**/*
+        - test/**/*
+        - openappstack/**/*
 
 # app rules
 #
@@ -46,28 +74,35 @@
 # Gitlab CI allows pushing CI vars via `git push` but a bug prevents this when
 # using merge request pipelines (see https://gitlab.com/gitlab-org/gitlab/-/issues/326098)
 .eventrouter_rules:
-  rules:
-    - when: always
-
+  extends:
+    - .general_rules
 
 .loki_stack_rules:
-  rules:
-    - when: always
+  extends:
+    - .general_rules
 
 .nextcloud_rules:
   rules:
     - changes:
-      - flux/**/$APP*.yaml
-      - ansible/roles/apps/templates/settings/$APP.yaml
-      - ansible/roles/apps/tasks/$APP.yaml
-      - test/behave/features/$APP.feature
+        - flux/**/$APP*.yaml
+        - ansible/roles/apps/templates/settings/$APP.yaml
+        - ansible/roles/apps/tasks/$APP.yaml
+        - test/behave/features/$APP.feature
     - if: '$TRIGGER_JOBS =~ /enable-nextcloud/'
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-nextcloud/'
     - if: '$CI_COMMIT_BRANCH == "master"'
 
 .prometheus_stack_rules:
-  rules:
-    - when: always
+  extends:
+    - .general_rules
+
+.cert_manager_rules:
+  extends:
+    - .general_rules
+
+.local_path_provisioner_rules:
+  extends:
+    - .general_rules
 
 .cert_manager_rules:
   rules:
@@ -76,10 +111,10 @@
 .rocketchat_rules:
   rules:
     - changes:
-      - flux/**/$APP*.yaml
-      - ansible/roles/apps/templates/settings/$APP.yaml
-      - ansible/roles/apps/tasks/$APP.yaml
-      - test/behave/features/$APP.feature
+        - flux/**/$APP*.yaml
+        - ansible/roles/apps/templates/settings/$APP.yaml
+        - ansible/roles/apps/tasks/$APP.yaml
+        - test/behave/features/$APP.feature
     - if: '$TRIGGER_JOBS =~ /enable-rocketchat/'
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-rocketchat/'
     - if: '$CI_COMMIT_BRANCH == "master"'
@@ -87,9 +122,9 @@
 .single_sign_on_rules:
   rules:
     - changes:
-      - flux/**/$APP*.yaml
-      - ansible/roles/apps/templates/settings/$APP.yaml
-      - ansible/roles/apps/tasks/$APP.yaml
+        - flux/**/$APP*.yaml
+        - ansible/roles/apps/templates/settings/$APP.yaml
+        - ansible/roles/apps/tasks/$APP.yaml
     - if: '$TRIGGER_JOBS =~ /enable-single-sign-on/'
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-single-sign-on/'
     - if: '$CI_COMMIT_BRANCH == "master"'
@@ -97,10 +132,10 @@
 .wordpress_rules:
   rules:
     - changes:
-      - flux/**/$APP*.yaml
-      - ansible/roles/apps/templates/settings/$APP.yaml
-      - ansible/roles/apps/tasks/$APP.yaml
-      - test/behave/features/$APP.feature
+        - flux/**/$APP*.yaml
+        - ansible/roles/apps/templates/settings/$APP.yaml
+        - ansible/roles/apps/tasks/$APP.yaml
+        - test/behave/features/$APP.feature
     - if: '$TRIGGER_JOBS =~ /enable-wordpress/'
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-wordpress/'
     - if: '$CI_COMMIT_BRANCH == "master"'
@@ -110,10 +145,6 @@
 # ===================
 
 # https://docs.gitlab.com/ee/ci/yaml/README.html#workflowrules-templates
-include:
-  - /.gitlab/ci_templates/kaniko.yml
-  - /.gitlab/ci_templates/ssh_setup.yml
-  - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
 
 stages:
   - build
@@ -137,7 +168,7 @@ variables:
   CLUSTER_DIR: "/builds/openappstack/openappstack/clusters/${CI_COMMIT_REF_SLUG}"
 
 default:
-  image: "${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_COMMIT_REF_SLUG}"
+  image: "${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_CONTAINER_TAG}"
 
 
 # Stage: build
@@ -145,13 +176,23 @@ default:
 #
 # Builds CI test container image
 # There are 2 moments in which we (re)build the container image. If some files are
-# changed, or when it needs to be done manually
-# Write "REBUILD_CONTAINER" in your commit message to force rebuilding the container.
+# changed, or when the job is triggered with TRIGGER_JOBS.
 
 ci-test-image-build:
   stage: build
   before_script:
     - *debug_information
+  after_script:
+    - |
+      echo "CI_CONTAINER_TAG=${CI_COMMIT_REF_SLUG}" > .ci.env
+  artifacts:
+    paths:
+      - .ci.env
+    expire_in: 1 month
+    when: always
+    reports:
+      dotenv:
+        .ci.env
   environment:
     name: image/$CI_COMMIT_REF_SLUG
     url: https://open.greenhost.net:4567/openappstack/openappstack/openappstack-ci:${CI_COMMIT_REF_SLUG}
@@ -163,6 +204,7 @@ ci-test-image-build:
     - changes:
         - Dockerfile
         - requirements.txt
+        - .gitlab/ci_templates/kaniko.yml
     # Also rebuild when the CI variable contain this jobs name
     # or commit msg contains /TRIGGER_JOBS=.*ci-test-image-build/
     - if: '$TRIGGER_JOBS =~ /ci-test-image-build/'
@@ -171,6 +213,44 @@ ci-test-image-build:
     - .kaniko_build
   interruptible: true
 
+report-ci-image-tag:
+  stage: build
+  image: "curlimages/curl"
+  before_script:
+    - *debug_information
+  script:
+    - |
+      TAG_INFORMATION=$(curl https://open.greenhost.net/api/v4/projects/openappstack%2Fopenappstack/registry/repositories/2/tags/${CI_COMMIT_REF_SLUG});
+      echo "Tag information: ${TAG_INFORMATION}"
+      if [ "$TAG_INFORMATION" == '{"message":"404 Tag Not Found"}' ]; then
+        echo "CI_CONTAINER_TAG=master" > .ci.env
+      else
+        echo "CI_CONTAINER_TAG=${CI_COMMIT_REF_SLUG}" > .ci.env
+      fi
+  artifacts:
+    paths:
+      - .ci.env
+    expire_in: 1 month
+    when: always
+    reports:
+      dotenv:
+        .ci.env
+  rules:
+    # Make sure this job does not run if ci-test-image-build runs
+    - changes:
+        - Dockerfile
+        - requirements.txt
+      when: never  # Never run on file changes that trigger ci-test-image-build
+    - if: '$TRIGGER_JOBS =~ /ci-test-image-build/'
+      when: never  # Never run when ci-test-image is triggered manually
+    - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*ci-test-image-build/'
+      when: never  # Never run when ci-test-image is triggered manually
+    - if: '$CI_COMMIT_BRANCH == "master"'
+      when: never  # Never run on master branch
+    - when: always
+  interruptible: true
+
+
 # Stage: create-vps
 # =================
 #
@@ -183,24 +263,12 @@ create-vps:
     # Creates a VPS based on a custom CI image for which --install-kubernetes
     # has already run. See CONTRIBUTING.md#ci-pipeline-image for more info
     - sh .gitlab/ci_scripts/create_vps.sh
-  artifacts:
-    paths:
-      - clusters
-    expire_in: 1 month
-    when: always
-    reports:
-      dotenv:
-        $CLUSTER_DIR/.cluster.env
-  rules:
-    - changes:
-        - .gitlab-ci.yml
-        - .gitlab/ci_scripts/*
-        - ansible/**/*
-        - flux/**/*
-        - test/**/*
-        - openappstack/**/*
+    # Make sure .ci.env variables are not lost
+    - cat .ci.env >> ${CLUSTER_DIR}/.cluster.env
   extends:
     - .ssh_setup
+    - .report_artifacts
+    - .general_rules
   environment:
     name: $CI_COMMIT_REF_SLUG
     url: https://$FQDN
@@ -221,10 +289,10 @@ create-vps:
     - |
       [ ! -d ./enabled_apps ] && mkdir enabled_apps || /bin/true
       touch ./enabled_apps/$APP
-  artifacts:
-    paths:
-      - ./clusters
-      - ./enabled_apps/$APP
+  needs:
+    - job: create-vps
+  extends:
+    - .report_artifacts
   interruptible: true
 
 enable-cert-manager:
@@ -291,6 +359,8 @@ enable-wordpress:
 
 test-dns:
   stage: setup-cluster
+  needs:
+    - job: create-vps
   script:
     - *debug_information
     - cd ansible/
@@ -320,20 +390,10 @@ setup-openappstack:
     # Show versions of installed apps/binaries
     - cd ansible
     - ansible master -m shell -a 'oas-version-info.sh 2>&1'
-  artifacts:
-    paths:
-      - ./clusters
-    expire_in: 1 month
-    when: always
-  rules:
-    - changes:
-        - .gitlab-ci.yml
-        - ansible/**/*
-        - flux/**/*
-        - test/**/*
-        - openappstack/**/*
   extends:
     - .ssh_setup
+    - .report_artifacts
+    - .general_rules
   interruptible: true
 
 
@@ -345,18 +405,14 @@ setup-openappstack:
 
 .helm-release:
   stage: helm-release
+  needs:
+    - job: setup-openappstack
+    - job: test-dns
   script:
     - *debug_information
     - cd ansible/
     - export KUBECONFIG="${PWD}/../clusters/${HOSTNAME}/secrets/kube_config_cluster.yml"
     - pytest -v -s -m 'helmreleases' --app="$APP" --connection=ansible --ansible-inventory=${CLUSTER_DIR}/inventory.yml --hosts='ansible://*' --reruns 120 --reruns-delay 20
-  artifacts:
-    paths:
-      - ./clusters
-      - ansible/inventory.yml
-      - ansible/group_vars/all/settings.yml
-    expire_in: 1 month
-    when: always
   extends:
     - .ssh_setup
   interruptible: true
@@ -375,6 +431,12 @@ eventrouter-helm-release:
     - .helm-release
     - .eventrouter_rules
 
+local-path-provisioner-helm-release:
+  variables:
+    APP: "local-path-provisioner"
+  extends:
+    - .helm-release
+    - .local_path_provisioner_rules
 
 loki-stack-helm-release:
   variables:
@@ -432,29 +494,46 @@ wordpress-helm-release:
     - cd ansible/
     - export KUBECONFIG="${PWD}/../clusters/${HOSTNAME}/secrets/kube_config_cluster.yml"
     - pytest -v -s -m 'apps_running' --app="$APP" --connection=ansible --ansible-inventory=${CLUSTER_DIR}/inventory.yml --hosts='ansible://*' --reruns 120 --reruns-delay 10
-  artifacts:
-    paths:
-      - ./clusters
-    expire_in: 1 month
-    when: always
   extends:
     - .ssh_setup
   interruptible: true
 
+cert-manager-ready:
+  variables:
+    APP: "cert-manager"
+  needs:
+    - job: cert-manager-helm-release
+    - job: setup-openappstack  # Needs makes sure the artifacts from that job are downloaded
+  extends:
+    - .apps-ready
+    - .cert_manager_rules
+
 eventrouter-ready:
   variables:
     APP: "eventrouter"
   needs:
     - job: eventrouter-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .eventrouter_rules
 
+local-path-provisioner-ready:
+  variables:
+    APP: "local-path-provisioner"
+  needs:
+    - job: local-path-provisioner-helm-release
+    - job: setup-openappstack
+  extends:
+    - .apps-ready
+    - .local_path_provisioner_rules
+
 loki-stack-ready:
   variables:
     APP: "loki-stack"
   needs:
     - job: loki-stack-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .loki_stack_rules
@@ -464,6 +543,7 @@ nextcloud-ready:
     APP: "nextcloud"
   needs:
     - job: nextcloud-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .nextcloud_rules
@@ -473,6 +553,7 @@ prometheus-stack-ready:
     APP: "prometheus-stack"
   needs:
     - job: prometheus-stack-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .prometheus_stack_rules
@@ -482,6 +563,7 @@ rocketchat-ready:
     APP: "rocketchat"
   needs:
     - job: rocketchat-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .rocketchat_rules
@@ -491,6 +573,7 @@ single-sign-on-ready:
     APP: "single-sign-on"
   needs:
     - job: single-sign-on-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .single_sign_on_rules
@@ -500,6 +583,7 @@ wordpress-ready:
     APP: "wordpress"
   needs:
     - job: wordpress-helm-release
+    - job: setup-openappstack
   extends:
     - .apps-ready
     - .wordpress_rules
@@ -515,11 +599,6 @@ wordpress-ready:
     - *debug_information
     - cd ansible/
     - pytest -v -s -m 'certs' --app="$APP" --connection=ansible --ansible-inventory=${CLUSTER_DIR}/inventory.yml --hosts='ansible://*' --reruns 120 --reruns-delay 10
-  artifacts:
-    paths:
-      - ./clusters
-    expire_in: 1 month
-    when: always
   extends:
     - .ssh_setup
   interruptible: true
@@ -529,6 +608,7 @@ nextcloud-cert:
     APP: "nextcloud"
   needs:
     - job: nextcloud-ready
+    - job: setup-openappstack
   extends:
     - .apps-cert
     - .nextcloud_rules
@@ -538,6 +618,7 @@ prometheus-stack-cert:
     APP: "prometheus-stack"
   needs:
     - job: prometheus-stack-ready
+    - job: setup-openappstack
   extends:
     - .apps-cert
     - .prometheus_stack_rules
@@ -547,6 +628,7 @@ rocketchat-cert:
     APP: "rocketchat"
   needs:
     - job: rocketchat-ready
+    - job: setup-openappstack
   extends:
     - .apps-cert
     - .rocketchat_rules
@@ -556,6 +638,7 @@ single-sign-on-cert:
     APP: "single-sign-on"
   needs:
     - job: single-sign-on-ready
+    - job: setup-openappstack
   extends:
     - .apps-cert
     - .single_sign_on_rules
@@ -565,6 +648,7 @@ wordpress-cert:
     APP: "wordpress"
   needs:
     - job: wordpress-ready
+    - job: setup-openappstack
   extends:
     - .apps-cert
     - .wordpress_rules
@@ -607,6 +691,7 @@ prometheus-stack-alerts:
     - .prometheus_stack_rules
   needs:
     - job: prometheus-stack-ready
+    - job: setup-openappstack
   interruptible: true
 
 
@@ -636,6 +721,7 @@ prometheus-stack-behave:
     APP: "prometheus-stack"
   needs:
     - job: prometheus-stack-cert
+    - job: setup-openappstack
   extends:
     - .behave
     - .prometheus_stack_rules
@@ -645,6 +731,7 @@ nextcloud-behave:
     APP: "nextcloud"
   needs:
     - job: nextcloud-cert
+    - job: setup-openappstack
   extends:
     - .behave
     - .nextcloud_rules
@@ -654,6 +741,7 @@ rocketchat-behave:
     APP: "rocketchat"
   needs:
     - job: rocketchat-cert
+    - job: setup-openappstack
   extends:
     - .behave
     - .rocketchat_rules
@@ -664,13 +752,14 @@ wordpress-behave:
     APP: "wordpress"
   needs:
     - job: wordpress-cert
+    - job: setup-openappstack
   extends:
     - .behave
     - .wordpress_rules
 
 
-  # Etc
-  # ===
+# Etc
+# ===
 
 
 # Terminates a droplet once the branch for it is deleted
@@ -703,14 +792,3 @@ delete-image:
   environment:
     name: image/$CI_COMMIT_REF_SLUG
     action: stop
-
-# We need one job that run every time (without any `only:` limitation).
-# This works around a Gitlab bug: if no job runs at all due to
-# `only`, Gitlab gets confused and doesn't allow you to merge the MR:
-# https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html#limitations
-gitlab-merge-workaround:
-  stage: build
-  image: busybox
-  script:
-    - echo "Not building anything, no changes."
-  interruptible: true
diff --git a/Dockerfile b/Dockerfile
index 61fc57812b8842d743b8565f86f277ea9bbaaeb8..23feb62a32ab5302a4865074961cacb66bd3c0ea 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,3 +32,4 @@ RUN \
   update-ca-certificates && \
   pip install --no-cache-dir -r /requirements.txt && \
   ln -s /usr/bin/python3 /usr/bin/python
+
diff --git a/README.md b/README.md
index 42f751a771eb218fbfec277be28def8516eadf16..5ff2f585eb635d86b99f48ebddaa1318c4258d5e 100644
--- a/README.md
+++ b/README.md
@@ -10,3 +10,4 @@ a single-node kubernetes cluster.
 Please refer to https://docs.openappstack.net for further details,
 and to [the installation tutorial](docs/installation_instructions.md) for a step
 by step installation tutorial how to install your cluster.
+
diff --git a/ansible/roles/apps/templates/settings/local-path-provisioner.yaml b/ansible/roles/apps/templates/settings/local-path-provisioner.yaml
index 78452d53788264eb04f33a347ad891f1319e713d..50a6828bb0d7c3d4794873827c1186b54379aa31 100644
--- a/ansible/roles/apps/templates/settings/local-path-provisioner.yaml
+++ b/ansible/roles/apps/templates/settings/local-path-provisioner.yaml
@@ -8,7 +8,7 @@ storageClass:
 # hostPath.
 image:
   repository: "open.greenhost.net:4567/openappstack/openappstack/local-path-provisioner"
-  tag: "02b021c-amd64"
+  tag: "52f994f-amd64"
 
 
 resources:
diff --git a/flux/kube-system/local-path-provisioner_hr.yaml b/flux/kube-system/local-path-provisioner_hr.yaml
index b36561ba005d6a521e870ca0f98cfbf962b0c62c..7d83e861ef97a82515e4b67989739ebe68f7fc7c 100644
--- a/flux/kube-system/local-path-provisioner_hr.yaml
+++ b/flux/kube-system/local-path-provisioner_hr.yaml
@@ -10,7 +10,7 @@ spec:
   releaseName: local-path-provisioner
   chart:
     git: https://github.com/rancher/local-path-provisioner
-    ref: v0.0.13
+    ref: v0.0.14
     path: deploy/chart
   valuesFrom:
     - secretKeyRef:
diff --git a/test/pytest/test_app_deployments.py b/test/pytest/test_app_deployments.py
index 01fadbdc4650c7294134f1b7975fb9285902d18d..362eb221c844286b1b566ef02a81ec38cbd074e1 100644
--- a/test/pytest/test_app_deployments.py
+++ b/test/pytest/test_app_deployments.py
@@ -25,9 +25,16 @@ EXPECTED_RELEASES = {
 }
 
 EXPECTED_APP_LABELS = {
+    'cert-manager': {
+        'namespace': 'cert-manager',
+        'label_selector': 'app.kubernetes.io/instance=cert-manager'
+    },
     'eventrouter': {
         'namespace': 'oas',
         'label_selector': 'app=eventrouter'},
+    'local-path-provisioner': {
+        'namespace': 'kube-system',
+        'label_selector': 'app.kubernetes.io/instance=local-path-provisioner'},
     'loki-stack': {
         'namespace': 'oas',
         'label_selector': 'app=loki'},