diff --git a/ansible/docker-devicemapper.yml b/ansible/docker-devicemapper.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e61c97644e7d6261568254939500f08321532b3b
--- /dev/null
+++ b/ansible/docker-devicemapper.yml
@@ -0,0 +1,11 @@
+---
+- name: Ensure docker devicemapper storage is configured
+  hosts: docker
+  tags:
+    - docker
+    - docker-devicemapper
+  tasks:
+    - name: Ensure docker devicemapper storage is configured
+      include_role:
+        name: docker-devicemapper
+      when: docker_storage_driver == 'devicemapper'
diff --git a/ansible/docker.yml b/ansible/docker.yml
index 712fda784cddcd2f12cba6ed1c4aaade90c525e6..c2405444abcd890bb0e3bbc462b7761ac221aeb1 100644
--- a/ansible/docker.yml
+++ b/ansible/docker.yml
@@ -7,4 +7,3 @@
     - docker_upper_constraints_file: "{{ pip_upper_constraints_file }}"
   roles:
     - role: docker
-      docker_daemon_mtu: "{{ public_net_name | net_mtu | default }}"
diff --git a/ansible/kolla-ansible.yml b/ansible/kolla-ansible.yml
index b5b7bb218ffe6b995fe3f592d2ca3bab065deebc..50481f5a5959bd94de0864dec92eba14dfc1662c 100644
--- a/ansible/kolla-ansible.yml
+++ b/ansible/kolla-ansible.yml
@@ -303,3 +303,4 @@
         # While kayobe has its own support for installing an NTP daemon, the
         # kolla-ansible baremetal role does a one-time sync which is useful.
         kolla_enable_host_ntp: "{{ ntp_service_enabled }}"
+        docker_daemon_mtu: "{{ public_net_name | net_mtu | default }}"
diff --git a/ansible/roles/docker-devicemapper/defaults/main.yml b/ansible/roles/docker-devicemapper/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b9f813b0b5202fb961ea1e4102dea64c6f32993d
--- /dev/null
+++ b/ansible/roles/docker-devicemapper/defaults/main.yml
@@ -0,0 +1,25 @@
+---
+# Name of the docker storage driver.
+docker_storage_driver: devicemapper
+
+# Name of the docker storage LVM volume group.
+docker_storage_volume_group:
+
+# Name of the docker storage data LVM volume.
+docker_storage_volume_thinpool:
+
+# Size of the docker storage data LVM volume (see lvol module size argument).
+docker_storage_volume_thinpool_size:
+
+# Name of the docker storage metadata LVM volume.
+docker_storage_volume_thinpool_meta:
+
+# Size of the docker storage metadata LVM volume (see lvol module size
+# argument).
+docker_storage_volume_thinpool_meta_size:
+
+# Threshold at which to extend thin-provisioned docker storage volumes.
+docker_storage_thinpool_autoextend_threshold: 80
+
+# Percentage by which to extend thin-provisioned docker storage volumes.
+docker_storage_thinpool_autoextend_percent: 20
diff --git a/ansible/roles/docker-devicemapper/handlers/main.yml b/ansible/roles/docker-devicemapper/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b7bf0f8303b6b79cf18900097b71d44e021374da
--- /dev/null
+++ b/ansible/roles/docker-devicemapper/handlers/main.yml
@@ -0,0 +1,13 @@
+---
+- name: Ensure the docker storage volume is converted to a thinpool
+  command: >
+    lvconvert -y --zero n -c 512K
+    --thinpool {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool }}
+    --poolmetadata {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool_meta }}
+  become: True
+
+- name: Ensure the docker storage metadata profile is applied
+  command: >
+    lvchange --metadataprofile docker-thinpool
+    {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool }}
+  become: True
diff --git a/ansible/roles/docker/tasks/storage.yml b/ansible/roles/docker-devicemapper/tasks/main.yml
similarity index 54%
rename from ansible/roles/docker/tasks/storage.yml
rename to ansible/roles/docker-devicemapper/tasks/main.yml
index eed6b6a13ac823621ab5fc77ebb5c2f9df457b58..fbda5a6075900b6a6424e07f22a181ef8020c1a1 100644
--- a/ansible/roles/docker/tasks/storage.yml
+++ b/ansible/roles/docker-devicemapper/tasks/main.yml
@@ -1,21 +1,33 @@
 ---
-- name: Ensure the docker daemon is stopped
-  service:
-    name: docker
-    state: stopped
-  become: True
-  notify: restart docker service
+- name: Query docker daemon information
+  command: "docker info"
+  register: docker_info
+  changed_when: False
+  failed_when: False
+
+- name: Fail when non-devicemapper containers or images exist
+  fail:
+    msg: >
+      Not configuring docker storage in {{ docker_storage_driver }} mode as
+      non-devicemapper containers or images exist.
+  when:
+    - docker_info.rc == 0
+    - "'Data loop file' in docker_info.stdout or 'devicemapper' not in docker_info.stdout"
+    - "'Images: 0' not in docker_info.stdout or 'Containers: 0' not in docker_info.stdout"
 
-- name: Ensure loopback storage state is absent
-  file:
-    path: "{{ item }}"
-    state: absent
-  with_items:
-    - "/var/lib/docker/devicemapper"
-    - "/var/lib/docker/images"
-    - "/var/lib/docker/containers"
+- name: Ensure the docker storage metadata profile exists
+  template:
+    src: docker-thinpool.profile.j2
+    dest: /etc/lvm/profile/docker-thinpool.profile
   become: True
 
+- name: Query LVM thinpool volume
+  command: "lvs {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool }}"
+  register: lvs_result
+  changed_when: false
+  failed_when: false
+  become: true
+
 - block:
     - name: Ensure the docker storage data and metadata volumes exist
       lvol:
@@ -38,15 +50,9 @@
         --poolmetadata {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool_meta }}
       become: True
 
-    - name: Ensure the docker storage metadata profile exists
-      template:
-        src: docker-thinpool.profile.j2
-        dest: /etc/lvm/profile/docker-thinpool.profile
-      become: True
-
     - name: Ensure the docker storage metadata profile is applied
       command: >
         lvchange --metadataprofile docker-thinpool
         {{ docker_storage_volume_group }}/{{ docker_storage_volume_thinpool }}
       become: True
-  when: docker_storage_driver == 'devicemapper'
+  when: lvs_result.rc != 0
diff --git a/ansible/roles/docker/templates/docker-thinpool.profile.j2 b/ansible/roles/docker-devicemapper/templates/docker-thinpool.profile.j2
similarity index 100%
rename from ansible/roles/docker/templates/docker-thinpool.profile.j2
rename to ansible/roles/docker-devicemapper/templates/docker-thinpool.profile.j2
diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml
index 59165a2511a1ee6192e6a26e3de3ee3dd7950d5f..756d5cca36233ce30b0d12882e8de9181fbabb97 100644
--- a/ansible/roles/docker/defaults/main.yml
+++ b/ansible/roles/docker/defaults/main.yml
@@ -1,44 +1,10 @@
 ---
-# Name of the docker storage driver.
-docker_storage_driver: devicemapper
-
-# Name of the docker storage LVM volume group.
-docker_storage_volume_group:
-
-# Name of the docker storage data LVM volume.
-docker_storage_volume_thinpool:
-
-# Size of the docker storage data LVM volume (see lvol module size argument).
-docker_storage_volume_thinpool_size:
-
-# Name of the docker storage metadata LVM volume.
-docker_storage_volume_thinpool_meta:
-
-# Size of the docker storage metadata LVM volume (see lvol module size
-# argument).
-docker_storage_volume_thinpool_meta_size:
-
-# Threshold at which to extend thin-provisioned docker storage volumes.
-docker_storage_thinpool_autoextend_threshold: 80
-
-# Percentage by which to extend thin-provisioned docker storage volumes.
-docker_storage_thinpool_autoextend_percent: 20
-
 # URL of docker registry
 docker_registry:
 
 # CA of docker registry
 docker_registry_ca:
 
-# List of Docker registry mirrors.
-docker_registry_mirrors: []
-
-# MTU to pass through to containers not using net=host
-docker_daemon_mtu: 1500
-
-# Enable live-restore on docker daemon
-docker_daemon_live_restore: false
-
 # Upper constraints file which is passed to pip when installing packages
 # into a venv.
 docker_upper_constraints_file:
diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml
index 8a285ad69a7cf03300832ad2a30dfa3afa8d73c4..356c6b6228c54d1a77dd9f093a3a43c6271c9942 100644
--- a/ansible/roles/docker/handlers/main.yml
+++ b/ansible/roles/docker/handlers/main.yml
@@ -1,10 +1,4 @@
 ---
-- name: restart docker service
-  service:
-    name: docker
-    state: restarted
-  become: True
-
 - name: reload docker service
   service:
     name: docker
diff --git a/ansible/roles/docker/tasks/config.yml b/ansible/roles/docker/tasks/config.yml
deleted file mode 100644
index cd01d350f513c827b97247ae8b60040629b17910..0000000000000000000000000000000000000000
--- a/ansible/roles/docker/tasks/config.yml
+++ /dev/null
@@ -1,22 +0,0 @@
----
-- name: Ensure the docker daemon configuration file exists
-  template:
-    src: daemon.json.j2
-    dest: /etc/docker/daemon.json
-  become: True
-  notify: restart docker service
-
-- name: Ensure the path for CA file for private registry exists
-  file:
-    path: "/etc/docker/certs.d/{{ docker_registry }}"
-    state: directory
-  become: True
-  when: docker_registry is not none and docker_registry_ca is not none
-
-- name: Ensure the CA file for private registry exists
-  copy:
-    src: "{{ docker_registry_ca }}"
-    dest: "/etc/docker/certs.d/{{ docker_registry }}/ca.crt"
-  become: True
-  when: docker_registry is not none and docker_registry_ca is not none
-  notify: reload docker service
diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml
index 9a792723ed3e0c26e75a75b3ec689c703059aeab..494656e37d3763ba13d183bf817781ab8ecc4153 100644
--- a/ansible/roles/docker/tasks/main.yml
+++ b/ansible/roles/docker/tasks/main.yml
@@ -57,24 +57,17 @@
     state: started
   become: True
 
-- name: Query docker daemon information
-  command: "docker info"
-  register: docker_info
-  changed_when: False
-  until: docker_info is success
-  retries: 3
-  delay: 5
-
-- name: Fail when loopback-mode containers or images exist
-  fail:
-    msg: >
-      Not configuring docker storage in {{ docker_storage_driver }} mode as
-      loopback-backed containers or images exist.
-  when:
-    - "'Data loop file' in docker_info.stdout or docker_storage_driver not in docker_info.stdout"
-    - "'Images: 0' not in docker_info.stdout or 'Containers: 0' not in docker_info.stdout"
-
-- include_tasks: storage.yml
-  when: "'Data loop file' in docker_info.stdout or docker_storage_driver not in docker_info.stdout"
+- name: Ensure the path for CA file for private registry exists
+  file:
+    path: "/etc/docker/certs.d/{{ docker_registry }}"
+    state: directory
+  become: True
+  when: docker_registry is not none and docker_registry_ca is not none
 
-- include_tasks: config.yml
+- name: Ensure the CA file for private registry exists
+  copy:
+    src: "{{ docker_registry_ca }}"
+    dest: "/etc/docker/certs.d/{{ docker_registry }}/ca.crt"
+  become: True
+  when: docker_registry is not none and docker_registry_ca is not none
+  notify: reload docker service
diff --git a/ansible/roles/kolla-ansible/defaults/main.yml b/ansible/roles/kolla-ansible/defaults/main.yml
index e239cf0def3b66ed61b89b935145e2ca4d60f845..d7a440a216d1b7b9be6479524aeff77e0d1d3e4a 100644
--- a/ansible/roles/kolla-ansible/defaults/main.yml
+++ b/ansible/roles/kolla-ansible/defaults/main.yml
@@ -312,3 +312,30 @@ kolla_selinux_state:
 
 # Whether to enable the NTP daemon.
 kolla_enable_host_ntp:
+
+###############################################################################
+# Docker configuration.
+
+# Name of the docker storage driver.
+docker_storage_driver: devicemapper
+
+# Name of the docker storage LVM volume group.
+docker_storage_volume_group:
+
+# Name of the docker storage data LVM volume.
+docker_storage_volume_thinpool:
+
+# URL of docker registry
+docker_registry:
+
+# CA of docker registry
+docker_registry_ca:
+
+# List of Docker registry mirrors.
+docker_registry_mirrors: []
+
+# MTU to pass through to containers not using net=host
+docker_daemon_mtu: 1500
+
+# Enable live-restore on docker daemon
+docker_daemon_live_restore: false
diff --git a/ansible/roles/kolla-ansible/tasks/config.yml b/ansible/roles/kolla-ansible/tasks/config.yml
index 55849026c731fd71bef7279e4603461d56b3ac95..6519fc892c266cf6abf94865fff783d0618dd737 100644
--- a/ansible/roles/kolla-ansible/tasks/config.yml
+++ b/ansible/roles/kolla-ansible/tasks/config.yml
@@ -45,6 +45,8 @@
     src: "globals.yml.j2"
     dest: "{{ kolla_config_path }}/globals.yml"
     mode: 0640
+  vars:
+    kolla_docker_custom_config: "{{ lookup('template', 'daemon.json.j2') }}"
 
 - name: Ensure the Kolla seed inventory file exists
   copy:
diff --git a/ansible/roles/docker/templates/daemon.json.j2 b/ansible/roles/kolla-ansible/templates/daemon.json.j2
similarity index 93%
rename from ansible/roles/docker/templates/daemon.json.j2
rename to ansible/roles/kolla-ansible/templates/daemon.json.j2
index f2d63d78b3e1861ed624f5ceb8abf8d58c4458dc..da8ede5251ef1ada7aa7d91208524c2bee102981 100644
--- a/ansible/roles/docker/templates/daemon.json.j2
+++ b/ansible/roles/kolla-ansible/templates/daemon.json.j2
@@ -6,7 +6,6 @@
 {%- endfor %}
   ],
 {%- endif %}
-  "storage-driver": "{{ docker_storage_driver }}",
 {% if docker_daemon_mtu %}
   "mtu": {{ docker_daemon_mtu }},
 {% endif %}
diff --git a/ansible/roles/kolla-ansible/templates/globals.yml.j2 b/ansible/roles/kolla-ansible/templates/globals.yml.j2
index 05fc332d9b5ce67aeb7705c367b74961b9061878..f34a66f2eca77727f9e58b344ab85d0e4024e82f 100644
--- a/ansible/roles/kolla-ansible/templates/globals.yml.j2
+++ b/ansible/roles/kolla-ansible/templates/globals.yml.j2
@@ -68,6 +68,8 @@ docker_namespace: "{{ kolla_docker_namespace }}"
 docker_registry_username: "{{ kolla_docker_registry_username }}"
 docker_registry_password: "{{ kolla_docker_registry_password }}"
 {% endif %}
+docker_storage_driver: "{{ docker_storage_driver }}"
+docker_custom_config: {{ kolla_docker_custom_config | to_nice_json | indent(2) }}
 
 ###################
 # Messaging options
diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py
index 9f0493366bac30c2dec1f2bc183b350ab1736a0a..170cf194f395ed914ebfae218deb8a84bcde0a6c 100644
--- a/kayobe/cli/commands.py
+++ b/kayobe/cli/commands.py
@@ -546,7 +546,7 @@ class SeedHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         playbooks += _build_playbook_list(
             "users", "yum", "dev-tools", "disable-selinux", "network",
             "sysctl", "ip-routing", "snat", "disable-glean", "ntp", "mdadm",
-            "lvm")
+            "lvm", "docker-devicemapper")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="seed")
 
         self.generate_kolla_ansible_config(parsed_args, service_config=False)
@@ -946,7 +946,7 @@ class OvercloudHostConfigure(KollaAnsibleMixin, KayobeAnsibleMixin, VaultMixin,
         playbooks += _build_playbook_list(
             "users", "yum", "dev-tools", "disable-selinux", "network",
             "sysctl", "disable-glean", "disable-cloud-init", "ntp", "mdadm",
-            "lvm")
+            "lvm", "docker-devicemapper")
         self.run_kayobe_playbooks(parsed_args, playbooks, limit="overcloud")
 
         self.generate_kolla_ansible_config(parsed_args, service_config=False)
diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py
index 8aad7d2268bc61df9c752081bf90f7e1922f5a9f..25e461207344501cdbcab14d1a9458841e4d098e 100644
--- a/kayobe/tests/unit/cli/test_commands.py
+++ b/kayobe/tests/unit/cli/test_commands.py
@@ -510,6 +510,8 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ntp.yml"),
                     utils.get_data_files_path("ansible", "mdadm.yml"),
                     utils.get_data_files_path("ansible", "lvm.yml"),
+                    utils.get_data_files_path("ansible",
+                                              "docker-devicemapper.yml"),
                 ],
                 limit="seed",
             ),
@@ -1135,6 +1137,8 @@ class TestCase(unittest.TestCase):
                     utils.get_data_files_path("ansible", "ntp.yml"),
                     utils.get_data_files_path("ansible", "mdadm.yml"),
                     utils.get_data_files_path("ansible", "lvm.yml"),
+                    utils.get_data_files_path("ansible",
+                                              "docker-devicemapper.yml"),
                 ],
                 limit="overcloud",
             ),
diff --git a/releasenotes/notes/docker-custom-config-5103260d5ddb7223.yaml b/releasenotes/notes/docker-custom-config-5103260d5ddb7223.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..32765860356e8a5f637bc186f4980e4129835c6e
--- /dev/null
+++ b/releasenotes/notes/docker-custom-config-5103260d5ddb7223.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Uses the new Kolla Ansible variable ``docker_custom_config`` to populate
+    Docker's ``daemon.json`` configuration file.