diff --git a/roles/build-docker-image/common.rst b/roles/build-docker-image/common.rst
index 10753e62e..5b899a8d6 100644
--- a/roles/build-docker-image/common.rst
+++ b/roles/build-docker-image/common.rst
@@ -51,6 +51,12 @@ using this role.
    when building all images from different folders with similarily
    named dockerfiles.
 
+.. zuul:rolevar:: docker_registry
+   :default: ''
+
+   The container registry the images should be tagged for, by default
+   zuul will push the image to dockerhub.
+
 .. zuul:rolevar:: docker_credentials
    :type: dict
 
diff --git a/roles/build-docker-image/defaults/main.yaml b/roles/build-docker-image/defaults/main.yaml
index d702500de..24a195ad1 100644
--- a/roles/build-docker-image/defaults/main.yaml
+++ b/roles/build-docker-image/defaults/main.yaml
@@ -1,2 +1,3 @@
 zuul_work_dir: "{{ zuul.project.src_dir }}"
 docker_dockerfile: "Dockerfile"
+docker_registry: ''
diff --git a/roles/build-docker-image/tasks/build.yaml b/roles/build-docker-image/tasks/build.yaml
index 8c622aaeb..a02ac21d6 100644
--- a/roles/build-docker-image/tasks/build.yaml
+++ b/roles/build-docker-image/tasks/build.yaml
@@ -15,9 +15,9 @@
     {% endif -%}
     {% for tag in zj_image.tags | default(['latest']) -%}
       {% if zuul.change | default(false) -%}
-        --tag {{ zj_image.repository }}:change_{{ zuul.change }}_{{ tag }}
+        --tag {{ docker_registry | ternary(docker_registry + '/', '') }}{{ zj_image.repository }}:change_{{ zuul.change }}_{{ tag }}
       {% endif -%}
-      --tag {{ zj_image.repository }}:{{ tag }}
+      --tag {{ docker_registry | ternary(docker_registry + '/', '') }}{{ zj_image.repository }}:{{ tag }}
     {% endfor -%}
     {% for label in zj_image.labels | default([]) -%}
       --label "{{ label }}"
diff --git a/roles/upload-docker-image/defaults/main.yaml b/roles/upload-docker-image/defaults/main.yaml
index 0c5fa0c84..fceb40e62 100644
--- a/roles/upload-docker-image/defaults/main.yaml
+++ b/roles/upload-docker-image/defaults/main.yaml
@@ -1,3 +1,4 @@
 zuul_work_dir: "{{ zuul.project.src_dir }}"
 docker_dockerfile: "Dockerfile"
 upload_docker_image_promote: true
+docker_registry: ''
diff --git a/roles/upload-docker-image/tasks/main.yaml b/roles/upload-docker-image/tasks/main.yaml
index e84d725d3..1e545d9c5 100644
--- a/roles/upload-docker-image/tasks/main.yaml
+++ b/roles/upload-docker-image/tasks/main.yaml
@@ -8,8 +8,8 @@
   fail:
     msg: "{{ zj_image.repository }} not permitted by {{ docker_credentials.repository }}"
 
-- name: Log in to dockerhub
-  command: "docker login -u {{ docker_credentials.username }} -p {{ docker_credentials.password }}"
+- name: Log in to registry
+  command: "docker login -u {{ docker_credentials.username }} -p {{ docker_credentials.password }} {{ docker_registry }}"
   no_log: true
 
 - name: Determine if we need to use buildx
diff --git a/roles/upload-docker-image/tasks/push.yaml b/roles/upload-docker-image/tasks/push.yaml
index b6c8fccff..f1d36ca7d 100644
--- a/roles/upload-docker-image/tasks/push.yaml
+++ b/roles/upload-docker-image/tasks/push.yaml
@@ -1,5 +1,5 @@
-- name: Upload tag to dockerhub
-  command: "docker push {{ zj_image.repository }}:{{ upload_docker_image_promote | ternary('change_' + zuul.get('change', '') + '_', '') }}{{ zj_image_tag }}"
+- name: Upload tag to registry
+  command: "docker push {{ docker_registry | ternary(docker_registry + '/', '' ) }}{{ zj_image.repository }}:{{ upload_docker_image_promote | ternary('change_' + zuul.get('change', '') + '_', '') }}{{ zj_image_tag }}"
   loop: "{{ zj_image.tags | default(['latest']) }}"
   loop_control:
     loop_var: zj_image_tag
diff --git a/test-playbooks/container/test-build-container-image-release.yaml b/test-playbooks/container/test-build-container-image-release.yaml
index 9d5243e1e..d749d2d66 100644
--- a/test-playbooks/container/test-build-container-image-release.yaml
+++ b/test-playbooks/container/test-build-container-image-release.yaml
@@ -1,5 +1,18 @@
 - hosts: all
-  tasks:
+  vars:
+    docker_registry: localhost:5000
+    upload_docker_image_promote: false
+    docker_credentials:
+      username: zuul
+      password: testpassword
+      repository: testrepo
+    docker_images:
+      - context: test-playbooks/container/docker
+        repository: "testrepo"
+        # This is what the Zuul repo uses to tag its releases:
+        tags: "{{ zuul.tag is defined | ternary([zuul.get('tag', '').split('.')[0], '.'.join(zuul.get('tag', '').split('.')[:2]), zuul.get('tag', '')], ['latest']) }}"
+    container_images: "{{ docker_images }}"
+  pre_tasks:
     - name: Save zuul variables
       set_fact:
         old_zuul: "{{ zuul }}"
@@ -16,10 +29,72 @@
       include_role:
         name: "build-{{ (container_command == 'docker') | ternary('docker', 'container') }}-image"
       vars:
-        docker_images:
-          - context: test-playbooks/container/docker
-            repository: "testrepo"
-            # This is what the Zuul repo uses to tag its releases:
-            tags: "{{ zuul.tag is defined | ternary([zuul.get('tag', '').split('.')[0], '.'.join(zuul.get('tag', '').split('.')[:2]), zuul.get('tag', '')], ['latest']) }}"
-        container_images: "{{ docker_images }}"
+        zuul: "{{ new_zuul }}"
+
+    - name: Create temporary registry working directory
+      tempfile:
+        state: directory
+      register: registry_tempdir
+
+    - name: Create auth directory
+      file:
+        path: "{{ registry_tempdir.path }}/auth"
+        state: directory
+    - name: Install passlib for htpasswd
+      become: true
+      package:
+        name:
+          - python3-passlib
+          - python3-bcrypt
+        state: present
+    - name: Write htpasswd file
+      htpasswd:
+        create: true
+        crypt_scheme: bcrypt
+        path: "{{ registry_tempdir.path }}/auth/htpasswd"
+        name: "{{ docker_credentials.username }}"
+        password: "{{ docker_credentials.password }}"
+
+    - name: Create certs directory
+      file:
+        state: directory
+        path: "{{ registry_tempdir.path }}/certs"
+    - name: Create self signed certificates
+      command: >
+        openssl req
+        -newkey rsa:4096 -nodes -sha256 -keyout certs/localhost.key
+        -x509 -days 365 -out certs/localhost.crt
+        -subj '/CN=localhost'
+      args:
+        chdir: "{{ registry_tempdir.path }}"
+    - name: Create docker certs dir
+      file:
+        state: directory
+        path: /etc/docker/certs.d/localhost:5000/
+      become: true
+    - name: Configure docker to trust certificate
+      copy:
+        src: "{{ registry_tempdir.path }}/certs/localhost.crt"
+        dest: /etc/docker/certs.d/localhost:5000/ca.crt
+        remote_src: true
+      become: true
+
+    - name: Start registry with basic auth
+      command: >-
+        {{ container_command }} run -d \
+        -p 5000:5000 \
+        -v {{ registry_tempdir.path }}/auth:/auth \
+        -e "REGISTRY_AUTH=htpasswd" \
+        -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
+        -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
+        -v {{ registry_tempdir.path }}/certs:/certs \
+        -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/localhost.crt \
+        -e REGISTRY_HTTP_TLS_KEY=/certs/localhost.key \
+        registry:2
+      args:
+        chdir: "{{ registry_tempdir.path }}"
+
+    - include_role:
+        name: "upload-{{ (container_command == 'docker') | ternary('docker', 'container') }}-image"
+      vars:
         zuul: "{{ new_zuul }}"