Docker From the Ground Up: Working With Containers, Part 1

This is part one of a two-part series about working with Docker containers. In this part, we’ll focus on the many ways and options to run an image and how the host can interact with a Docker container.

Docker From the Ground Up: Working With Containers, Part 1

In the second part we’ll cover listing, starting, stopping and restarting containers as well as executing commands on running containers. Docker images are the units of deployment. When you run an image, you instantiate a Docker container that runs a single process in its own isolated environment for the file system, networking and processes tree.

Docker containers are very flexible and enable many use cases that are too heavyweight, complex and/or expensive with other technologies like virtual machines and bare metal servers.

Before we start, make sure that Docker is installed properly in your environment. Depending on how it installed and your user, you may need to run it as sudo. I’ll skip the sudo.

Running an Image

You launch a Docker container by running an image. There are several ways to run a container that affect how easy it is to manage all the containers. When the container starts, it typically runs the command defined in the Dockerfile. Here is the Dockerfile for the hello-world container:

FROM scratch
COPY hello /
CMD ["/hello"]

The command simply runs the “hello” binary that was copied to the root of the container when building the image.

Foreground vs. Detached

A container can run in the foreground where it blocks until the process exits and the container stops running. In foreground mode, the container prints its output to the console and can read standard input. In detached mode (when you provide the -d flag), control returns immediately and the container

Running an Unnamed Container

The simplest way to run a container is: docker run <image id or name>.

When you run a container using this command, Docker will assign it a name composed of two random words. For example: docker run hello-world.

If you already have the hello-world image then Docker will run it. If you don’t, it will pull it from the official Docker repository DockerHub and then run it. The output should look like:

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate work-flows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

The hello program exits after displaying the message, which terminates the process running inside the container and ends the container run. The container still sticks around in case you want to connect to it, examine logs, or anything else. To view the container, you can run the following command:

docker ps -a  --format "table {{.ID}}t{{.Status}}t{{.Names}}"

CONTAINER ID        STATUS                     NAMES
8e2e491accb5        Exited (0) 2 minutes ago   clever_liskov

I’ll explain later how to list containers and all the relevant options. For now, let’s focus on the Names section. Docker generated the name “clever_liskov” automatically, and I’ll have to use it or the container ID to refer to this container for any purpose like restarting it, removing it, or executing a command.

Running a Named Container

Using container IDs or auto-generated names is sometimes inconvenient. If you interact frequently with a container you re-create frequently, then it will get a different ID and auto-generated name. Also, the name will be random.

Docker lets you name your containers when you run them by providing a “–name <container name>” command-line argument. In simple cases, where you have just one container per image, you can name the container after your image: docker run --name hello-world hello-world.

Now, if we look at the process (I removed clever_liskov earlier) we’ll see that the container is named hello-world:

docker ps -a --format "table {{.ID}}t{{.Names}}"
CONTAINER ID        NAMES
f6fe77b3b6e8        hello-world

There are several benefits to a named container:

  • You have a stable name for your containers you use both interactively and in scripts.
  • You can choose a meaningful name.
  • You can choose a short name for convenience when working interactively.
  • It prevents you from accidentally having multiple containers of the same image (as long as you always provide the same name).

Let’s look at the last option. If I try to run the same run command again with the same “hello-world” name, I get a clear error message:

docker run --name hello-world hello-world
docker: Error response from daemon: Conflict. The container name
"/hello-world" is already in use by container 
f6fe77b3b6e8e77ccf346c32c599e67b2982893ca39f0415472c2949cacc4a51. 
You have to remove (or rename) that container to be able to reuse 
that name.
See 'docker run --help'.

Running an Auto-Remove Image

Containers stick around by default. Sometimes, you don’t need them. Instead of manually removing exited containers, you make the container go away on its own. The --rm command-line argument does the trick: docker run --rm hello-world.

Running a Different Command

By default, Docker runs the command specified in the Dockerfile used to build the image (or directly the entry-point if no command is found). You can always override it by providing your own command at the end of the run command. Let’s run ls -la on the busybox image (the hello-world image has no ls executable):

docker run busybox ls -la
total 44
drwxr-xr-x   18 root     root          4096 Mar 18 17:06 .
drwxr-xr-x   18 root     root          4096 Mar 18 17:06 ..
-rwxr-xr-x    1 root     root             0 Mar 18 17:06 .dockerenv
drwxr-xr-x    2 root     root         12288 Mar  9 00:05 bin
drwxr-xr-x    5 root     root           340 Mar 18 17:06 dev
drwxr-xr-x    2 root     root          4096 Mar 18 17:06 etc
drwxr-xr-x    2 nobody   nogroup       4096 Mar  9 00:05 home
dr-xr-xr-x   85 root     root             0 Mar 18 17:06 proc
drwxr-xr-x    2 root     root          4096 Mar  9 00:05 root
dr-xr-xr-x   13 root     root             0 Mar 18 17:06 sys
drwxrwxrwt    2 root     root          4096 Mar  9 00:05 tmp
drwxr-xr-x    3 root     root          4096 Mar  9 00:05 usr
drwxr-xr-x    4 root     root          4096 Mar  9 00:05 var

Interacting With the Host

Docker containers run isolated processes in their own little world. But it is often necessary and useful to provide access to the host.

Passing Environment Variables to a Container

Docker containers don’t automatically inherit the environment of the host process that ran them. You need to explicitly provide environment variables to the container when you run it using the -e command-line flag. You can pass multiple environment variables. Here is an example:

docker run --rm -it -e ENV_FROM_HOST="123" busybox
/ # env
HOSTNAME=8e7672bce5a7
SHLVL=1
HOME=/root
ENV_FROM_HOST=123
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
/ #

The first line runs the busybox container, passing it the ENV_FROM_HOST variable and then inside the container running env shows that the ENV_FROM_HOST is properly set.

You can use host environment variables too. This sets a couple of host environment variables and uses them in the run command:

$ export VAR_1=1
$ export VAR_2=2
$ docker run --rm -it -e VAR_1="$VAR_1" -e VAR_2="$VAR_2" busybox

Inside the container, they are now visible:

/ # env | grep VAR
VAR_1=1
VAR_2=2

Mounting Host Directories

One of the most useful interactions is mounting host directories. That allows several interesting use cases:

  • Shared storage between containers running on the same host.
  • Viewing and editing files using your host environment and tools and using the files in the container.
  • Host-level persistence beyond the lifetime of a container.

Here I create a file on the host: $ echo "Yeah, it works!" > ~/data/1.txt

Then I run the busybox image mounting the ~/data directory to /data in the container and displaying the file contents on the screen:

$ docker run --rm -v ~/data:/data busybox cat /data/1.txt
Yeah, it works!

I used the cat /data/1.txt command here.

Exposing Ports to the Host

If you expose a port in your Dockerfile using EXPOSE, it will be accessible only to other docker containers. To make it accessible on the host, you need to use the -p command-line argument. The syntax is -p <host port>:<exposed container port>.

Here is running the nginx image, which exposes port 80 and uses the -p command-line argument to make it visible on the host on port 9000:

docker run --name nginx --rm -d -p 9000:80 nginx

Note that unlike the previous commands that performed some tasks and completed, the nginx container will keep running and listening to incoming requests. Let’s verify that nginx is really up and running and responds to requests on port 9000. I prefer the excellent httpie HTTP client over curl for hitting web servers and services from the command line:

http HEAD localhost:9000

HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 612
Content-Type: text/html
Date: Sun, 19 Mar 2017 07:35:55 GMT
ETag: "58a323e4-264"
Last-Modified: Tue, 14 Feb 2017 15:36:04 GMT
Server: nginx/1.11.10

Conclusion

There are many ways to run a Docker image to create a container, and there are many options. Each combination supports a particular use. It is very useful when working with Docker containers to fully grasp the details and use the best method to launch your containers.

In addition, attaching host volumes and exposing and publishing ports allows tight integration with the host and a plethora of use scenarios. In part two, we’ll dive into managing a bunch of containers and taking advantage of the full power Docker provides.