Repository: docker/docker-py

Issue:
Using volumes does not work as explained in docs. This breaks projects like fig.
In newer versions of docker (>= v1.4.0), docker-py does not bind-mount volumes.

If volume is specified in `volumes` parameter to `c.create_container()`,  then volume is created. But according to docs, when `binds` parameter is specified to  `c.start()`,  volume should be bind mounted.

http://docker-py.readthedocs.org/en/latest/volumes/

But it seems that `start()` **only** binds volumes **NOT** specified on `volumes` parameter of `c.create_volumes()`.

I think this is breaking projects like fig (can't mount volumes) or saltstack dockerio module:
https://github.com/docker/fig/issues/622

Code example reproducing issue for me:

``` python
from pprint import pprint
from docker import Client
c = Client(base_url='unix://var/run/docker.sock')

res = c.create_container('busybox', name='pytest', detach=True, 
    command='sh -c "while true;do echo test;sleep 1;done"',
    volumes=['/mnt/test',])
binds={
    '/test':
        {
            'bind': '/mnt/test',
            'ro': False
        },
    '/var/www':
        {
            'bind': '/mnt/vol1',
            'ro': True
        }
}
# start
c.start(res['Id'], binds=binds)

# inspect container
pprint(c.inspect_container(res['Id'])['Volumes'])

#result
{u'/mnt/test': u'/var/lib/docker/vfs/dir/3727ff0ae94652889ad5d1095b38c62fe24defd0aa952787cf1e1b0b6b04faf7',
 u'/mnt/vol1': u'/var/www'}

```

You can see as `/var/www` is correctly bind-mounted, but `/mnt/test` is not. The difference is that `/mnt/vol1` was not specified in `volumes` parameter to `c.create_container()`

Do you think is a docker-py issue with newer docker engine?



Discussion:
This shouldn't be an issue on < 1.4
In 1.4 passing in HostConfig (ie, bind mounts, volumes-from, other host specific things) on start is deprecated... that said you can still pass in hostconfig on start.
.. However, in 1.4 volumes are now created when at container creation instead of on container start... so if the Binds parameter is not passed in on create, the bind-mounts are not created.

> .. However, in 1.4 volumes are now created when at container creation instead of on container start... 
> so if the Binds parameter is not passed in on create, the bind-mounts are not created.

Are you sure? , In my example, `binds` parameter is passed on `start()`, not on `create_container()`, and it works.

Any way, seems clear that API does not work as stated on docs, at least with docker v1.4

I'm writing some tests for this right now on the docker side.
But I'm pretty sure that if a volume was passed into the create command (without specifying the binds in the HostConfig), if you then pass the Binds parameter on start, it will be ignored, since a volume would already be initialized for the given volume path.

Yes,  I agree, this is what happens in my test.
But also, I see that if you don't specify `volumes` nor `binds` to `create()`, and only pass `binds` to `start()`, it works (see `/mnt/vol1` in example above)

Confirmed, only happens in docker >=1.4 , I have modified my issue description, sorry.

I'm going to work on a fix on the Docker side since this was an unexpected breaking change.
However, docker-py should definitely be updated to pass all container configuration on create.

@cpuguy83 

> In 1.4 passing in HostConfig on start is deprecated

It sounds like it's actually been removed entirely, not just deprecated

No, just deprecated.  The issue is with the volume initialization.
I'll have a PR up momentarily that fixes this on the Docker side.

@cpuguy83 awesome, thanks!

Issue is detailed here: docker/docker#9628


Resolving patch:
diff --git a/docker/client.py b/docker/client.py
--- a/docker/client.py
+++ b/docker/client.py
@@ -34,7 +34,7 @@
 if not six.PY3:
     import websocket
 
-DEFAULT_DOCKER_API_VERSION = '1.15'
+DEFAULT_DOCKER_API_VERSION = '1.16'
 DEFAULT_TIMEOUT_SECONDS = 60
 STREAM_HEADER_SIZE_BYTES = 8
 
@@ -110,7 +110,8 @@ def _container_config(self, image, command, hostname=None, user=None,
                           volumes=None, volumes_from=None,
                           network_disabled=False, entrypoint=None,
                           cpu_shares=None, working_dir=None,
-                          domainname=None, memswap_limit=0, cpuset=None):
+                          domainname=None, memswap_limit=0, cpuset=None,
+                          host_config=None):
         if isinstance(command, six.string_types):
             command = shlex.split(str(command))
         if isinstance(environment, dict):
@@ -225,7 +226,8 @@ def _container_config(self, image, command, hostname=None, user=None,
             'CpuShares': cpu_shares,
             'Cpuset': cpuset,
             'WorkingDir': working_dir,
-            'MemorySwap': memswap_limit
+            'MemorySwap': memswap_limit,
+            'HostConfig': host_config
         }
 
     def _post_json(self, url, data, **kwargs):
@@ -263,6 +265,12 @@ def _attach_websocket(self, container, params=None):
     def _create_websocket_connection(self, url):
         return websocket.create_connection(url)
 
+    def _warn_deprecated(self, arg_name, version):
+        warning_message = (
+            '{0!r} is deprecated for API version >= {1}'
+        ).format(arg_name, version)
+        warnings.warn(warning_message, DeprecationWarning)
+
     def _get_raw_response_socket(self, response):
         self._raise_for_status(response)
         if six.PY3:
@@ -536,17 +544,20 @@ def create_container(self, image, command=None, hostname=None, user=None,
                          mem_limit=0, ports=None, environment=None, dns=None,
                          volumes=None, volumes_from=None,
                          network_disabled=False, name=None, entrypoint=None,
-                         cpu_shares=None, working_dir=None,
-                         domainname=None, memswap_limit=0, cpuset=None):
+                         cpu_shares=None, working_dir=None, domainname=None,
+                         memswap_limit=0, cpuset=None, host_config=None):
 
         if isinstance(volumes, six.string_types):
             volumes = [volumes, ]
 
+        if host_config and utils.compare_version('1.15', self._version) < 0:
+            raise errors.APIError('host_config is not supported in API < 1.15')
+
         config = self._container_config(
             image, command, hostname, user, detach, stdin_open, tty, mem_limit,
             ports, environment, dns, volumes, volumes_from, network_disabled,
             entrypoint, cpu_shares, working_dir, domainname,
-            memswap_limit, cpuset
+            memswap_limit, cpuset, host_config
         )
         return self.create_container_from_config(config, name)
 
@@ -570,7 +581,7 @@ def events(self):
     def execute(self, container, cmd, detach=False, stdout=True, stderr=True,
                 stream=False, tty=False):
         if utils.compare_version('1.15', self._version) < 0:
-            raise Exception('Exec is not supported in API < 1.15!')
+            raise errors.APIError('Exec is not supported in API < 1.15')
         if isinstance(container, dict):
             container = container.get('Id')
         if isinstance(cmd, six.string_types):
@@ -911,6 +922,9 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
               dns=None, dns_search=None, volumes_from=None, network_mode=None,
               restart_policy=None, cap_add=None, cap_drop=None, devices=None,
               extra_hosts=None):
+
+        start_config = {}
+
         if isinstance(container, dict):
             container = container.get('Id')
 
@@ -920,9 +934,9 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
                 formatted.append({'Key': k, 'Value': str(v)})
             lxc_conf = formatted
 
-        start_config = {
-            'LxcConf': lxc_conf
-        }
+        if lxc_conf:
+            start_config['LxcConf'] = lxc_conf
+
         if binds:
             start_config['Binds'] = utils.convert_volume_binds(binds)
 
@@ -931,7 +945,8 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
                 port_bindings
             )
 
-        start_config['PublishAllPorts'] = publish_all_ports
+        if publish_all_ports:
+            start_config['PublishAllPorts'] = publish_all_ports
 
         if links:
             if isinstance(links, dict):
@@ -953,7 +968,8 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
 
             start_config['ExtraHosts'] = formatted_extra_hosts
 
-        start_config['Privileged'] = privileged
+        if privileged:
+            start_config['Privileged'] = privileged
 
         if utils.compare_version('1.10', self._version) >= 0:
             if dns is not None:
@@ -963,16 +979,15 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
                     volumes_from = volumes_from.split(',')
                 start_config['VolumesFrom'] = volumes_from
         else:
-            warning_message = ('{0!r} parameter is discarded. It is only'
-                               ' available for API version greater or equal'
-                               ' than 1.10')
 
             if dns is not None:
-                warnings.warn(warning_message.format('dns'),
-                              DeprecationWarning)
+                raise errors.APIError(
+                    'dns is only supported for API version >= 1.10'
+                )
             if volumes_from is not None:
-                warnings.warn(warning_message.format('volumes_from'),
-                              DeprecationWarning)
+                raise errors.APIError(
+                    'volumes_from is only supported for API version >= 1.10'
+                )
         if dns_search:
             start_config['DnsSearch'] = dns_search
 
@@ -992,6 +1007,8 @@ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
             start_config['Devices'] = utils.parse_devices(devices)
 
         url = self._url("/containers/{0}/start".format(container))
+        if not start_config:
+            start_config = None
         res = self._post_json(url, data=start_config)
         self._raise_for_status(res)
 
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -1,5 +1,5 @@
 from .utils import (
     compare_version, convert_port_bindings, convert_volume_binds,
     mkbuildcontext, ping, tar, parse_repository_tag, parse_host,
-    kwargs_from_env, convert_filters
+    kwargs_from_env, convert_filters, create_host_config
 ) # flake8: noqa
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -294,3 +294,70 @@ def convert_filters(filters):
             v = [v, ]
         result[k] = v
     return json.dumps(result)
+
+
+def create_host_config(
+    binds=None, port_bindings=None, lxc_conf=None,
+    publish_all_ports=False, links=None, privileged=False,
+    dns=None, dns_search=None, volumes_from=None, network_mode=None,
+    restart_policy=None, cap_add=None, cap_drop=None, devices=None
+):
+    host_config = {
+        'Privileged': privileged,
+        'PublishAllPorts': publish_all_ports,
+    }
+
+    if dns_search:
+        host_config['DnsSearch'] = dns_search
+
+    if network_mode:
+        host_config['NetworkMode'] = network_mode
+
+    if restart_policy:
+        host_config['RestartPolicy'] = restart_policy
+
+    if cap_add:
+        host_config['CapAdd'] = cap_add
+
+    if cap_drop:
+        host_config['CapDrop'] = cap_drop
+
+    if devices:
+        host_config['Devices'] = parse_devices(devices)
+
+    if dns is not None:
+        host_config['Dns'] = dns
+
+    if volumes_from is not None:
+        if isinstance(volumes_from, six.string_types):
+            volumes_from = volumes_from.split(',')
+        host_config['VolumesFrom'] = volumes_from
+
+    if binds:
+        host_config['Binds'] = convert_volume_binds(binds)
+
+    if port_bindings:
+        host_config['PortBindings'] = convert_port_bindings(
+            port_bindings
+        )
+
+    host_config['PublishAllPorts'] = publish_all_ports
+
+    if links:
+        if isinstance(links, dict):
+            links = six.iteritems(links)
+
+        formatted_links = [
+            '{0}:{1}'.format(k, v) for k, v in sorted(links)
+        ]
+
+        host_config['Links'] = formatted_links
+
+    if isinstance(lxc_conf, dict):
+        formatted = []
+        for k, v in six.iteritems(lxc_conf):
+            formatted.append({'Key': k, 'Value': str(v)})
+        lxc_conf = formatted
+    host_config['LxcConf'] = lxc_conf
+
+    return host_config


Test patch:
diff --git a/tests/fake_api.py b/tests/fake_api.py
--- a/tests/fake_api.py
+++ b/tests/fake_api.py
@@ -12,7 +12,7 @@
 #    See the License for the specific language governing permissions and
 #    limitations under the License.
 
-CURRENT_VERSION = 'v1.15'
+CURRENT_VERSION = 'v1.16'
 
 FAKE_CONTAINER_ID = '3cc2351ab11b'
 FAKE_IMAGE_ID = 'e9aa60c60128'
diff --git a/tests/integration_test.py b/tests/integration_test.py
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -33,6 +33,9 @@
 
 DEFAULT_BASE_URL = os.environ.get('DOCKER_HOST')
 
+warnings.simplefilter('error')
+create_host_config = docker.utils.create_host_config
+
 
 class BaseTestCase(unittest.TestCase):
     tmp_imgs = []
@@ -154,22 +157,51 @@ def runTest(self):
 
         filename = 'shared.txt'
         shared_file = os.path.join(mount_origin, filename)
+        binds = {
+            mount_origin: {
+                'bind': mount_dest,
+                'ro': False,
+            },
+        }
 
         with open(shared_file, 'w'):
             container = self.client.create_container(
                 'busybox',
-                ['ls', mount_dest], volumes={mount_dest: {}}
+                ['ls', mount_dest], volumes={mount_dest: {}},
+                host_config=create_host_config(binds=binds)
             )
             container_id = container['Id']
-            self.client.start(
-                container_id,
-                binds={
-                    mount_origin: {
-                        'bind': mount_dest,
-                        'ro': False,
-                    },
-                },
+            self.client.start(container_id)
+            self.tmp_containers.append(container_id)
+            exitcode = self.client.wait(container_id)
+            self.assertEqual(exitcode, 0)
+            logs = self.client.logs(container_id)
+
+        os.unlink(shared_file)
+        self.assertIn(filename, logs)
+
+
+class TestStartContainerWithBinds(BaseTestCase):
+    def runTest(self):
+        mount_dest = '/mnt'
+        mount_origin = tempfile.mkdtemp()
+        self.tmp_folders.append(mount_origin)
+
+        filename = 'shared.txt'
+        shared_file = os.path.join(mount_origin, filename)
+        binds = {
+            mount_origin: {
+                'bind': mount_dest,
+                'ro': False,
+            },
+        }
+
+        with open(shared_file, 'w'):
+            container = self.client.create_container(
+                'busybox', ['ls', mount_dest], volumes={mount_dest: {}}
             )
+            container_id = container['Id']
+            self.client.start(container_id, binds=binds)
             self.tmp_containers.append(container_id)
             exitcode = self.client.wait(container_id)
             self.assertEqual(exitcode, 0)
@@ -225,6 +257,31 @@ def runTest(self):
             self.assertEqual(inspect['State']['ExitCode'], 0)
 
 
+class TestCreateContainerPrivileged(BaseTestCase):
+    def runTest(self):
+        res = self.client.create_container(
+            'busybox', 'true', host_config=create_host_config(privileged=True)
+        )
+        self.assertIn('Id', res)
+        self.tmp_containers.append(res['Id'])
+        self.client.start(res['Id'])
+        inspect = self.client.inspect_container(res['Id'])
+        self.assertIn('Config', inspect)
+        self.assertIn('Id', inspect)
+        self.assertTrue(inspect['Id'].startswith(res['Id']))
+        self.assertIn('Image', inspect)
+        self.assertIn('State', inspect)
+        self.assertIn('Running', inspect['State'])
+        if not inspect['State']['Running']:
+            self.assertIn('ExitCode', inspect['State'])
+            self.assertEqual(inspect['State']['ExitCode'], 0)
+        # Since Nov 2013, the Privileged flag is no longer part of the
+        # container's config exposed via the API (safety concerns?).
+        #
+        if 'Privileged' in inspect['Config']:
+            self.assertEqual(inspect['Config']['Privileged'], True)
+
+
 class TestStartContainerPrivileged(BaseTestCase):
     def runTest(self):
         res = self.client.create_container('busybox', 'true')
@@ -457,6 +514,35 @@ def runTest(self):
 
 
 class TestPort(BaseTestCase):
+    def runTest(self):
+
+        port_bindings = {
+            1111: ('127.0.0.1', '4567'),
+            2222: ('127.0.0.1', '4568')
+        }
+
+        container = self.client.create_container(
+            'busybox', ['sleep', '60'], ports=port_bindings.keys(),
+            host_config=create_host_config(port_bindings=port_bindings)
+        )
+        id = container['Id']
+
+        self.client.start(container)
+
+        # Call the port function on each biding and compare expected vs actual
+        for port in port_bindings:
+            actual_bindings = self.client.port(container, port)
+            port_binding = actual_bindings.pop()
+
+            ip, host_port = port_binding['HostIp'], port_binding['HostPort']
+
+            self.assertEqual(ip, port_bindings[port][0])
+            self.assertEqual(host_port, port_bindings[port][1])
+
+        self.client.kill(id)
+
+
+class TestStartWithPortBindings(BaseTestCase):
     def runTest(self):
 
         port_bindings = {
@@ -551,6 +637,88 @@ def runTest(self):
         self.assertEqual(len(res), 0)
 
 
+class TestCreateContainerWithVolumesFrom(BaseTestCase):
+    def runTest(self):
+        vol_names = ['foobar_vol0', 'foobar_vol1']
+
+        res0 = self.client.create_container(
+            'busybox', 'true', name=vol_names[0]
+        )
+        container1_id = res0['Id']
+        self.tmp_containers.append(container1_id)
+        self.client.start(container1_id)
+
+        res1 = self.client.create_container(
+            'busybox', 'true', name=vol_names[1]
+        )
+        container2_id = res1['Id']
+        self.tmp_containers.append(container2_id)
+        self.client.start(container2_id)
+        with self.assertRaises(docker.errors.DockerException):
+            self.client.create_container(
+                'busybox', 'cat', detach=True, stdin_open=True,
+                volumes_from=vol_names
+            )
+        res2 = self.client.create_container(
+            'busybox', 'cat', detach=True, stdin_open=True,
+            host_config=create_host_config(volumes_from=vol_names)
+        )
+        container3_id = res2['Id']
+        self.tmp_containers.append(container3_id)
+        self.client.start(container3_id)
+
+        info = self.client.inspect_container(res2['Id'])
+        self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names)
+
+
+class TestCreateContainerWithLinks(BaseTestCase):
+    def runTest(self):
+        res0 = self.client.create_container(
+            'busybox', 'cat',
+            detach=True, stdin_open=True,
+            environment={'FOO': '1'})
+
+        container1_id = res0['Id']
+        self.tmp_containers.append(container1_id)
+
+        self.client.start(container1_id)
+
+        res1 = self.client.create_container(
+            'busybox', 'cat',
+            detach=True, stdin_open=True,
+            environment={'FOO': '1'})
+
+        container2_id = res1['Id']
+        self.tmp_containers.append(container2_id)
+
+        self.client.start(container2_id)
+
+        # we don't want the first /
+        link_path1 = self.client.inspect_container(container1_id)['Name'][1:]
+        link_alias1 = 'mylink1'
+        link_env_prefix1 = link_alias1.upper()
+
+        link_path2 = self.client.inspect_container(container2_id)['Name'][1:]
+        link_alias2 = 'mylink2'
+        link_env_prefix2 = link_alias2.upper()
+
+        res2 = self.client.create_container(
+            'busybox', 'env', host_config=create_host_config(
+                links={link_path1: link_alias1, link_path2: link_alias2}
+            )
+        )
+        container3_id = res2['Id']
+        self.tmp_containers.append(container3_id)
+        self.client.start(container3_id)
+        self.assertEqual(self.client.wait(container3_id), 0)
+
+        logs = self.client.logs(container3_id)
+        self.assertIn('{0}_NAME='.format(link_env_prefix1), logs)
+        self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs)
+        self.assertIn('{0}_NAME='.format(link_env_prefix2), logs)
+        self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix2), logs)
+
+
 class TestStartContainerWithVolumesFrom(BaseTestCase):
     def runTest(self):
         vol_names = ['foobar_vol0', 'foobar_vol1']
@@ -633,12 +801,13 @@ def runTest(self):
 
 class TestRestartingContainer(BaseTestCase):
     def runTest(self):
-        container = self.client.create_container('busybox', ['false'])
+        container = self.client.create_container(
+            'busybox', ['false'], host_config=create_host_config(
+                restart_policy={"Name": "on-failure", "MaximumRetryCount": 1}
+            )
+        )
         id = container['Id']
-        self.client.start(id, restart_policy={
-            "Name": "on-failure",
-            "MaximumRetryCount": 1
-        })
+        self.client.start(id)
         self.client.wait(id)
         self.client.remove_container(id)
         containers = self.client.containers(all=True)
@@ -726,7 +895,8 @@ class TestRemoveLink(BaseTestCase):
     def runTest(self):
         # Create containers
         container1 = self.client.create_container(
-            'busybox', 'cat', detach=True, stdin_open=True)
+            'busybox', 'cat', detach=True, stdin_open=True
+        )
         container1_id = container1['Id']
         self.tmp_containers.append(container1_id)
         self.client.start(container1_id)
@@ -736,10 +906,14 @@ def runTest(self):
         link_path = self.client.inspect_container(container1_id)['Name'][1:]
         link_alias = 'mylink'
 
-        container2 = self.client.create_container('busybox', 'cat')
+        container2 = self.client.create_container(
+            'busybox', 'cat', host_config=create_host_config(
+                links={link_path: link_alias}
+            )
+        )
         container2_id = container2['Id']
         self.tmp_containers.append(container2_id)
-        self.client.start(container2_id, links={link_path: link_alias})
+        self.client.start(container2_id)
 
         # Remove link
         linked_name = self.client.inspect_container(container2_id)['Name'][1:]
diff --git a/tests/test.py b/tests/test.py
--- a/tests/test.py
+++ b/tests/test.py
@@ -14,6 +14,7 @@
 
 import base64
 import datetime
+import gzip
 import io
 import json
 import os
@@ -23,7 +24,7 @@
 import tarfile
 import tempfile
 import unittest
-import gzip
+import warnings
 
 import docker
 import requests
@@ -31,13 +32,16 @@
 
 import fake_api
 
-
 try:
     from unittest import mock
 except ImportError:
     import mock
 
 
+warnings.simplefilter('error')
+create_host_config = docker.utils.create_host_config
+
+
 def response(status_code=200, content='', headers=None, reason=None, elapsed=0,
              request=None):
     res = requests.Response()
@@ -102,6 +106,17 @@ def setUp(self):
     def tearDown(self):
         self.client.close()
 
+    def base_create_payload(self, img='busybox', cmd=None):
+        if not cmd:
+            cmd = ['true']
+        return {"Tty": False, "Image": img, "Cmd": cmd,
+                "AttachStdin": False, "Memory": 0,
+                "AttachStderr": True, "AttachStdout": True,
+                "StdinOnce": False,
+                "OpenStdin": False, "NetworkDisabled": False,
+                "MemorySwap": 0
+                }
+
     def test_ctor(self):
         try:
             docker.Client(version=1.12)
@@ -109,7 +124,7 @@ def test_ctor(self):
             self.assertTrue(isinstance(e, docker.errors.DockerException))
             if not six.PY3:
                 self.assertEqual(
-                    e.message,
+                    str(e),
                     'version parameter must be a string. Found float'
                 )
 
@@ -570,10 +585,35 @@ def test_start_container(self):
             args[0][0],
             url_prefix + 'containers/3cc2351ab11b/start'
         )
+        self.assertEqual(json.loads(args[1]['data']), {})
         self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False}
+            args[1]['headers'], {'Content-Type': 'application/json'}
+        )
+        self.assertEqual(
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_create_container_with_lxc_conf(self):
+        try:
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    lxc_conf={'lxc.conf.k': 'lxc.conf.value'}
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+        args = fake_request.call_args
+        self.assertEqual(
+            args[0][0],
+            url_prefix + 'containers/create'
         )
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['LxcConf'] = [
+            {"Value": "lxc.conf.value", "Key": "lxc.conf.k"}
+        ]
+
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
         self.assertEqual(
             args[1]['headers'],
             {'Content-Type': 'application/json'}
@@ -583,6 +623,244 @@ def test_start_container(self):
             docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
+    def test_create_container_with_lxc_conf_compat(self):
+        try:
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    lxc_conf=[{'Key': 'lxc.conf.k', 'Value': 'lxc.conf.value'}]
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['LxcConf'] = [
+            {"Value": "lxc.conf.value", "Key": "lxc.conf.k"}
+        ]
+        self.assertEqual(
+            json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(args[1]['headers'],
+                         {'Content-Type': 'application/json'})
+        self.assertEqual(
+            args[1]['timeout'],
+            docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_create_container_with_binds_ro(self):
+        try:
+            mount_dest = '/mnt'
+            mount_origin = '/tmp'
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    binds={mount_origin: {
+                        "bind": mount_dest,
+                        "ro": True
+                    }}
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix +
+                         'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:ro"]
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(args[1]['headers'],
+                         {'Content-Type': 'application/json'})
+        self.assertEqual(
+            args[1]['timeout'],
+            docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_create_container_with_binds_rw(self):
+        try:
+            mount_dest = '/mnt'
+            mount_origin = '/tmp'
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    binds={mount_origin: {
+                        "bind": mount_dest,
+                        "ro": False
+                    }}
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix +
+                         'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:rw"]
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(args[1]['headers'],
+                         {'Content-Type': 'application/json'})
+        self.assertEqual(
+            args[1]['timeout'],
+            docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_create_container_with_port_binds(self):
+        self.maxDiff = None
+        try:
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    port_bindings={
+                        1111: None,
+                        2222: 2222,
+                        '3333/udp': (3333,),
+                        4444: ('127.0.0.1',),
+                        5555: ('127.0.0.1', 5555),
+                        6666: [('127.0.0.1',), ('192.168.0.1',)]
+                    }
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        data = json.loads(args[1]['data'])
+        self.assertEqual(data['HostConfig']['PublishAllPorts'], False)
+        port_bindings = data['HostConfig']['PortBindings']
+        self.assertTrue('1111/tcp' in port_bindings)
+        self.assertTrue('2222/tcp' in port_bindings)
+        self.assertTrue('3333/udp' in port_bindings)
+        self.assertTrue('4444/tcp' in port_bindings)
+        self.assertTrue('5555/tcp' in port_bindings)
+        self.assertTrue('6666/tcp' in port_bindings)
+        self.assertEqual(
+            [{"HostPort": "", "HostIp": ""}],
+            port_bindings['1111/tcp']
+        )
+        self.assertEqual(
+            [{"HostPort": "2222", "HostIp": ""}],
+            port_bindings['2222/tcp']
+        )
+        self.assertEqual(
+            [{"HostPort": "3333", "HostIp": ""}],
+            port_bindings['3333/udp']
+        )
+        self.assertEqual(
+            [{"HostPort": "", "HostIp": "127.0.0.1"}],
+            port_bindings['4444/tcp']
+        )
+        self.assertEqual(
+            [{"HostPort": "5555", "HostIp": "127.0.0.1"}],
+            port_bindings['5555/tcp']
+        )
+        self.assertEqual(len(port_bindings['6666/tcp']), 2)
+        self.assertEqual(args[1]['headers'],
+                         {'Content-Type': 'application/json'})
+        self.assertEqual(
+            args[1]['timeout'],
+            docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
+    def test_create_container_with_links(self):
+        try:
+            link_path = 'path'
+            alias = 'alias'
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    links={link_path: alias}
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(
+            args[0][0], url_prefix + 'containers/create'
+        )
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Links'] = ['path:alias']
+
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(
+            args[1]['headers'], {'Content-Type': 'application/json'}
+        )
+
+    def test_create_container_with_multiple_links(self):
+        try:
+            link_path = 'path'
+            alias = 'alias'
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    links={
+                        link_path + '1': alias + '1',
+                        link_path + '2': alias + '2'
+                    }
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Links'] = [
+            'path1:alias1', 'path2:alias2'
+        ]
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(
+            args[1]['headers'], {'Content-Type': 'application/json'}
+        )
+
+    def test_create_container_with_links_as_list_of_tuples(self):
+        try:
+            link_path = 'path'
+            alias = 'alias'
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    links=[(link_path, alias)]
+                )
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Links'] = ['path:alias']
+
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(
+            args[1]['headers'], {'Content-Type': 'application/json'}
+        )
+
+    def test_create_container_privileged(self):
+        try:
+            self.client.create_container(
+                'busybox', 'true',
+                host_config=create_host_config(privileged=True)
+            )
+        except Exception as e:
+            self.fail('Command should not raise exception: {0}'.format(e))
+
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Privileged'] = True
+        args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+        self.assertEqual(args[1]['headers'],
+                         {'Content-Type': 'application/json'})
+        self.assertEqual(
+            args[1]['timeout'],
+            docker.client.DEFAULT_TIMEOUT_SECONDS
+        )
+
     def test_start_container_with_lxc_conf(self):
         try:
             self.client.start(
@@ -598,8 +876,7 @@ def test_start_container_with_lxc_conf(self):
         )
         self.assertEqual(
             json.loads(args[1]['data']),
-            {"LxcConf": [{"Value": "lxc.conf.value", "Key": "lxc.conf.k"}],
-             "PublishAllPorts": False, "Privileged": False}
+            {"LxcConf": [{"Value": "lxc.conf.value", "Key": "lxc.conf.k"}]}
         )
         self.assertEqual(
             args[1]['headers'],
@@ -624,11 +901,7 @@ def test_start_container_with_lxc_conf_compat(self):
                          'containers/3cc2351ab11b/start')
         self.assertEqual(
             json.loads(args[1]['data']),
-            {
-                "LxcConf": [{"Key": "lxc.conf.k", "Value": "lxc.conf.value"}],
-                "PublishAllPorts": False,
-                "Privileged": False,
-            }
+            {"LxcConf": [{"Key": "lxc.conf.k", "Value": "lxc.conf.value"}]}
         )
         self.assertEqual(args[1]['headers'],
                          {'Content-Type': 'application/json'})
@@ -652,10 +925,9 @@ def test_start_container_with_binds_ro(self):
         args = fake_request.call_args
         self.assertEqual(args[0][0], url_prefix +
                          'containers/3cc2351ab11b/start')
-        self.assertEqual(json.loads(args[1]['data']),
-                         {"Binds": ["/tmp:/mnt:ro"],
-                          "PublishAllPorts": False,
-                          "Privileged": False})
+        self.assertEqual(
+            json.loads(args[1]['data']), {"Binds": ["/tmp:/mnt:ro"]}
+        )
         self.assertEqual(args[1]['headers'],
                          {'Content-Type': 'application/json'})
         self.assertEqual(
@@ -675,10 +947,9 @@ def test_start_container_with_binds_rw(self):
         args = fake_request.call_args
         self.assertEqual(args[0][0], url_prefix +
                          'containers/3cc2351ab11b/start')
-        self.assertEqual(json.loads(args[1]['data']),
-                         {"Binds": ["/tmp:/mnt:rw"],
-                          "PublishAllPorts": False,
-                          "Privileged": False})
+        self.assertEqual(
+            json.loads(args[1]['data']), {"Binds": ["/tmp:/mnt:rw"]}
+        )
         self.assertEqual(args[1]['headers'],
                          {'Content-Type': 'application/json'})
         self.assertEqual(
@@ -704,7 +975,6 @@ def test_start_container_with_port_binds(self):
         self.assertEqual(args[0][0], url_prefix +
                          'containers/3cc2351ab11b/start')
         data = json.loads(args[1]['data'])
-        self.assertEqual(data['PublishAllPorts'], False)
         self.assertTrue('1111/tcp' in data['PortBindings'])
         self.assertTrue('2222/tcp' in data['PortBindings'])
         self.assertTrue('3333/udp' in data['PortBindings'])
@@ -755,9 +1025,7 @@ def test_start_container_with_links(self):
             url_prefix + 'containers/3cc2351ab11b/start'
         )
         self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "Links": ["path:alias"]}
+            json.loads(args[1]['data']), {"Links": ["path:alias"]}
         )
         self.assertEqual(
             args[1]['headers'],
@@ -785,11 +1053,7 @@ def test_start_container_with_multiple_links(self):
         )
         self.assertEqual(
             json.loads(args[1]['data']),
-            {
-                "PublishAllPorts": False,
-                "Privileged": False,
-                "Links": ["path1:alias1", "path2:alias2"]
-            }
+            {"Links": ["path1:alias1", "path2:alias2"]}
         )
         self.assertEqual(
             args[1]['headers'],
@@ -812,13 +1076,10 @@ def test_start_container_with_links_as_list_of_tuples(self):
             url_prefix + 'containers/3cc2351ab11b/start'
         )
         self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "Links": ["path:alias"]}
+            json.loads(args[1]['data']), {"Links": ["path:alias"]}
         )
         self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
 
     def test_start_container_privileged(self):
@@ -832,8 +1093,7 @@ def test_start_container_privileged(self):
             args[0][0],
             url_prefix + 'containers/3cc2351ab11b/start'
         )
-        self.assertEqual(json.loads(args[1]['data']),
-                         {"PublishAllPorts": False, "Privileged": True})
+        self.assertEqual(json.loads(args[1]['data']), {"Privileged": True})
         self.assertEqual(args[1]['headers'],
                          {'Content-Type': 'application/json'})
         self.assertEqual(
@@ -851,130 +1111,117 @@ def test_start_container_with_dict_instead_of_id(self):
             args[0][0],
             url_prefix + 'containers/3cc2351ab11b/start'
         )
+        self.assertEqual(json.loads(args[1]['data']), {})
         self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False}
-        )
-        self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
         self.assertEqual(
-            args[1]['timeout'],
-            docker.client.DEFAULT_TIMEOUT_SECONDS
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
-    def test_start_container_with_restart_policy(self):
+    def test_create_container_with_restart_policy(self):
         try:
-            self.client.start(fake_api.FAKE_CONTAINER_ID,
-                              restart_policy={
-                                  "Name": "always",
-                                  "MaximumRetryCount": 0
-                              })
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    restart_policy={
+                        "Name": "always",
+                        "MaximumRetryCount": 0
+                    }
+                )
+            )
         except Exception as e:
             self.fail('Command should not raise exception: {0}'.format(e))
         args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['RestartPolicy'] = {
+            "MaximumRetryCount": 0, "Name": "always"
+        }
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
+
         self.assertEqual(
-            args[0][0],
-            url_prefix + 'containers/3cc2351ab11b/start'
-        )
-        self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "RestartPolicy": {"MaximumRetryCount": 0, "Name": "always"}}
-        )
-        self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
         self.assertEqual(
-            args[1]['timeout'],
-            docker.client.DEFAULT_TIMEOUT_SECONDS
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
-    def test_start_container_with_added_capabilities(self):
+    def test_create_container_with_added_capabilities(self):
         try:
-            self.client.start(fake_api.FAKE_CONTAINER_ID,
-                              cap_add=['MKNOD'])
+            self.client.create_container(
+                'busybox', 'true',
+                host_config=create_host_config(cap_add=['MKNOD'])
+            )
         except Exception as e:
             self.fail('Command should not raise exception: {0}'.format(e))
         args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['CapAdd'] = ['MKNOD']
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
         self.assertEqual(
-            args[0][0],
-            url_prefix + 'containers/3cc2351ab11b/start'
-        )
-        self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "CapAdd": ["MKNOD"]}
-        )
-        self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
         self.assertEqual(
-            args[1]['timeout'],
-            docker.client.DEFAULT_TIMEOUT_SECONDS
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
-    def test_start_container_with_dropped_capabilities(self):
+    def test_create_container_with_dropped_capabilities(self):
         try:
-            self.client.start(fake_api.FAKE_CONTAINER_ID,
-                              cap_drop=['MKNOD'])
+            self.client.create_container(
+                'busybox', 'true',
+                host_config=create_host_config(cap_drop=['MKNOD'])
+            )
         except Exception as e:
             self.fail('Command should not raise exception: {0}'.format(e))
         args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['CapDrop'] = ['MKNOD']
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
         self.assertEqual(
-            args[0][0],
-            url_prefix + 'containers/3cc2351ab11b/start'
-        )
-        self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "CapDrop": ["MKNOD"]}
-        )
-        self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
         self.assertEqual(
-            args[1]['timeout'],
-            docker.client.DEFAULT_TIMEOUT_SECONDS
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
-    def test_start_container_with_devices(self):
+    def test_create_container_with_devices(self):
         try:
-            self.client.start(fake_api.FAKE_CONTAINER_ID,
-                              devices=['/dev/sda:/dev/xvda:rwm',
-                                       '/dev/sdb:/dev/xvdb',
-                                       '/dev/sdc'])
+            self.client.create_container(
+                'busybox', 'true', host_config=create_host_config(
+                    devices=['/dev/sda:/dev/xvda:rwm',
+                             '/dev/sdb:/dev/xvdb',
+                             '/dev/sdc']
+                )
+            )
         except Exception as e:
             self.fail('Command should not raise exception: {0}'.format(e))
         args = fake_request.call_args
+        self.assertEqual(args[0][0], url_prefix + 'containers/create')
+        expected_payload = self.base_create_payload()
+        expected_payload['HostConfig'] = create_host_config()
+        expected_payload['HostConfig']['Devices'] = [
+            {'CgroupPermissions': 'rwm',
+             'PathInContainer': '/dev/xvda',
+             'PathOnHost': '/dev/sda'},
+            {'CgroupPermissions': 'rwm',
+             'PathInContainer': '/dev/xvdb',
+             'PathOnHost': '/dev/sdb'},
+            {'CgroupPermissions': 'rwm',
+             'PathInContainer': '/dev/sdc',
+             'PathOnHost': '/dev/sdc'}
+        ]
+        self.assertEqual(json.loads(args[1]['data']), expected_payload)
         self.assertEqual(
-            args[0][0],
-            url_prefix + 'containers/3cc2351ab11b/start'
-        )
-        self.assertEqual(
-            json.loads(args[1]['data']),
-            {"PublishAllPorts": False, "Privileged": False,
-             "Devices": [{'CgroupPermissions': 'rwm',
-                          'PathInContainer': '/dev/xvda',
-                          'PathOnHost': '/dev/sda'},
-                         {'CgroupPermissions': 'rwm',
-                          'PathInContainer': '/dev/xvdb',
-                          'PathOnHost': '/dev/sdb'},
-                         {'CgroupPermissions': 'rwm',
-                          'PathInContainer': '/dev/sdc',
-                          'PathOnHost': '/dev/sdc'}]}
-        )
-        self.assertEqual(
-            args[1]['headers'],
-            {'Content-Type': 'application/json'}
+            args[1]['headers'], {'Content-Type': 'application/json'}
         )
         self.assertEqual(
-            args[1]['timeout'],
-            docker.client.DEFAULT_TIMEOUT_SECONDS
+            args[1]['timeout'], docker.client.DEFAULT_TIMEOUT_SECONDS
         )
 
     def test_resize_container(self):
diff --git a/tests/utils_test.py b/tests/utils_test.py
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -71,16 +71,16 @@ def test_kwargs_from_env(self):
                               'testdata/certs'),
                           DOCKER_TLS_VERIFY='1')
         kwargs = kwargs_from_env(assert_hostname=False)
-        self.assertEquals('https://192.168.59.103:2376', kwargs['base_url'])
+        self.assertEqual('https://192.168.59.103:2376', kwargs['base_url'])
         self.assertTrue('ca.pem' in kwargs['tls'].verify)
         self.assertTrue('cert.pem' in kwargs['tls'].cert[0])
         self.assertTrue('key.pem' in kwargs['tls'].cert[1])
-        self.assertEquals(False, kwargs['tls'].assert_hostname)
+        self.assertEqual(False, kwargs['tls'].assert_hostname)
         try:
             client = Client(**kwargs)
-            self.assertEquals(kwargs['base_url'], client.base_url)
-            self.assertEquals(kwargs['tls'].verify, client.verify)
-            self.assertEquals(kwargs['tls'].cert, client.cert)
+            self.assertEqual(kwargs['base_url'], client.base_url)
+            self.assertEqual(kwargs['tls'].verify, client.verify)
+            self.assertEqual(kwargs['tls'].cert, client.cert)
         except TypeError as e:
             self.fail(e)