diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index 31db08125b..e513c9b1b8 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -40,6 +40,7 @@ database_user: "root"
 ####################
 # Docker options
 ####################
+docker_registry_email:
 docker_registry:
 docker_namespace: "kollaglue"
 docker_registry_username:
@@ -54,6 +55,18 @@ docker_restart_policy: "always"
 # '0' means unlimited retries
 docker_restart_policy_retry: "10"
 
+# Common options used throughout docker
+docker_common_options:
+    auth_email: "{{ docker_registry_email }}"
+    auth_password: "{{ docker_registry_password }}"
+    auth_registry: "{{ docker_registry }}"
+    auth_username: "{{ docker_registry_username }}"
+    environment:
+      KOLLA_CONFIG_STRATEGY: "{{ config_strategy }}"
+    insecure_registry: "{{ docker_insecure_registry }}"
+    restart_policy: "{{ docker_restart_policy }}"
+    restart_retries: "{{ docker_restart_policy_retry }}"
+
 
 ####################
 # Networking options
diff --git a/ansible/library/kolla_docker.py b/ansible/library/kolla_docker.py
new file mode 100644
index 0000000000..b2006b4359
--- /dev/null
+++ b/ansible/library/kolla_docker.py
@@ -0,0 +1,436 @@
+#!/usr/bin/python
+
+# Copyright 2015 Sam Yaple
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+DOCUMENTATION = '''
+---
+module: kolla_docker
+short_description: Module for controling Docker
+description:
+     - A module targeting at controling Docker as used by Kolla.
+options:
+  example:
+    description:
+      - example
+    required: True
+    type: bool
+author: Sam Yaple
+'''
+
+EXAMPLES = '''
+- hosts: kolla_docker
+  tasks:
+    - name: Start container
+      kolla_docker:
+          example: False
+'''
+
+import os
+
+import docker
+
+
+class DockerWorker(object):
+
+    def __init__(self, module):
+        self.module = module
+        self.params = self.module.params
+        self.changed = False
+
+        # TLS not fully implemented
+        # tls_config = self.generate_tls()
+
+        options = {
+            'version': self.params.get('api_version')
+        }
+
+        self.dc = docker.Client(**options)
+
+    def generate_tls(self):
+        tls = {'verify': self.params.get('tls_verify')}
+        tls_cert = self.params.get('tls_cert'),
+        tls_key = self.params.get('tls_key'),
+        tls_cacert = self.params.get('tls_cacert')
+
+        if tls['verify']:
+            if tlscert:
+                self.check_file(tls['tls_cert'])
+                self.check_file(tls['tls_key'])
+                tls['client_cert'] = (tls_cert, tls_key)
+            if tlscacert:
+                self.check_file(tls['tls_cacert'])
+                tls['verify'] = tls_cacert
+
+        return docker.tls.TLSConfig(**tls)
+
+    def check_file(self, path):
+        if not os.path.isfile(path):
+            self.module.fail_json(
+                failed=True,
+                msg='There is no file at "{}"'.format(path)
+            )
+        if not os.access(path, os.R_OK):
+            self.module.fail_json(
+                failed=True,
+                msg='Permission denied for file at "{}"'.format(path)
+            )
+
+    def check_image(self):
+        find_image = ':'.join(self.parse_image())
+        for image in self.dc.images():
+            for image_name in image['RepoTags']:
+                if image_name == find_image:
+                    return image
+
+    def check_volume(self):
+        for vol in self.dc.volumes()['Volumes']:
+            if vol['Name'] == self.params.get('name'):
+                return vol
+
+    def check_container(self):
+        find_name = '/{}'.format(self.params.get('name'))
+        for cont in self.dc.containers(all=True):
+            if find_name in cont['Names']:
+                return cont
+
+    def check_container_differs(self):
+        container = self.check_container()
+        if not container:
+            return True
+        container_info = self.dc.inspect_container(self.params.get('name'))
+
+        return (
+            self.compare_image(container_info) or
+            self.compare_privileged(container_info) or
+            self.compare_pid_mode(container_info) or
+            self.compare_volumes(container_info) or
+            self.compare_volumes_from(container_info) or
+            self.compare_environment(container_info)
+        )
+
+    def compare_pid_mode(self, container_info):
+        new_pid_mode = self.params.get('pid_mode')
+        current_pid_mode = container_info['HostConfig'].get('PidMode')
+        if not current_pid_mode:
+            current_pid_mode = None
+
+        if new_pid_mode != current_pid_mode:
+            return True
+
+    def compare_privileged(self, container_info):
+        new_privileged = self.params.get('privileged')
+        current_privileged = container_info['HostConfig']['Privileged']
+        if new_privileged != current_privileged:
+            return True
+
+    def compare_image(self, container_info):
+        new_image = self.check_image()
+        current_image = container_info['Image']
+        if new_image['Id'] != current_image:
+            return True
+
+    def compare_volumes_from(self, container_info):
+        new_vols_from = self.params.get('volumes_from')
+        current_vols_from = container_info['HostConfig'].get('VolumesFrom')
+        if not new_vols_from:
+            new_vols_from = list()
+        if not current_vols_from:
+            current_vols_from = list()
+
+        if set(current_vols_from).symmetric_difference(set(new_vols_from)):
+            return True
+
+    def compare_volumes(self, container_info):
+        volumes, binds = self.generate_volumes()
+        current_vols = container_info['Config'].get('Volumes')
+        current_binds = container_info['HostConfig'].get('Binds')
+        if not volumes:
+            volumes = list()
+        if not current_vols:
+            current_vols = list()
+        if not current_binds:
+            current_binds = list()
+
+        if set(volumes).symmetric_difference(set(current_vols)):
+            return True
+
+        new_binds = list()
+        if binds:
+            for k, v in binds.iteritems():
+                new_binds.append("{}:{}:{}".format(k, v['bind'], v['mode']))
+
+        if set(new_binds).symmetric_difference(set(current_binds)):
+            return True
+
+    def compare_environment(self, container_info):
+        if self.params.get('environment'):
+            current_env = dict()
+            for kv in container_info['Config'].get('Env', list()):
+                k, v = kv.split('=', 1)
+                current_env.update({k: v})
+
+            for k, v in self.params.get('environment').iteritems():
+                if k not in current_env:
+                    return True
+                if current_env[k] != v:
+                    return True
+
+    def parse_image(self):
+        full_image = self.params.get('image')
+
+        if '/' in full_image:
+            registry, image = full_image.split('/', 1)
+        else:
+            image = full_image
+
+        if ':' in image:
+            return full_image.rsplit(':', 1)
+        else:
+            return full_image, 'latest'
+
+    def pull_image(self):
+        if self.params.get('auth_username'):
+            self.dc.login(
+                username=self.params.get('auth_username'),
+                password=self.params.get('auth_password'),
+                registry=self.params.get('auth_registry'),
+                email=self.params.get('auth_email')
+            )
+
+        image, tag = self.parse_image()
+
+        status = [
+            json.loads(line.strip()) for line in self.dc.pull(
+                repository=image, tag=tag, stream=True
+            )
+        ]
+
+        if "Downloaded newer image for" in status[-1].get('status'):
+            self.changed = True
+        elif "Image is up to date for" in status[-1].get('status'):
+            # No new layer was pulled, no change
+            pass
+        else:
+            self.module.fail_json(
+                msg="Invalid status returned from pull",
+                changed=True,
+                failed=True
+            )
+
+    def remove_container(self):
+        if self.check_container():
+            self.changed = True
+            self.dc.remove_container(
+                container=self.params.get('name'),
+                force=True
+            )
+
+    def generate_volumes(self):
+        volumes = self.params.get('volumes')
+        if not volumes:
+            return None, None
+
+        vol_list = list()
+        vol_dict = dict()
+
+        for vol in volumes:
+            if ':' not in vol:
+                vol_list.append(vol)
+                continue
+
+            split_vol = vol.split(':')
+
+            if (len(split_vol) == 2
+               and ('/' not in split_vol[0] or '/' in split_vol[1])):
+                split_vol.append('rw')
+
+            vol_list.append(split_vol[1])
+            vol_dict.update({
+                split_vol[0]: {
+                    'bind': split_vol[1],
+                    'mode': split_vol[2]
+                }
+            })
+
+        return vol_list, vol_dict
+
+    def build_host_config(self, binds):
+        options = {
+            'network_mode': 'host',
+            'pid_mode': self.params.get('pid_mode'),
+            'privileged': self.params.get('privileged'),
+            'volumes_from': self.params.get('volumes_from')
+        }
+
+        if self.params.get('restart_policy') in ['on-failure', 'always']:
+            options['restart_policy'] = {
+                'Name': self.params.get('restart_policy'),
+                'MaximumRetryCount': self.params.get('restart_retries')
+            }
+
+        if binds:
+            options['binds'] = binds
+
+        return self.dc.create_host_config(**options)
+
+    def build_container_options(self):
+        volumes, binds = self.generate_volumes()
+        return {
+            'detach': self.params.get('detach'),
+            'environment': self.params.get('environment'),
+            'host_config': self.build_host_config(binds),
+            'image': self.params.get('image'),
+            'name': self.params.get('name'),
+            'volumes': volumes,
+            'tty': True
+        }
+
+    def create_container(self):
+        self.changed = True
+        options = self.build_container_options()
+        self.dc.create_container(**options)
+
+    def start_container(self):
+        if not self.check_image():
+            self.pull_image()
+
+        container = self.check_container()
+        if container and self.check_container_differs():
+            self.remove_container()
+            container = self.check_container()
+
+        if not container:
+            self.create_container()
+            container = self.check_container()
+
+        if not container['Status'].startswith('Up '):
+            self.changed = True
+            self.dc.start(container=self.params.get('name'))
+
+        # We do not want to detach so we wait around for container to exit
+        if not self.params.get('detach'):
+            rc = self.dc.wait(self.params.get('name'))
+            if rc != 0:
+                self.module.fail_json(
+                    failed=True,
+                    changed=True,
+                    msg="Container exited with non-zero return code"
+                )
+            if self.params.get('remove_on_exit'):
+                self.remove_container()
+
+    def create_volume(self):
+        if not self.check_volume():
+            self.changed = True
+            self.dc.create_volume(name=self.params.get('name'), driver='local')
+
+    def remove_volume(self):
+        if self.check_volume():
+            self.changed = True
+            try:
+                self.dc.remove_volume(name=self.params.get('name'))
+            except docker.errors.APIError as e:
+                if e.response.status_code == 409:
+                    self.module.fail_json(
+                        failed=True,
+                        msg="Volume named '{}' is currently in-use".format(
+                            self.params.get('name')
+                        )
+                    )
+                raise
+
+
+def generate_module():
+    argument_spec = dict(
+        common_options=dict(required=False, type='dict', default=dict()),
+        action=dict(requried=True, type='str', choices=['create_volume',
+                                                        'pull_image',
+                                                        'remove_container',
+                                                        'remove_volume',
+                                                        'start_container']),
+        api_version=dict(required=False, type='str', default='auto'),
+        auth_email=dict(required=False, type='str'),
+        auth_password=dict(required=False, type='str'),
+        auth_registry=dict(required=False, type='str'),
+        auth_username=dict(required=False, type='str'),
+        detach=dict(required=False, type='bool', default=True),
+        name=dict(required=True, type='str'),
+        environment=dict(required=False, type='dict'),
+        image=dict(required=False, type='str'),
+        insecure_registry=dict(required=False, type='bool', default=False),
+        pid_mode=dict(required=False, type='str', choices=['host']),
+        privileged=dict(required=False, type='bool', default=False),
+        remove_on_exit=dict(required=False, type='bool', default=True),
+        restart_policy=dict(required=False, type='str', choices=['no',
+                                                                 'never',
+                                                                 'on-failure',
+                                                                 'always']),
+        restart_retries=dict(required=False, type='int', default=10),
+        tls_verify=dict(required=False, type='bool', default=False),
+        tls_cert=dict(required=False, type='str'),
+        tls_key=dict(required=False, type='str'),
+        tls_cacert=dict(required=False, type='str'),
+        volumes=dict(required=False, type='list'),
+        volumes_from=dict(required=False, type='list')
+    )
+    required_together = [
+        ['tls_cert', 'tls_key']
+    ]
+    return AnsibleModule(
+        argument_spec=argument_spec,
+        required_together=required_together
+    )
+
+
+def generate_nested_module():
+    module = generate_module()
+
+    # We unnest the common dict and the update it with the other options
+    new_args = module.params.get('common_options')
+    new_args.update(module._load_params()[0])
+    module.params = new_args
+
+    # Override ARGS to ensure new args are used
+    global MODULE_ARGS
+    global MODULE_COMPLEX_ARGS
+    MODULE_ARGS = ''
+    MODULE_COMPLEX_ARGS = json.dumps(module.params)
+
+    # Reprocess the args now that the common dict has been unnested
+    return generate_module()
+
+
+def main():
+    module = generate_nested_module()
+
+    # TODO(SamYaple): Replace with required_if when Ansible 2.0 lands
+    if (module.params.get('action') in ['pull_image', 'start_container']
+       and not module.params.get('image')):
+        self.module.fail_json(
+            msg="missing required arguments: image",
+            failed=True
+        )
+
+    try:
+        dw = DockerWorker(module)
+        getattr(dw, module.params.get('action'))()
+        module.exit_json(changed=dw.changed)
+    except Exception as e:
+        module.exit_json(failed=True, changed=True, msg=repr(e))
+
+# import module snippets
+from ansible.module_utils.basic import *  # noqa
+if __name__ == '__main__':
+    main()