To go over it quickly, docker-py uses the requests library to send commands directly to the docker host socket. All that’s needed is to establish a connection and call the methods that tie into whatever command you’re trying to execute.

However, if the application is intended as a generic solution that should function across different environments, deciding at the on how to connect to the socket at the application level can get tricky. This is mainly because exposing the underlying unix socket is a fairly big security concern — there are plenty of articles out there explaining it in detail — so I chose to go with a utility function that passes the connection arguments to the client object based on your environment variables.

from docker import Client

from docker.utils import kwargs_from_env client = Client(**kwargs_from_env())

This mechanism leaves connection configuration to the user of your application, making it operate with whatever setup is in place for the regular command line.

The docker-py client object exposes a method equivalent to almost every function available at through the shell. Anything from downloading images, to building them, to starting containers and managing their state.

So what did the Python say to the Whale?

While a state-less API has its advantages, we really needed something that could represent images and containers as objects. In principle, this seemed as simple as tracking a string with image:tag-like references, but there were several cases where that label would encompass several running containers, and some containers weren’t named, so identifiers needed to be used directly. To help with this, I created DockerImage and DockerContainer classes that wrap the commonly used methods.

DockerImage(repository, tag) is all you need to download or build an image, as well as check whether it exists in your docker host. It also provides a method to add or reassign tags.

The base image APIs provided by docker-py were fairly simple to interact with. They use an iterator interface when performing a build or download, something that makes perfect sense as these actions are essentially streams of data or build steps which you may want for debug purposes. However, I decided to dump them to logging.debug until they complete, therefore masking the wall of text that you usually don’t care about if using a proven image.

DockerContainer(image, name) creates an object that will allow you to inspect the container it represents, and also provides a way to run it (which creates the container if it doesn’t exist) and functions that start, stop, remove and restart a running container.

Given that containers may or may not be named, interfacing with them — while not complicated — can be hard to follow in code because most of the docker-py methods require the container id, not the name. This was abstracted out, along with container status checks, in places where it made sense.

Once I reached the point of adding an attach method — which subprocesses out the command so that getting into your container is like starting a new shell — I realized it could do something fairly neat: replace python virtual environments with containers. And so capsule was born.