Docker is a very interesting project. It promises to alleviate the pain of running and deploying modern, scalable server applications. I want to believe the hype, but I have some reservations about using it for real projects. In this post, I am going to start from the beginning and point out things that surprise me about docker's behavior. Then I am going to revisit the docs and devise experiments to clarify concepts I don't understand.

This post will examine the basics of working with containers and images - the foundation upon which all other docker features are built. It is not comprehensive, but instead focuses on what is most interesting or important to me.

If you plan to follow along, you will benefit from a running docker installation. For installing docker locally, I found Docker Machine very easy to install and configure. If you would prefer, Digital Ocean can launch a droplet with the latest Docker already installed and ready for immediate use.

I am using Docker 1.6.0 for the examples in this post. Lines that start with the ~$ prompt are from my local shell and lines that start with root@ are from inside the docker container.

My container is gone! Where did it go?

Below I am running my first docker container. I use the -i (interactive) and -t (pseudo TTY) flags together, which let me interact with the container like I interact with my terminal.

~$ docker run -it ubuntu /bin/bash
[email protected]:/# echo hello docker > /message.txt
[email protected]:/# cat /message.txt
hello docker
[email protected]:/# exit

Notice I created an important file, /message.txt, before exiting my container. I would like to read this file back now.

~$ docker run -it ubuntu cat /message.txt
cat: /message.txt: No such file or directory

Uh oh! There is no such file. I run docker ps to list my containers, but there is no such container.

It turns out, the command I am looking for is docker ps -a, which lists all of my containers, running or not.

~$ docker ps -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS                     PORTS               NAMES
b3d8c3ef31a0        ubuntu:latest       "cat /message.txt"   3 minutes ago       Exited (1) 3 minutes ago                       admiring_lalande
1f608dc4e5b4        ubuntu:latest       "/bin/bash"          4 minutes ago       Exited (0) 3 minutes ago                       insane_wright

That's interesting. It looks like I created two containers, not just one. Why is that?

Docker images combine a set of file system layers to form a single file system using something called a Union file system. From a user perspective, the combined layers appear as a single file system. Docker image layers are read-only, so when you run a container via the docker run command, Docker creates a read-write layer for you to work with. In our example, we specify the ubuntu image in our docker run command, so the file system layers in the ubuntu image form the starting point for our container.

Now to answer why there are two containers. Each time you run a container, Docker creates a new read-write layer. The read-write layer for one container is not shared with other containers. Becase we used the docker run command twice, we now have two separate containers, each with a distinct read-write layer. While the containers do share a common set of base layers from the ubuntu image, the read-write layer for each container is unique.

How do I access the data in my old container?

Now that we know our containers still exist, let's retrieve the contents of our message file. Looking back at the output of docker ps -a, we see that the second container, the one with /bin/bash in the command column, is the container that has /message.txt file. We can reference this container by it's ID, 1f608dc4e5b4, or by its name, insane_wright.

For the rest of the examples, I will refer to containers by name because I find the container names Docker generates both more entertaining and easier to remember than container IDs. If you prefer, you can name your container via the --name argument of the docker run command.

Let's restart our container to view our message file.

~$ docker start -ai insane_wright
[email protected]:/# cat /message.txt
hello docker
[email protected]:/# exit

I used the docker start command with the -a (attach) argument to view stdout and the -i (interactive) argument to write to stidn. The container starts with the same read-write layer in the same state as when we exited the container. Hooray! We can view our file again.

How do I save and reusue the data in my container?

We found our data, but it would be useful to save and access our data from other containers. We can do this by creating an image from our container with the docker commit command.

~$ docker commit -a "Jordan Bach" -m "saved my message" insane_wright jbgo/message:v0.0.1
1c9195e4c24c894a274f857a60b46fb828ee70ff0e78d18017dfb79c5bf68409

This command turns the read-write layer from our insane_wright container into a read-only layer. Docker places this layer on top of the layers from the ubuntu image on which we originally based the container. This new image is stored in the jbgo/message repository (more on repositories later) with the v0.0.1 tag. Tags are useful for distinguish different versions of the same image.

If you are following along, you should replace jbgo with your Docker Hub user name. If you don't have a Docker Hub account yet, it is definitely worth signing up if you plan to work with docker at all in the future.

Let's verify that our new image exists.

[email protected]:~# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
jbgo/message        v0.0.1              1c9195e4c24c        18 seconds ago      188.3 MB
ubuntu              latest              b7cf8f0d9e82        4 days ago          188.3 MB

Awesome! I see two images. The first image is our new jbgo/message:v0.0.1 image, and the second image is ubuntu:latest, which Docker automatically downloaded from Docker Hub when we ran our first container.

Now we can start containers with our new image and they will have the /message.txt file in them.

~$ docker run -it jbgo/message:v0.0.1 cat /message.txt
hello docker

If we list our containers again, we can see our new container using our new image.

~$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND              CREATED             STATUS                      PORTS               NAMES
01bd4c098203        jbgo/message:v0.0.1   "cat /message.txt"   10 seconds ago      Exited (0) 9 seconds ago                        hopeful_lovelace
b3d8c3ef31a0        ubuntu:latest         "cat /message.txt"   33 minutes ago      Exited (1) 33 minutes ago                       admiring_lalande
1f608dc4e5b4        ubuntu:latest         "/bin/bash"          34 minutes ago      Exited (0) 15 minutes ago                       insane_wright

I find it interesting that docker ps shows the most recently created container first. If you only want to see the most recent container, you can use docker ps with the -l (latest) argument.

How do I backup my image?

At this point, we have a custom docker image that contains a message file. We can run new containers with this image and those containers can read and write to the message file. But what if we lose our docker host to a crash or a security compromise? How can we still keep our image?

We need a docker registry - a repository where we can upload and download docker images. Docker provides the Docker Hub registry, which is free for public use and paid for private use. We can also run our own registry if we desire. I will use the Docker Hub registry, since it is free and ready to use. If you're following along, remember to replace jbgo with your actual Docker Hub username in the upcoming examples.

Let's upload our image to the Docker registry.

~$ docker push jbgo/message
The push refers to a repository [jbgo/message] (len: 1)
1c9195e4c24c: Image push failed

Please login prior to push:
Username: jbgo
Password:
Email: [email protected]
WARNING: login credentials saved in /root/.dockercfg.
Login Succeeded
The push refers to a repository [jbgo/message] (len: 1)
1c9195e4c24c: Image already exists
b7cf8f0d9e82: Image successfully pushed
2c014f14d3d9: Image successfully pushed
a62a42e77c9c: Image successfully pushed
706766fe1019: Image successfully pushed
Digest: sha256:b2a98b19e06a4df91150f8a2cd47a6e440cbc13b39f1fc235d46f97f631ad117

Because this was my first time pushing to the docker registry on this host, docker prompts me for my login credentials.

Here is my image on Docker Hub: https://registry.hub.docker.com/u/jbgo/message/

You can download this image on other docker hosts with the docker pull command.

However, you may notice a problem. I tagged my image incorrectly, and docker can't find my image.

~$ docker pull jbgo/message
Pulling repository jbgo/message
FATA[0007] Tag latest not found in repository jbgo/message

Let's fix it. I did not specify a tag in my pull command (as in docker pull jbgo/message:v0.0.1), so docker attempts to use the latest tag by default. While I could pull a specific tag, for now I prefer to use the default, so I will create a latest tag for my image. The v0.0.1 tag will continue to exist and may be beneficial for people who don't always want to use the latest version of my image.

~$ docker tag jbgo/message:v0.0.1 jbgo/message:latest

Now I can push my image with the new tag.

~$ docker push jbgo/message:latest
The push refers to a repository [jbgo/message] (len: 1)
1c9195e4c24c: Image already exists
b7cf8f0d9e82: Image already exists
2c014f14d3d9: Image already exists
a62a42e77c9c: Image already exists
706766fe1019: Image already exists
Digest: sha256:cc2fbbb2029c6402cea639b2454da08ef05672da81176ae97f57d4f51be19fc3

Notice that the push was faster this time because docker recognizes image layers that already exist in the registry and only uploads new layers. Let's try to pull the image again to see if it works.

~ $ docker pull jbgo/message
latest: Pulling from jbgo/message
1c9195e4c24c: Already exists
706766fe1019: Already exists
a62a42e77c9c: Already exists
2c014f14d3d9: Already exists
b7cf8f0d9e82: Already exists
Digest: sha256:cc2fbbb2029c6402cea639b2454da08ef05672da81176ae97f57d4f51be19fc3
Status: Downloaded newer image for jbgo/message:latest

That works, but because we're on the same host, docker did not actually download anything. For fun I'm going to spin up a new docker host and download the image to the new host. You can do this with docker-machine or your favorite cloud provider, or you can skip this step if you trust me.

docker2:~$ docker pull jbgo/message
latest: Pulling from jbgo/message
706766fe1019: Pull complete
a62a42e77c9c: Pull complete
2c014f14d3d9: Pull complete
b7cf8f0d9e82: Pull complete
1c9195e4c24c: Already exists
Digest: sha256:cc2fbbb2029c6402cea639b2454da08ef05672da81176ae97f57d4f51be19fc3
Status: Downloaded newer image for jbgo/message:latest

Based on the command output, we see that docker actually downloaded the image layers this time. Because computers don't operate on blind faith, let's once again verify our message file is present.

docker2:~$ docker run jbgo/message cat /message.txt
hello docker

Success!

What do I do with containers when I'm done with them?

Ok, I think I'm beginning to understand how to work with containers and images. However, I'm creating a bunch of containers and images I don't need anymore. I would like to get rid of these to free up some disk space and unclutter the output of my docker commands.

Below is how you can remove one or more containers.

~$ docker rm hopeful_lovelace insane_wright admiring_lalande
hopeful_lovelace
insane_wright
admiring_lalande

Be sure to verify that they are actually gone.

~$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

At this point, you can no longer recover these contaienrs. Fortunately, we already created an image for the container we cared about and pushed the image to a docker registry. As long as we don't explicity docker rm a container we can still recover the container and its file system contents. This was one of my major concerns when I first tried docker, but now that I know more about how docker containers and images work, I no longer worry about what happens to my containers when I am done with them.

Wait a minute! Why did docker push more than one image?

If you are paying attention, you noticed that when we pushed the jbgo/message image the first time, docker uploaded more than one image to the registry. How could this be?

~$ docker push jbgo/message:v0.0.1
...
1c9195e4c24c: Image already exists
b7cf8f0d9e82: Image successfully pushed
2c014f14d3d9: Image successfully pushed
a62a42e77c9c: Image successfully pushed
706766fe1019: Image successfully pushed

We can use the docker history command to give us some clues.

~$ docker history jbgo/message:v0.0.1
IMAGE               CREATED             CREATED BY                                      SIZE
1c9195e4c24c        33 minutes ago      /bin/bash                                       108 B
b7cf8f0d9e82        4 days ago          /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
2c014f14d3d9        4 days ago          /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
a62a42e77c9c        4 days ago          /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   194.5 kB
706766fe1019        4 days ago          /bin/sh -c #(nop) ADD file:777fad733fc954c0c1   188.1 MB

It seems that we did only create one image. By using docker inspect on the first image, we can see that I'm the author and that the commit message is present as a comment on the image.

~$ docker inspect 1c9195e4c24c
[{
    "Architecture": "amd64",
    "Author": "Jordan Bach",
    "Comment": "saved my message",
    "Config": {

What does the history look like for the ubuntu image?

~$ docker history ubuntu
IMAGE               CREATED             CREATED BY                                      SIZE
b7cf8f0d9e82        4 days ago          /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B
2c014f14d3d9        4 days ago          /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB
a62a42e77c9c        4 days ago          /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic   194.5 kB
706766fe1019        4 days ago          /bin/sh -c #(nop) ADD file:777fad733fc954c0c1   188.1 MB

Ah, mystery solved! Those are the mysterious images in my image. It appears that when I pushed to my Docker Hub account, docker copied the ubuntu layers to my jbgo/message repository. The copy ensures that my image always contains the exact same layers. Otherwise, my image could break if docker did not copy all of the layers into my repository and a new, incompatible version of the ubuntu image was published.

How do I delete an image?

We already deleted our containers, but we still have images we don't want anymore. Let's delete those, too.

docker2:~$ docker rmi jbgo/message
Error response from daemon: Conflict, cannot delete 1c9195e4c24c because the container 2ea39e64a130 is using it, use -f to force
FATA[0000] Error: failed to remove one or more images

Well, crap! I don't have any containers running. But I do have some stopped containers.

docker2:~$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND              CREATED             STATUS                     PORTS               NAMES
2ea39e64a130        jbgo/message:latest   "cat /message.txt"   2 minutes ago       Exited (0) 2 minutes ago                       lonely_jones

This is for your own good. If docker allowed you to delete an image that a container uses, you would never be able to start that container again. In our case, we really want to delete the image, so let's delete the container first.

docker2:~$ docker rm lonely_jones
lonely_jones

Now we can delete the image. Notice the command is docker rmi for remove image as opposed to the shorter docker rm. which is just for containers.

docker2:~$ docker rmi jbgo/message
Untagged: jbgo/message:latest
Deleted: 1c9195e4c24c894a274f857a60b46fb828ee70ff0e78d18017dfb79c5bf68409
Deleted: b7cf8f0d9e82c9d96bd7afd22c600bfdb86b8d66c50d29164e5ad2fb02f7187b
Deleted: 2c014f14d3d95811df672ddae2af376f9557f6b8f5623e3e3f8c5ca3f6ff42e6
Deleted: a62a42e77c9c3626118dc411092d23cf658220261acdafe21a7589a8eeba627e
Deleted: 706766fe101906a1a6628173c2677173a5f8c6c469075083f3cf3a8f5e5eb367

As always, let's verify that the image is really gone.

docker2:~$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE

Conclusion

Congratulations! That was a lot of poking and prodding to understand how docker behaves. There are many more wonderful, useful features of docker that I hope to explore with you soon.