It is probably fair to say that access to servers has never been easier. With platforms such as AWS, Azure, and Google GCE we can now launch on-demand servers of all varieties and configurations. This programable infrastructure (IaaS) help companies, agencies, and institutions maintain agility as market and mission pressures evolve. However, even with the rise of IaaS, application packaging, configuration, and composition has not advanced despite considerable efforts in configuration management. This is where docker
comes in.
Docker
is not about full virtualization but rather about the ease of packaging and running applications using Linux containers. The idea is that docker
containers wrap a piece of software or application in a complete filesystem that contains everything needed to run: code, runtime, system tools, system libraries (i.e. anything that can be installed on a server). This guarantees that the software will always run the same everywhere, regardless of the OS/compute environment running the container. Docker also provides portable Linux deployment such that containers can be run on any Linux system with kernel is 3.10 or later. All major Linux distros have supported Docker since 2014. While no doubt containers and virtual machines have similar resource isolation and allocation benefits, the architectural approach of Linux containers allows containerized applications to be more portable and efficient.
At NVIDIA, we use containers in a variety of ways including development, testing, benchmarking, and of course in production as the mechanism for deploying deep learning frameworks. Using nvidia-docker
, a light-weight docker
plugin, we can develop and prototype GPU applications on a workstation, and then deploy those applications anywhere that supports GPU containers.
In [17]:
docker --version
We can ask nvidia-docker
for the version information too
In [18]:
nvidia-docker --version
Notice that nvidia-docker
invocation here was simply "pass through" to docker
command itself.
Next best way to get familiar with docker command line is to ask for --help
In [4]:
docker --help
The format of docker
command line interactions is:
docker [OPTIONS] COMMAND [arg...]
and as the help display shows there are a lot of commands to choose from. Don't worry, much like a big city, once we become more familiar with these commands the list won't seem so big. We can start to drill down and get help that is specific to each command. For example, one of the most useful docker
commands is images
which list all local containers on the host that docker
knows about
In [19]:
docker images --help
OK, lets now ask docker
about the what container images are available locally on the host
In [20]:
docker images
Here the output specifies three container images with some general metadata associated with each one. First you'll notice that the images are quite large on average (~ 2GB) and that each image is associated with a unique ID hash. When containers are created (i.e. via the create
command) they are created from images. There is no limit to the number of containers we can create from an images so it is important that docker
associates UUIDs for each image and container. Notice the REPOSITORY
and TAG
columns here specify more human readable image labels. The repository loosely coresponds to the image name (i.e. url) and just as in the version control system GIT images can be modified and "tagged" rather than explicitly changing the image name for each image version.
Here we have the "nvidia/cuda
" container with ID c54a2cc56cbb
and is tagged as the "latest
" version of the image (i.e. most current). The deep learning library cuDNN
was added to the image and a new image was created under the same name but tagged appropriately as "8.0-cudnn5-devel
".
You're probably wondering already "where does docker store these containers?". In general, docker works in /var/lib/docker
and images are stored in image
subdirectory. For more information and details about where and how docker stores images on the host machine, see here.
For now just know that docker works with "images" and all containers are created from these images. We will go into all the details about creating and modifying images etc in just a bit. But first, lets actually kick around some containers!
First things first lets have docker list all containers using the ps
command
In [21]:
docker ps -a
There are probably no containers listed which is fine because we're going to create some containers from images next. Again, don't forget you can get help for each command with docker [COMMAND] --help
. Use this to get additional details on the ps
command.
Lets now use the docker command create
to initialize a container from the nvidia/cuda:latest
image
In [22]:
docker create nvidia/cuda:latest
The responce we recieved is a sha256 UUID for the generated container and listing docker
containers again we see this new container now listed
In [23]:
docker ps -a
It is important to understand that the container is not actually doing anything right now. We've only "stamped" out a container from an image -- the container is not running at this point. Were the container active the STATUS
would read "running". OK, so what is the container doing there? Well the answer is "nothing". Think about when we enter commands on the command-line -- each time we hit enter we implicitly specify that we would like that command to be executed immediately. You can think of containers as a command that has not yet executed. This command is wrapped up in the container and has all the resources (libraries etc) needed for successful execution. Speaking of which, lets actually run this container ...
Using the 12 character container id provided by the docker ps -a
command above we can run the container as follows
In [24]:
# copy your CONTAINER ID from the docker ps -a command above
nvidia-docker run 41524c54cf1c
hm ... we got an ERROR. Using the run
command seemed like a good guess! Why does the run
command not work here? More on this later in the next few cells.
Lets try the start
command instead
In [25]:
nvidia-docker start 41524c54cf1c
OK, that looks better. Using the start
command docker returned the sha256 UUID. Lets have a look at the docker containers again
In [26]:
docker ps -a
Now the status says "Exited (0) ...". Notice that the command (i.e. entry point) is /bin/bash
. When the start
was issued the "COMMAND" was executed and by definition bash command language interpreter that executes commands read from the standard input or from a file. However, there were no commands to execute from standard input! Containers can have other entry points -- the reason /bin/bash
is used most often is that it allows the container to act more generically as a shell so we can send it additional instructions. Note that all containers have a default entrypoint of "/bin/sh -c
" unless otherwise specified.
Here our hands are tied with what the container will do. Each time we issue the start
command the container executes the entrypoint "/bin/bash
" and since there is nothing on standard input the container simply exits. This is where the run
command comes in.
Instead of creating and starting a container explicitly we can use the run
command to exectue a command within a particular image via creating a container from that image with the appropriate entrypoint. Lets issue a run
command passing the image ID of the "nvidia/cuda:latest
" image as the argument.
In [27]:
# don't forget to use the container id from the "docker ps -a" command
nvidia-docker run 367795fb1051
Notice that the command start
takes a container ID as the argument while the run
command takes an image ID. Lets have a look at the containers
In [28]:
docker ps -a
Now we have an additional container both from image 367795fb1051
which have exited.
At this point the run
command has done exactly what the start
command has done (i.e. started a container which executed the entrypoint and exited). However, the docker run
command allows us to pass an alternative command to the container (docker run --help
). Lets try to pass an alternative instruction.
In [29]:
nvidia-docker run 367795fb1051 nvidia-smi
Finally! Just to be clear, the nvidia-smi
command was exectued within the container -- not the host. Lets have a look at the containers yet again
In [30]:
docker ps -a
We now have a new container from image 367795fb1051
but the "COMMAND" has been set to nvidia-smi
as instructed by our run
command. Just for kicks lets issue a start
command to this new container. Each container gets a uuid that will change every time this lab is run so make sure to replace the container ID in the command below with the appropriate container ID listed above.
In [31]:
docker start 9cedbf73f11d
Now wait a minute, where is our output??
In [32]:
docker ps -a
Sure enough when we check the docker container status it has status of "Exited (0) 10 seconds ago ..." which means that the start
command did indeed start the container. The long story short is that the run
command automatically provides the standard output from the command specified where as start
does not forward the stdout by default -- we have to explicitly ask. According to the help section for the start
command, the option "--attach
" attaches STDOUT/STDERR and forward signals.
In [33]:
docker start --attach 9cedbf73f11d
Bingo! We got the output form the command using --attach
option when using the start
command. We can get the associated help for the attach option to see that indeed we get STDOUT/STDERR
In [60]:
docker start --help
It is reasonable to ask where STDOUT goes when not attached? The answer is that STDOUT/STDERR are piped to the container log file. Each container has a log file associated with it which can be accessed using the logs
command
In [38]:
docker logs 9cedbf73f11d
A few final words on starting and running containers. Keep an eye out on the container list when using the run
command as each invocation creates a new image. There is no problem having many (many) container stitting around but eventually it creates clutter. Remember, containers are ment to be light-weight and disposable. To that end lets clean up our containers.
In [39]:
# generate a list of container ID from the docker ps command
docker ps -a | awk '{print $1}' | tail -n +2
In [40]:
# for each container ID use the docker "rm" command to remove/delete the container
for cid in $(docker ps -a | awk '{print $1}' | tail -n +2);do docker rm $cid; done
All cleaned up!
In [42]:
docker ps -a
So, the run
command creates a new container each time and using the docker ps
command we can see each new container as we run commands. However this can get combersom to have to manually clean out containers all the time. The solution to this is to use the --rm
option with the run
command. This instructs docker to simply remove the container after execution. This is quite convenient for most situations. Keep in mind however, that once the container has been deleted it can not be started again etc -- it's gone. In general this is the desired workflow since containers are intended to be light-weight disposible execution units. After all, if you need the container again, no problem, just create another one!
So far we have discussed what docker is (i.e. virtual machines v.s. Linux containers) and how to view (ps
), create
, start
, run
, and rm
containers created from docker
images. Furthermore we've investigated various options associated with these docker
commands such as --attach
and --rm
and familiarized ourselves with how to obtain help for docker
and each of the docker
commands.
Make sure that you're comfortable creating containers and executing commands before moving forward. Docker is quite forgiving so do be afraid to try lots of different things out while you explore. Here are a few suggestions:
run
command with the option --rm
and confirm the continer is cleaned up --name
option with the run
commandwhoami
. Think about what user might get returned before you run this.ifconfig
inside of the container. Is the MAC address the same every time?-c1
so you don't ping forever) df -h
)??--env
option with the run
command to set environment variables AWS_S3_BUCKET, AWS_ACCESS_KEY, and AWS_SECRET_KEYWhat happens when you execute rm -rf /
inside a container?
By now you probably comfortable with launching containers with docker
from the images that were already available when we started. The next step is understanding how to manage your own images. This includes things like importing images into docker
, modifying existing images, exporting images, and of course deleting images.
In the docker
world most images have a "parent". This means that the image was created by modifying some existing image. In general, this is the typical workflow in docker
. The idea is that it is easy to create images and lets just reuse what's already existing so as to be most efficient.
However, there is an essential difference when working with docker
images. In the virtual machine world, you modify a 4 GB machine image and then do "save as" and create a new 4 GB machine image that contains your changes/updates. In this way virtual machine images are totally independent but very heavy weight.
When we make modification to an existing docker
image and use this to create our own "new" image, docker
does not store two images. Just a the GIT version control does not make a new copy of a file everytime a modification is commited, so too docker
works with images in "layers" so that changes or modifications to an image are stored as a new light-weight image deltas called "layers". In this way we can take an existing 2 GB docker
image and create 10 new images each with a few modifications of this base image without having to store 20 GB of new images! That is, in creating a new docker image from a parent, we only have to store the changes to the parent image.
As you might imagine, only having to store deltas to images allows for many many (many!) images to be generated without having to pay the full cost of having all those images around. Therefore in the docker
world, images abound since it is efficient and light-weight to generate new images from an existing parent image. Luckily, docker
provides ways to manage all these images using "image repositories" so that we can manage images just like we would version controled files in a git
repository. More on docker
repositories later.
Lets first update the existing nvidia/cuda
image by creating a container that executes apt-get update
.
In [43]:
docker images
In [44]:
nvidia-docker run 367795fb1051 apt-get update
Ok, since we did not use the --rm
option our container is still available -- which is what we want since we're going to create a new image from this updated container. Notice that we can not save changes to a container that has been removed/deleted. This should be obvious but just saying ... :)
In [45]:
docker ps -a
Docker
lets us keep these changes by committing them into a new image. Under the hood, docker
keeps track of the differenced between the base image (nvidia/cuda
or rather 367795fb1051
) by creating a new image layer using the union filesystem (UnionFS). To see this, we can inspect the changes to the container using the docker
diff
command which takes a the container ID as an argument.
In [48]:
# make sure to use container id from "docker ps -a" command above
docker diff 938e7acea158
where A
means that the file or directory listed was added, C
means created and D
means deleted
Lets now use the docker
commit
command to generate a new image from this container. You might want to check your disk usage before and after image creation just to verify for yourself that the new image does not eat up an additional 2 GB of disk space on the host.
In [68]:
df -h
In [124]:
# make sure to use the appropriate container ID here
docker commit <CONTAINER-ID> newiamgename:update
Here using the docker
commit
command we provided the unique container ID as provided by the ps
command and a new name:tag for the resulting image. Now lets list the docker
images and we should see our new image there.
In [125]:
docker images
Again, notice that the image size says something like 1.6 GB. Verify with df
that we have not actually used additional physical space on host disk in generating this image
In [73]:
df -h
GOTCHA: docker
does not allow upper case characters in the image names and doing so generates the error message: "invalid reference format"
There are two docker
commands for creating a tar
file that can be shared with others. The first is that we can use the docker
commands save
and load
to create and ingest image tar files. The second option is to use the docker
commands export
and import
to create and ingest container tar files
Notice the help
definitions for each set of commands:
When working with Containers | |
---|---|
export |
Export a container's filesystem as a tar archive |
import |
Import the contents from a tarball to create a filesystem image |
When working with Images | |
save |
Save one or more images to a tar archive (streamed to STDOUT by default) |
load |
Load an image from a tar archive or STDIN |
Lets save the new image created in the previous section as a tar-ball on the file system.
In [126]:
docker save -o dockerimageexport.tar newiamgename:update
Now if we look in our current working directory we should see a nice fat tar-ball of our docker
image
In [102]:
ls -lah dockerimageexport.tar
Keep in mind that when an image is saved to the host file system the full size of the image is physically allocated. We can see this here as the file dockerimageexport.tar
has size 1.6G.
Lets now remove our new image we just commited from docker
using the rmi
command
In [127]:
docker rmi 5980494cc212
If we look at our docker
images again we no longer see newimagename:update
In [128]:
docker images
Finally, load the saved image into docker using the load
command
In [129]:
docker load --input dockerimageexport.tar
In [131]:
docker images
You should see the image newimagename:update
listed!
A few final words on saving vs exporting. While the two methods are indeed similar in functionality, the difference is that saving an image will keep its history (i.e. all parent layers, tags, and versions) while exporting a container will squash its history producing a flattened single layer resource.
Launching containers, making updates, and commiting the changes does work well however, it is quite manual. To automate this image construction workflow docker
provides a manifesto, called a dockerfile
, which is a text file that lists the build steps. Dockerfiles are quite popular in the docker
community and often docker files are exchanged rather than image tar-balls. While simple, there are a few common pitfalls when createing dockerfiles. Read up on the dockerfile best practices for some excellent pointers that will save you lots of time.
Dockerfiles are quite simple lets create a dockerfile to build and updated version of the nvidia/cuda:latest
image
In [82]:
cat << LINES > Dockerfile
FROM nvidia/cuda:latest
RUN apt-get update
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["nvidia-smi"]
LINES
The FROM
instruction sets the base image for subsequent instructions. As such, a valid dockerfile
must have FROM
as its first instruction. The image can be any valid image. The RUN
instruction will execute any commands in a new layer on top of the current image and commit the results. The resulting committed image will be used for the next step in the dockerfile
. Finally, the main purpose of a CMD
is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT
instruction as well. For more information on how CMD
and ENTRYPOINT
interact see here.
To build an image using this dockerfile
we invoke the docker
build
command (this takes about 60 seconds)
In [117]:
docker build -t foo:bar .
In [118]:
docker images
You should now see the new image built with the dockerfile. Notice that we used the -t
option when building the Dockerfile so that we could provide a REPOSITORY
and TAG
. Without the -t
option both repository and tag would be set to "<none>
". This is not necessarily a problem, it just means that you will have have no other option but to reference the image using the IMAGE ID
. Don't hesitate to use the rmi
command to clean up.
Dockerfiles are quite powerful and have many additional commands for adding mount points, exposing ports, setting environmental variables etc. Be sure to read the docs for complete details. For an advanced example of how to add Jupyter notebooks to an image see Appendix B below.
It is a Dockerfile Best Practice to use a .dockerignore
file when building images. Using a .dockerignore
file you can prevent files an directories from being copied to the images during the build to ensure the images contains only essential files.
OK, notice that our image created with the dockerfile
has a non-descript name foo:bar
. We can use the tag
command to rename an image
In [119]:
docker tag foo bettername
hm ... we got an ERROR. We have to use the full name with TAG
(i.e. foo:bar
) when renaming images. Alternatively, we could use the "IMAGE ID
" instead.
In [120]:
docker tag foo:bar bettername
In [121]:
docker images
It is important to notice here that we now have an extra images listed! However, you can confirm with the df -h
command that the rename/copy we did here didn't actually use any additional hard disk space. Also, notice that docker automatically gave the image a TAG
of "latest". We can of course provide a tag explicitly when renaming the image as follows
In [122]:
docker tag foo:bar bettername:tagtagtag
In [123]:
docker images
Typically for personal/local use, image names don't matter too much. However once we start to share and distribute images, there is an image naming convention that must be followed with docker. Use the rmi
command to clean up.
In [126]:
docker rmi foo:bar;
docker rmi bettername:latest;
docker rmi bettername:tagtagtag;
FYI: There is a possibility when deleting images that you could get the ERROR message
Error response from daemon: conflict: unable to delete <IMAGE ID> (must be forced) - image is being used by stopped container <CONTAINER ID>
This means that there are some existing containers using the image you are trying to delete. You must first remove those containers and then remove the associated image.
In [127]:
docker images
In this section we learned how create new images from existing containers using the docker
command commit
. Furthermore, using the export
/import
commands with containers, or alternatively save
/load
commands with images, we can move containers and images in and out of docker
for sharing and backup etc. To facilitate consisten build process we can use a Dockerfile
script with the build
command to generate more complex images requiring a more complex configuration. Finally, we saw how we could use the tag
command to rename/copy our images and the rmi
command to delete images.
At this point you should be comfortable launching containers, looking a logs, attaching standard output, creating your own images with docker
commands and Dockerfiles
, deleting images, renaming images etc etc. Here are a few suggestions for to investigate further and grow your docker
knowledge base:
Dockerfile
command ENV
to create an images with predefined environmental variablesdocker
command inspect
to have a look at what enviroment variables are defined in an image AWS_ACCESS_KEY
, AWS_SECRET_KEY
, and AWS_S3_BUCKET
when building your docker
image.docker
image with others??ADD
and COPY
commands in a Dockerfile
?ENTRYPOINT
and CMD
commands in a Dockerfile
?entrypoint
for a container?VOLUME
command do and when should it be used?WORKDIR
command in a Dockerfile
?According to the Dockerfile Best Practices, which set of commands is better and why??
RUN apt-get install -y automake
RUN apt-get install -y build-essential
OR
RUN apt-get install -y automake build-essential
Is it possible to automate container build using a git
repository hook?
Docker by nature is a social container famework. That is, the docker community likes to share containers. The docker
repository is the primary mechanism for pushing and pulling images. Furthermore, many developers have come to actually distribute software as a ready made container using public docker
repositories. In this way, users simply pull the docker
container and launch the software with zero configuration hassel. For example, many deep learning frameworks are rather difficult to configure and install locally. Therefore, most deep learning frameworks are also published as docker
images which can be pulled from DockerHub. Additonally, many deep learning frameworks have conflicting library dependencies which prevents having those frameworks installed locally at the same time. Pulling ready-made DL framework containers from DockerHub aliviates much of the drudgery of framework lifecycle.
The default docker
repository is index.docker.io
which points to what the community calls "DockerHub". DockerHub is the cental public repository for any docker
images. There are no credentials required to pull
images from DockerHub. On the other hand, to push
images to this public repository does require creating an account to obtain a DockerHub ID with which all your published images will be associated.
In [1]:
docker images
When other try to pull
your publicly avaliable image they will reference the image by the username/image:tag
convention. For example, the full public identifier of the cuda
container we have been working with here is
nvidia/cuda:latest
where "nvidia
" is the official NVIDIA user ID on DockerHub, "cuda
" is the name of the image published by user "nvidia
" and finally the tag "latest
" is used to ask for the most recent version of that image. As usual, even when pulling images from DockerHub, if no tag
is specified by the user, the "latest
" tag will be automatically infered. The generic naming convention is then
[USER]/IMAGE[:TAG]
See here fore additional information on getting started with DockerHub.
In [2]:
# DELETE ALL EXISTING CONTAINERS
# for each container ID use the docker "rm" command to remove/delete the container
for cid in $(docker ps -a | awk '{print $1}' | tail -n +2);do docker rm $cid; done
In [3]:
# DELETE ALL EXISTING IMAGES
# for each image ID, use the docker "rmi" command to remove/delete the image
for iid in $(docker images| awk '{print $3}' | tail -n +2);do docker rmi $iid; done
In [4]:
# confirm no images
docker images
Now lets use the docker
command pull
to get the latest cuda
image from NVIDIA on DockerHub (this takes about a minute)
In [9]:
docker pull nvidia/cuda:latest
In [10]:
docker images
Congratulations! You have now pulled your frist image from DockerHub. Of course, you know how to run a command in this new container
In [12]:
nvidia-docker run --rm nvidia/cuda:latest nvidia-smi
FYI: Notice that if you do not use the docker
pull
command and issue a run
command, for better or worse, docker
will look for the image specified locally and if not found, try to automatically pull the image for you from DockerHub. Lets remove the image we just pulled and issue a run
command when the container is not available locally.
In [42]:
docker rmi nvidia/cuda:latest
In [43]:
nvidia-docker run --rm nvidia/cuda:latest nvidia-smi
However, be very carful here since docker
run
command does not pull the image every time. That is, the docker
run command will only pull the image once (i.e. the first time) if the image is not available locally. Therefore, every time after, the docker
run
command will use the local image. This means that the "latest" image could be updated remotely on DockerHub but your local copy will not change! Therefore, you local image becomes stale. You must issue a docker
pull
command to update your local container images with the most recent changes on DockerHub. Unfortunately, the docker
run
command does not have a --pull
option (see discussion) so if you want the bonefied most recent image every time you run
then you will need to issue pull
and run
commands in tandem like so:
docker pull <IMAGE> && nvidia-docker run <IMAGE>
This useage will ensure the "latest" image is retreived from the repository before running a container from that image.
You might be wondering where exactly did docker
put this new image? The contents of the /var/lib/docker
directory vary depending on the driver docker
is using for storage. You can find out more about how docker
organizes images here. To figure out what driver is being used for storage we can use the docker
info
command
In [39]:
docker info 2> /dev/null | grep "Storage Driver:"
In [38]:
docker info 2> /dev/null | grep "Data loop file:"
Here docker
on Ubuntu uses devicemapper
by default. You can read more about how the devicemapper
storage driver works here. The acutual binary file with the image data is specified by the "Data loop file
". Out of curiosity you can ask how big this file is but it does require sudo
access:
In [41]:
sudo du -sh /var/lib/docker/devicemapper/devicemapper/data
To demonstrate how easy it is to do awesome stuff with docker
containers, lets use TensorFlow from Google to perform optical character recognition of handwritten digits 0 through 9 in the MNIST dataset.
First lets pull the GPU accelerated tensorflow container from DockerHub
In [44]:
docker pull tensorflow/tensorflow:latest-gpu
In [45]:
docker images
Next lets train a deep convolution neural network to recognize 28x28 pixel images of handwritten digits 0 - 9 (this takes a few minutes)
In [46]:
nvidia-docker run --rm 48a64e7e7fee python -m tensorflow.models.image.mnist.convolutional
You can visit the DockerHub page for the tensorflow image here. Additionally, the actual Dockerfile used to generate this TensorFlow image on DockerHub is available on github here. If you look at the Dockerfile for creating the GPU enable TensorFlow image you will see that it is built FROM
the NVIDIA container nvidia/cuda:8.0-cudnn5-devel
. Building the image from the Dockerfile ensures that the image is fresh and contains the latest version of everything but it does take a long time to build. Usually, pulling the container from DockerHub is much faster but might not contain most recent versions of all src etc.
You can learn more about MNIST in TensorFlow here
In [55]:
docker pull kaixhin/cuda-mxnet:8.0
Next, we run the MXNet example for just a few epochs of training
In [58]:
nvidia-docker run \
--rm \
--workdir=/root/mxnet/example/image-classification \
kaixhin/cuda-mxnet:8.0 \
python train_mnist.py --network lenet --gpus 0 --num-epochs 2
There you go! Notice that we have run both TensorFlow and MXNet frameworks with little to no effort in configuring the frameworks. We just pull the latest images (with gpu tags) and run the training in the a container. Setting up these frameworks on your local machine is often a length task taking hours or even days (worst case). As you can see, using containers encapsulates the framework and allows us to focus on actually getting work done. Not to mention, using containers keeps the local machine clean.
You might notice the error in the output:
libdc1394 error: Failed to initialize libdc1394
but don't worry about it. The libdc1394 is actually a camera driver and does not effect this example.
If you look at the Dockerfile for creating the GPU enable MXNet docker
image you will see that it is built FROM
the NVIDIA container nvidia/cuda:8.0-cudnn5-devel
just like the TensorFlow image. Again, building the image from the Dockerfile ensures that the image is fresh and contains the latest version of everything but it does take a long time to build. Usually, pulling the container from DockerHub is much faster but might not contain most recent versions of all src etc. Both TensorFlow and MXNet have regular/routine image builds pushed to DockerHub.
You can learn more about MNIST in MXNet here.
There are many reason you might want to run your own local repository. For example, maybe there is super secret code in your container or your network is restricted and you can't access DockerHub. There are basic instructions avaliable on the docker blog for getting started with your own local registry. For simplicity, we are actually going to use a docker
container to run our local registry.
Lets first pull the registry image from DockerHub
In [59]:
docker pull registry
Notice we did not have to specify a use name there! Next, we launch a container in detached mode (i.e. background) since the registry is a service which runs continuously waiting for requests. If we don't use detatch here then we never finish evaluating this command and we just wait and wait ... and wait since the service will never quit.
In [61]:
docker run --detach --publish 5000:5000 registry:latest
Now we have a container running in the background. You might have noticed that we launched this container with docker
rather than nvidia-docker
. This is because there is no GPU activity in the registry
image so there is no need to use the nvidia docker plugin here when launching associated containers.
Notice that we have published port 5000 from the container and have mapped it to our localhost port of 5000. In this way, the container can listen for incoming connections. Learn more about binding container ports to the host here.
To see our running containers use the docker
ps
command
In [62]:
docker ps
We can get a response from the registry using curl
In [63]:
curl -i http://localhost:5000/v2
As we can see from the reponse, this Docker registry is running with API version 2.0. More on the docker
registry API here. We are now ready to prepare an image for our local registry. The key here is that we must use the full docker
naming convension so that docker
knows where to put the image. To do this we will simply use the tag
command to manipulate the image name. For demonstration purposes we will use the docker
image busybox
which is a very small image (~1MB) which doesn't do anything.
In [64]:
docker pull busybox
In [65]:
docker images
In [66]:
docker tag busybox localhost:5000/busybusy
Now with issue an images
command to see the localhost:5000/busybusy:latest
image created by the tag
command
In [67]:
docker images
Next, we use the push
command to publish our image to the local registry
In [68]:
docker push localhost:5000/busybusy
Now, we can ask the registry for the image catalog to verify our image is now available
In [70]:
curl http://localhost:5000/v2/_catalog
If we push another image (with different tag) to the local repository we will see another entry in the catalog
In [71]:
docker tag busybox localhost:5000/verybusy
docker push localhost:5000/verybusy
curl http://localhost:5000/v2/_catalog
Each image in the repository is described by a manifest which can be accessed via the API. For example, to access the manifest describing the image verybusy
we write
In [80]:
curl http://localhost:5000/v2/verybusy/manifests/latest | head -15
Finally, we can ask for available tags with the API call
GET /v2/<name>/tags/list
as follows
In [81]:
curl http://localhost:5000/v2/verybusy/tags/list
For more information, see the official docker
documentation for registry deployment.
Don't forget to stop your registry container and clean up!
In [85]:
docker ps
In [83]:
docker stop 917dc854922f && docker rm 917dc854922f
In this section you learned how to leverage images hosted on the public docker
image registry DockerHub. You can always pull images from DockerHub anonymously but if you want to push images you'll need to sign up and get a Docker ID. When pulling and images to you local machine it is important to keep in mind that updates to the images can occur on DockerHub but you'll need to re-pull the image to get the updates. Just keep this in mind so that you don't get stuck working with stale images. A common practice is to distribute Dockerfiles
which allows users to build the image locally which ensures that all the image contents are up-to-date. Furthermore, we identified docker
images hosted on DockerHub for major deep learning frameworks such as TensorFlow and MXNet as well as where to find their respective Dockerfile
s. For both of these frameworks we demonstrated how to run deep learning training on the MNIST dataset. Finally, for those not able or willing to use public image repositories such as DockerHub, we breifely showed how to create a local docker
registry using the registry:latest
image and interact with the service using the Docker Registry HTTP API V2. In configuring this local docker
registry we learned how to lauch containers in detached mode so that the container could run continuously in the background while returning control to the user. Finally, for services requiring connections we saw how we can bind host and container network ports.
What you have seen hear is just the tip of the iceberg. In follow on tutorials we will show how to scale out containerized workflows using Kubernetes and Mesos. In learning how to scale out containerized applications you'll need to understand a bit more about docker
contianer networking. Additionally, as you might guess ther is an entire ecosystem of utilities and services for managing docker configurations, development, and deployments such as runc, nsenter, Docker Remote, docker-py, Docker Compose, Docker Machine, Docker Swarm, and Vagrant. Not to mention, container support by the major Cloud service providers such as Google Compute Engine, Amazon Web Services, and Microsoft Azure. Finally, there is a new breed of operating systems gaining popularity which only contain enough functionality to be able to launch containers! Examples of these optimized container OSes are CoreOS, Atomic, Ubuntu Core, and RancherOS. Many of these container OSes are already supported on the aforementioned Cloud providers, AWS in particular.
Here are some installation details for getting docker and nvidia-docker up and running from scratch ...
In order to get GPU access with in docker/nvidia-docker we need to make sure that the NVIDIA driver is available on the host system. It's possible to obtain the appropriate device driver from either the standard driver download page or via CUDA installation.
Once the NVIDIA device driver has been successfully installed, we need to install docker
it self. The installation of docker is quite simple but it is just slightly different for each OS. The steps for docker
installation on Ubuntu 14.04 can be found here. Don't worry, the docker
docs have install instructions for many other operating systems including RedHat, CentOS, Debian, and so on.
Docker provides an official installation script via https://get.docker.com which can accessed via command-line using "wget -qO-
" or "curl -sSL
"
The final configuration step is to obtain the nvidia-docker
plugin which properly exposes the GPU hardware and drivers for docker
containers. Official installation instructions for nvidia-docker
for Ubuntu, CentOS, and other distributions can be found here
In [ ]:
# create config file for jupyter (file copied by Dockerfile)
cat << LINES > jupyter_notebook_config.py
c.NotebookApp.ip = '*'
c.NotebookApp.port = 8888
c.NotebookApp.open_browser = False
LINES
In [ ]:
# create run hook for launch (file copied by Dockerfile)
cat << LINES > jupyter_notebook_config.py
#!/bin/bash
jupyter notebook "$@"
LINES
In [ ]:
cat << LINES > Dockerfile
FROM <YOUR/IMAGE:HERE>
RUN apt-get update && apt-get install -y \
libzmq3-dev \
python-dev \
python-matplotlib \
python-pandas \
python-pip \
python-sklearn && \
rm -rf /var/lib/apt/lists/*
RUN pip install \
ipykernel \
jupyter && \
python -m ipykernel.kernelspec
COPY jupyter_notebook_config.py /root/.jupyter/
COPY jupyter.sh /usr/local/bin
WORKDIR /data
VOLUME /data
EXPOSE 8888
CMD ["jupyter.sh"]
LINES