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. 

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:

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:

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:

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:

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:

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):

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: 

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:

Inside the container, they are now visible:

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:

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: 

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:

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.

Tags:

Comments

Related Articles