Docker: inside docker

Docker container

Docker allows you to run applications inside containers. Running an application inside a container takes a single command: docker run.

# Usage:  [sudo] docker [command] [flags] [arguments] ..
$ sudo docker run ubuntu:14.04 /bin/echo 'Hello world'

We told Docker what command to run inside our new container, /bin/echo ‘Hello world’. When our container was launched Docker created a new Ubuntu 14.04 environment and then executed the /bin/echo command inside it. Docker containers only run as long as the command you specify is active. Here, as soon as Hello world was echoed, the container stopped.

$ sudo docker run -t -i ubuntu:14.04 /bin/bash

We’ve passed in two flags: -t and -i. The -t flag assigns a pseudo-tty or terminal inside our new container and the -i flag allows us to make an interactive connection by grabbing the standard in (STDIN) of the container. We’ve also specified a new command for our container to run: /bin/bash. This will launch a Bash shell inside our container. You can play around inside this container and when you’re done you can use the exit command or enter Ctrl-D to finish. Once the Bash shell process has finished, the container is stopped.

sudo docker run -d ubuntu:14.04 /bin/sh -c "while true; do echo hello world; sleep 1; done"

We ran docker run but this time we specified a flag: -d. The -d flag tells Docker to run the container and put it in the background, to daemonize it. So why aren’t we seeing any hello world’s? Instead Docker has returned a really long string:

1e5535038e285177d5214659a068137486f96ee5c2e85a4ac52dc83f2ebe4147

This really long string is called a container ID. It uniquely identifies a container so we can work with it. The container ID is a bit long and unwieldy, but a shorter ID is possible and some ways to name our containers to make working with them easier.

We can use this container ID to see what’s happening with our hello world daemon. We can do that with the docker ps command. The docker ps command queries the Docker daemon for information about all the containers it knows about. The docker ps has returned some useful information about it, starting with a shorter variant of its container ID: 1e5535038e28.

#  Show the version of Docker client and daemon you are using, but also show the version of Go (the programming language powering Docker).
$ sudo docker version
 
# See a list of all currently available commands
$ sudo docker
 
# Stop container
$ sudo docker stop nostalgic_morse
 
# Restart container
$ sudo docker restart nostalgic_morse
 
# Remove container
$ sudo docker rm nostalgic_morse

For a sample web application we’re going to run a Python Flask application:

sudo docker run -d -P training/webapp python app.py
 
# See our running container
# -l tells the docker ps command to return the details of the last container started
#  If you want to see stopped containers too use the -a flag
sudo docker ps -l

The -P flag is tells Docker to map any required network ports inside our container to our host. We’ve specified a command for our container to run: python app.py. This launches our web application. For example Docker has exposed port 5000 (the default Python Flask port) on port 49155. View the app by typing localhost:49155 in a webbrowser.

Network port bindings are very configurable in Docker. In our last example the -P flag is a shortcut for -p 5000 that maps port 5000 inside the container to a high port (from the range 49153 to 65535) on the local Docker host. We can also bind Docker containers to specific ports using the -p flag, for example:

$ sudo docker run -d -p 5000:5000 training/webapp python app.py

Using the docker ps command to return the mapped port is a bit clumsy so Docker has a useful shortcut we can use: docker port. To use docker port we specify the ID or name of our container and then the port for which we need the corresponding public-facing port:

$ sudo docker port nostalgic_morse 5000
0.0.0.0:49155
 
# See docker logs
# -f causes the docker logs command to act like the tail -f command and watch the container's standard out 
$ sudo docker logs -f nostalgic_morse
* Running on http://0.0.0.0:5000/
10.0.2.2 - - [23/May/2014 20:16:31] "GET / HTTP/1.1" 200 -
10.0.2.2 - - [23/May/2014 20:16:31] "GET /favicon.ico HTTP/1.1" 404 -
 
# Examine the processes running inside it using the docker top command
$ sudo docker top nostalgic_morse
 
# Take a low-level dive into our Docker container using the docker inspect command
# It returns a JSON hash of useful configuration and status information about Docker containers
$ sudo docker inspect nostalgic_morse

Docker Images

Docker images are the basis of containers.

#  Listing the images we have locally on our host
$ sudo docker images
 
# Search for images on dockerhub
sudo docker search ubuntu
 
#  Pre-load an image we can download it using the docker pull command
$ sudo docker pull centos

So far we’ve seen two types of images repositories, images like ubuntu, which are called base or root images. These base images are provided by Docker Inc and are built, validated and supported. These can be identified by their single word names.

We’ve also seen user images, for example the training/sinatra image we’ve chosen. A user image belongs to a member of the Docker community and is built and maintained by them. You can identify user images as they are always prefixed with the user name, here training, of the user that created them.

The team has found the training/sinatra image pretty useful but it’s not quite what they need and we need to make some changes to it. There are two ways we can update and create images:

  • We can update a container created from an image and commit the results to an image
  • We can use a Dockerfile to specify instructions to create an image

To update an image we first need to create a container from the image we’d like to update:

$ sudo docker run -t -i training/sinatra /bin/bash
 
# Inside our running container let's add the json gem
root@0b2616b0e5a8:/# gem install json

Once this has completed let’s exit our container using the exit command. Now we have a container with the change we want to make. We can then commit a copy of this container to an image using the docker commit command.

$ sudo docker commit -m "Added json gem" -a "Kate Smith" \
0b2616b0e5a8 ouruser/sinatra:v2
4f177bd27a9ff0f6dc2a830403925b5360bfe0b93d476f7fc3231110e7f71b1c

Here we’ve used the docker commit command. We’ve specified two flags: -m and -a. The -m flag allows us to specify a commit message, much like you would with a commit on a version control system. The -a flag allows us to specify an author for our update. We’ve also specified the container we want to create this new image from, 0b2616b0e5a8 (the ID we recorded earlier) and we’ve specified a target for the image (ouruser/sinatra:v2).

It consists of a new user, ouruser, that we’re writing this image to. We’ve also specified the name of the image, here we’re keeping the original image name sinatra. Finally we’re specifying a tag for the image: v2. We can then look at our new ouruser/sinatra image using the docker images command:

$ sudo docker images

Using the docker commit command is a pretty simple way of extending an image but it’s a bit cumbersome and it’s not easy to share a development process for images amongst a team. Instead we can use a new command, docker build, to build new images from scratch.

To do this we create a Dockerfile that contains a set of instructions that tell Docker how to build our image. Let’s create a directory and a Dockerfile first:

$ mkdir sinatra
$ cd sinatra
$ touch Dockerfile
 
# Each instruction creates a new layer of the image
# Each instruction prefixes a statement and is capitalized
# FROM tells Docker what the source of our image is
# MAINTAINER instruction to specify who maintains our new image
# A RUN instruction executes a command inside the image
 
# This is a comment
FROM ubuntu:14.04
MAINTAINER Kate Smith <ksmith@example.com>
RUN apt-get update && apt-get install -y ruby ruby-dev
RUN gem install sinatra
 
# Build an image
$ sudo docker build -t ouruser/sinatra:v2 .

We’ve specified our docker build command and used the -t flag to identify our new image as belonging to the user ouruser, the repository name sinatra and given it the tag v2. We’ve also specified the location of our Dockerfile using the . to indicate a Dockerfile in the current directory.

An image can’t have more than 127 layers regardless of the storage driver. This limitation is set globally to encourage optimization of the overall size of images.

We can then create a container from our new image:

$ sudo docker run -t -i ouruser/sinatra:v2 /bin/bash

You can also add a tag to an existing image after you commit or build it. We can do this using the docker tag command. Let’s add a new tag to our ouruser/sinatra image:

$ sudo docker tag 5db5f8471261 ouruser/sinatra:devel

Once you’ve built or created a new image you can push it to Docker Hub using the docker push command. This allows you to share it with others, either publicly, or push it into a private repository.

$ sudo docker push ouruser/sinatra
 
# You can also remove images on your Docker host in a way similar to containers using the docker rmi command
$ sudo docker rmi training/sinatra

Docker container

Linking Containers Together

By default the -p flag will bind the specified port to all interfaces on the host machine. But you can also specify a binding to a specific interface, for example only to the localhost:

# Bind port 5000 inside the container to port 5000 on the localhost or 127.0.0.1 interface on the host machine
$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py
 
# Bind port 5000 of the container to a dynamic port but only on the localhost
$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py
 
# Bind UDP ports by adding a trailing /udp
$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

Docker also has a linking system that allows you to link multiple containers together and send connection information from one to another. When containers are linked, information about a source container can be sent to a recipient container. This allows the recipient to see selected data describing aspects of the source container.

To establish links, Docker relies on the names of your containers. You can also name containers yourself. This naming provides two useful functions:

  • It can be useful to name containers that do specific functions in a way that makes it easier for you to remember them, for example naming a container containing a web application web
  • It provides Docker with a reference point that allows it to refer to other containers, for example, you can specify to link the container web to container db

You can name your container by using the –name flag:

$ sudo docker run -d -P --name web training/webapp python app.py

Links allow containers to discover each other and securely transfer information about one container to another container. When you set up a link, you create a conduit between a source container and a recipient container. The recipient can then access select data about the source. To create a link, you use the –link flag. –link :alias, where name is the name of the container we’re linking to and alias is an alias for the link name.

You can link multiple recipient containers to a single source. For example, you could have multiple (differently named) web containers attached to your db container.

First, create a new container, this time one containing a database:

$ sudo docker run -d --name db training/postgres
 
# Create a new web container and link it with your db container
$ sudo docker run -d -P --name web --link db:db training/webapp python app.py

Docker creates a secure tunnel between the containers that doesn’t need to expose any ports externally on the container; you’ll note when we started the db container we did not use either the -P or -p flags. That’s a big benefit of linking: we don’t need to expose the source container, here the PostgreSQL database, to the network.

Docker exposes connectivity information for the source container to the recipient container in two ways: environment variables and updating the /etc/hosts file.

Environment Variables

Docker creates several environment variables when you link containers. Docker automatically creates environment variables in the target container based on the –link parameters. It will also expose all environment variables originating from Docker from the source container. These environment variables enable programmatic discovery from within the target container of information related to the source container. These include variables from:

  • the ENV commands in the source container’s Dockerfile
  • the -e, –env and –env-file options on the docker run command when the source container is started

It is important to understand that all environment variables originating from Docker within a container are made available to any container that links to it. This could have serious security implications if sensitive data is stored in them. You can run the env command to list the specified container’s environment variables:

$ sudo docker run --rm --name web2 --link db:db training/webapp env

Unlike host entries in the /etc/hosts file, IP addresses stored in the environment variables are not automatically updated if the source container is restarted. We recommend using the host entries in /etc/hosts to resolve the IP address of linked containers. These environment variables are only set for the first process in the container. Some daemons, such as sshd, will scrub them when spawning shells for connection.

$ sudo docker run -t -i --rm --link db:db training/webapp /bin/bash
root@aed84ee21bde:/opt/webapp# cat /etc/hosts
172.17.0.7  aed84ee21bde
. . .
172.17.0.5  db

You can see two relevant host entries. The first is an entry for the web container that uses the Container ID as a host name. The second entry uses the link alias to reference the IP address of the db container. You can ping that host now via this host name.

root@aed84ee21bde:/opt/webapp# apt-get install -yqq inetutils-ping
root@aed84ee21bde:/opt/webapp# ping db

If you restart the source container, the linked containers /etc/hosts files will be automatically updated with the source container’s new IP address, allowing linked communication to continue.

Managing Data in Containers

A data volume is a specially-designated directory within one or more containers. Data volumes provide several useful features for persistent or shared data:

  • Volumes are initialized when a container is created. If the container’s base image contains data at the specified mount point, that data is copied into the new volume
  • Data volumes can be shared and reused among containers
  • Changes to a data volume are made directly
  • Changes to a data volume will not be included when you update an image
  • Data volumes persist even if the container itself is deleted

Data volumes are designed to persist data, independent of the container’s life cycle. Docker therefore never automatically delete volumes when you remove a container, nor will it “garbage collect” volumes that are no longer referenced by a container.

You can add a data volume to a container using the -v flag with the docker create and docker run command. You can use the -v multiple times to mount multiple data volumes. You can also use the VOLUME instruction in a Dockerfile to add one or more new volumes to any container created from that image.

# Create a new volume inside a container at /webapp
$ sudo docker run -d -P --name web -v /webapp training/webapp python app.py
 
# Mount a directory from your Docker daemon's host into a container
# Mount the host directory, /src/webapp, into the container at /opt/webapp
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py
 
# Docker defaults to a read-write volume
# Mount a directory read-only
$ sudo docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py
 
# Mount a single file from the host machine
# This will drop you into a bash shell in a new container, you will have your bash history from the host
# When you exit the container, the host will have the history of the commands typed while in the container
$ sudo docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

If you have some persistent data that you want to share between containers, or want to use from non-persistent containers, it’s best to create a named Data Volume Container, and then to mount the data from it.

Let’s create a new named container with a volume to share. While this container doesn’t run an application, it reuses the training/postgres image so that all containers are using layers in common, saving disk space.

$ sudo docker create -v /dbdata --name dbdata training/postgres
 
# You can then use the --volumes-from flag to mount the /dbdata volume in another container
$ sudo docker run -d --volumes-from dbdata --name db1 training/postgres

If you remove containers that mount volumes, including the initial dbdata container, or the subsequent containers db1 and db2, the volumes will not be deleted. To delete the volume from disk, you must explicitly call docker rm -v against the last container with a reference to the volume. This allows you to upgrade, or effectively migrate data volumes between containers.