serdar_yegulalp
Senior Writer

Docker tutorial: Get started with Docker

how-to
Jan 17, 201817 mins
Cloud ComputingSoftware Development

Docker has revolutionized how applications are deployed. Follow this step-by-step guide from installing Docker to building a Docker container for the Apache web server

shipping industry with loading binary code containers on ship representing the concept of software
Credit: Thinkstock

Containers provide a lightweight way to take application workloads portable, like a virtual machine but without the overhead and bulk typically associated with VMs. With containers, apps and services can be packaged up and moved freely between physical, virtual, or cloud environments.

Docker, a container creation and management system created by Docker Inc., takes the native container functionality found in Linux and makes it available to end-users through a command-line interface and a set of APIs.

Many common application components are now available as prepackaged Docker containers, making it easy to deploy stacks of software as decoupled components (the microservices model). That said, it helps to know how the pieces fit together from the inside out.

Thus, in this guide, I install the Apache web server in a Docker container and investigate how Docker operates along the way.

Install Docker

I’m using Ubuntu as the foundation of the Docker build. Ubuntu is not just a popular and widely used distribution, but the Docker team itself uses Ubuntu for development, and Docker is supported on Ubuntu Server from versions 12.04 and up. For the sake of simplicity, I start with instructions when using a fresh install of Ubuntu 16.04.

Prepare Ubuntu Linux for Docker

The first thing to do is obtain the proper version of the kernel and its headers:

$ sudo apt-get install --install-recommends linux-generic-hwe-16.04

This process may take some time and will require a reboot when you’re done:

$ sudo reboot

You may also need to upgrade other packages in the system afterwards, too:

$ sudo apt-get update

$ sudo apt-get upgrade

Install Docker on Ubuntu

Installation of Docker on CentOS, Fedora, Debian, Ubuntu, and Raspbian Linux distributions is made easy by way of a shell script that you can download from https://get.docker.com/. For that you’ll need the curl command. To get the newest version of curl:

sudo apt-get install curl

Once you have curl installed, fetch the install script and set it running:

curl -s https://get.docker.com | sudo sh

When the script finishes installing, you see a note like the following, with installation details about the version of Docker, both the client and the server components:

docker install warning IDG

Note the details near the bottom about adding nonroot users to Docker. It’s convenient to do this, but if you do, it’s recommended to create a nonroot user specifically for working with Docker and for no other function. For the sake of this tutorial, though, I’m sticking with using sudo to run Docker by way of a nonprivileged user.

Now you can test out a basic Docker container:

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

This command downloads the generic Docker Ubuntu image (as per the ubuntu parameter) and run the /bin/bash command in that container. The -i and -t options open standard input and a pseudo TTY respectively. 

If it’s successful, you should see the hostname in the command prompt change to something like root@216b04387924:/#, which indicates the ID number (and hostname) of your new running container. To leave, type exit, same as you would to leave any shell session.

You should now have a functional Docker installation on your server. You can test it and get basic information using the docker info command:

$ sudo docker info

The output of the docker info command shows the number of containers and images, among other pertinent information. Note that it may be quite lengthy; this example shows only the last of two pages.

sudo docker info IDG

One last change you will need to make if you’re running Ubuntu’s UFW firewall is to allow for packet forwarding. You can check whether UFW is running by entering the following:

$ sudo ufw status

If the command returns a status of inactive, you can skip this next step. Otherwise you will need to edit the UFW configuration file /etc/default/ufw and change the policy for forwarding from DROP to ACCEPT. To do this using the Nano editor, enter the following:

$ sudo nano /etc/default/ufw

And change this line:

DEFAULT_FORWARD_POLICY="DROP"

To this:

DEFAULT_FORWARD_POLICY="ACCEPT"

Save the file, then run:

$ sudo ufw reload

Work with Docker images and Docker containers

Docker containers are much more efficient than virtual machines. When a container is not running a process, it is completely dormant. You might think of Docker containers as self-contained processes—when they’re not actively running, they consume no resources apart from storage.

You can view active and inactive containers using the docker ps command:

# This command will show ALL containers on the system $ sudo docker ps  -a

# This will show only RUNNING containers $ sudo docker ps       

You can view all available commands by simply entering docker. For an up-to-date rundown of all commands, their options, and full descriptions, consult the official command-line client documentation.

When I ran docker run earlier, that command automatically pulled an Ubuntu container image from the Docker Hub registry service. Most of the time, though, you’ll want to pull container images into the local cache ahead of time, rather than do that on demand. To do so, use docker pull, like this:

$ sudo docker pull ubuntu

A full, searchable list of images and repositories is available on the Docker Hub.

Docker images vs. containers

Something worth spelling out at this point is how images, containers, and the pull/push process all work together.

Docker containers are built from images, which are essentially shells of operating systems that contain the necessary binaries and libraries to run applications in a container.

Images are labeled with tags, essentially metadata, that make it easy to store and pull different versions of an image. Naturally, a single image can be associated with multiple tags: ubuntu:16.04, ubuntu:xenial-20171201, ubuntu:xenial, ubuntu:latest.

When I typed docker pull ubuntu earlier, I pulled the default Ubuntu image from the Ubuntu repository, which is the image tagged latest. In other words, the command docker pull ubuntu is equivalent to docker pull ubuntu:latest and (at the time of this writing) docker pull ubuntu:xenial

Note that if I had typed: 

$ sudo docker pull -a ubuntu

I would have puledl all images (the -a flag) in the Ubuntu repository into my local system. Most of the time, though, you will want either the default image or a specific version. For example, if you want the image for Ubuntu Saucy Salamander, you’d use docker pull -a ubuntu:saucy to fetch the image with that particular tag from that repo.

The same logic behind repos and tags applies to other manipulations of images. If you pulled saucy as per the above example, you would run it by typing sudo docker run -i -t ubuntu:saucy /bin/bash. If you type sudo docker image rm ubuntu, to remove the ubuntu image, it will remove only the image tagged latest . To remove images other than the default, such as Ubuntu Saucy, you must include the appropriate tag:

sudo docker image rm ubuntu:saucy

Docker image and container workflow

Back to working with images. Once you’ve pulled an image, whatever it may be, you create a live container from it (as I’ve shown) by executing the docker run command. After you have added software and changed any settings inside the container, you can create a new image from those changes by using the docker commit command.

It’s important to note that Docker only stores the deltas, or changes, in images built from other images. As you build your own images, only the changes you make to the base image are stored in the new image, which links back to the base image for all its dependencies. Thus you can create images that have a virtual size of 266MB, but take up only a few megabytes on disk, due to this efficiency.

Fully configured containers can then be pushed up to a central repository to be used elsewhere in the organization or even shared publicly. In this way, an application developer can publish a public container for an app, or you can create private repositories to store all the containers used internally by your organization.

Create a new Docker image from a container

Now that you have a better understanding of how images and containers work, let’s set up a Apache web server container and make it permanent.

Start with a new Docker container

First, you need to build a new container. There are a few ways to do this, but because you have a few commands to run, start a root shell in a new container:

$ sudo docker run -i -t --name apache_web ubuntu /bin/bash

This creates a new container with a unique ID and the name apache_web. It also gives you a root shell because you specified /bin/bash as the command to run. Now install the Apache web server using apt-get:

root@d7c8f02c3c8c:/# apt-get update root@d7c8f02c3c8c:/# apt-get install apache2

Note that you don’t need to use sudo, because your’re running as root inside the container. Note that you do need to run apt-get update, because, again, the package list inside the container is not the same as the one outside of it.

The normal apt-get output appears, and the Apache2 package is installed in your new container. Once the install has completed, start Apache, install curl, and test the installation, all from within your container:

root@d7c8f02c3c8c:/# service apache2 start root@d7c8f02c3c8c:/# apt-get install curl root@d7c8f02c3c8c:/# curl http://localhost

Following the last command, you should see the raw HTML of the default Apache page displayed in the console. This means our Apache server is installed and running in your container.

If you were doing this in a production environment, you’d next configure Apache to your requirements and install an application for it to serve. Docker letd directories outside a container be mapped to paths inside it, so one approach is to store your web app in a directory on the host and make it visible to the container through a mapping.

Create a startup script for a Docker container

Remember that a Docker container runs only as long as its process or processes are active. So if the process you launch when you first run a container moves into the background, like a system daemon, Docker will stop the container. Therefore, you need to run Apache in the foreground when the container launches, so that the container doesn’t exit as soon as it fires up.

Create a script, startapache.sh, in /usr/local/sbin: 

# You might need to first install Nano inside the container root@d7c8f02c3c8c:/# apt-get install nano

root@d7c8f02c3c8c:/# nano /usr/local/sbin/startapache.sh

In the startapache.sh file, add these lines:

#!/bin/bash . /etc/apache2/envvars /usr/sbin/apache2 -D FOREGROUND

Write the changes and save the file. Then make it executable:

root@d7c8f02c3c8c:/# chmod +x /usr/local/sbin/startapache.sh

All this small script does is bring in the appropriate environment variables for Apache and start the Apache process in the foreground.

You’re done modifying the contents of the container, so you can leave the container by typing exit. When you exit the container, the container will stop.

Commit the container to create a new Docker image

Now you need to commit the container to save the changes you’ve made:

$ sudo docker commit apache_web local:apache_web

The commit will save your container as a new image and return a unique ID. The argument local:apache_web will cause the commit to be placed in a local repository named local with a tag of apache_web.

You can see this by running the command sudo docker images:

REPOSITORY  TAG         IMAGE ID      CREATED      VIRTUAL SIZE local       apache_web  d95238078ab0  4 minutes ago  284.1 MB

Note that the exact details of your image—the image ID, the size of the container—will be different from my example.

Docker containers are designed to be immutable. Whenever you commit changes to a container, the results are written out to an entirely new container, never to the original. If you want to swap out Apache with, say, Nginx, you would start with the original ubuntu:latest container, add Nginx to that, and save out the results as an all-new container named something like local:nginx.

Understand Docker networking basics

Now that you have our image, you can start our container and begin serving pages. Before you do, however, let me take a moment to explain how Docker handles networking.

When Docker is installed, it creates three virtual networks that can be used by Docker containers:

  • bridge: This is the network that containers connect to by default. The bridge network allows containers to talk to each other directly, but not to the host system.
  • host: This network lets containers be seen by the host directly, as if any apps within them were running as local network services.
  • none: This is essentially a null or loopback network. A container connected to none can’t see anything but itself.

When you want to launch a container and have it communicate with both other containers and the outside world, you need to manually map ports from that container to the host. For the sake of my example, you can do this on the command line when you launch your newly created container:

$ sudo docker run -d -p 8080:80 --name apache local:apache_web /usr/local/sbin/startapache.sh

The -p switch is used for port mapping. Here, it maps port 8080 on the host to port 80 inside the container.

The --name flag lets you specify a name for the running container. The name is optional (you can always refer to a container by the first five digits of its container ID) but must be different from the name of any other container or image. If you don’t specify a name, one will be randomly generated—such as inspiring_hodgkin or loving_austin.

Once you run this command, you should be able to point a web browser at the IP address of the host and see the default Apache web server page.

Again, you can see the status of the container and the TCP port mappings by using the docker ps command. And you can look up the network mappings by using the docker port command:

$ sudo docker port apache 80 0.0.0.0:8080

Note that you could use the -P option on the docker run command to publish all open ports on the container to the host and map a random high port such as 49153 back to port 80 on the container. This can be used in scripting as necessary, although it is generally a bad idea to do this in production.

At this point, you have a fully functional Docker container running your Apache process. When you stop the container, it will remain in the system and can be restarted at any time via the docker restart command.

Automate Docker image builds with Dockerfiles

As educational as it is to build Docker containers manually, it is pure tedium to do this again and again. To make the build process easy, consistent, and repeatable, Docker provides a form of automation for creating Docker images called Dockerfiles.

Dockerfiles are text files, stored in a repository alongside Docker images. They describe how an specific container is built, letting Docker perform the build process for you automatically. Here is an example Dockerfile for a minimal container, much like the one I built in the first stages of this demo:

FROM ubuntu:latest RUN apt-get update RUN apt-get install -y curl ENTRYPOINT ["/bin/bash"]

If you save this file as dftest in your local directory, you can build an image named ubuntu:testing from dftest with the following command:

$ sudo docker build -t ubuntu:testing - < dftest

Docker will build a new image based on the ubuntu:latest image. Then, inside the container, it will perform an apt-get update and use apt-get to install curl. Finally, it will set the default command to run at container launch as /bin/bash. You could then run:

$ sudo docker run -i -t ubuntu:testing

Et voilà! You have a root shell on a new container built to those specifications. Note that you can also launch the container with this command:

$ sudo docker run -i -t dftest

There are numerous options that can be used in a Dockerfile, such as mapping host directories to containers, setting environment variables, and even setting triggers to be used in future builds. A full list of Dockerfile operators is in the Dockerfile Reference page.

Docker for Mac and Docker for Windows

Docker containers are a Linux-specific technology. However, you can easily run Docker on MacOS or Windows using virtualization. Docker Inc. has developed editions of Docker designed to be run in the Mac and Windows desktop environments: Docker for Mac and Docker for Windows.

Docker for Mac and Docker for Windows work roughly the same way. They install as conventional desktop applications, and they use the native hypervisor (Xhyve on the Mac and Hyper-V on Windows) to run containers. They also provide exactly the same command-line interface used in Docker for Linux, so if you start your Docker adventures in one realm you can move to the other without having to relearn commands.

docker for windows 1 IDG

The command-line interface for Docker on Windows is identical to the Linux version, barring the use of sudo.

If you launch a command-line session (PowerShell in Windows, Terminal on the Mac) and type docker run -i -t ubuntu /bin/bash as I did near the beginning of this tutorial, you should see Docker execute the same steps to pull a fresh copy of the Ubuntu image into the local repository and launch it. 

The desktop editions of Docker also provide a convenient GUI for managing how Docker interacts with the local system. For example, you can define which local drives are automatically made available to containers, so you don’t have to wrangle those permissions yourself. You can also control how much CPU or memory is made available to Docker from the host, how network connections and network proxies behave, and so on.

docker for windows shared drives IDG

Desktop editions of Docker provide a GUI to control interactions between Docker and the host.

Some caveats about Docker for Windows are worth spelling out here:

  • You don’t use sudo to run Docker if you use an admin-level account, because sudo doesn’t exist on Windows and administrative permissions are handled differently anyway. Keep this in mind if you decide to re-enact any tutorials using Docker for Windows.
  • If you use the Oracle VM VirtualBox hypervisor, note that installing Docker for Windows will disable it. Docker for Windows uses Hyper-V, and VirtualBox can’t run when Hyper-V has been enabled. To use VirtualBox, you’ll have to disable Hyper-V (which requires a reboot).

Of course there’s much more to Docker than I’ve covered in this guide, but this should give you a basic understanding of how Docker operates, a grasp of the key Docker concepts, and the know-how to build functional containers. You can find more information on the Docker website including an online tutorial. A guide with more advanced examples can be found at PayneDigital.com.

serdar_yegulalp
Senior Writer

Serdar Yegulalp is a senior writer at InfoWorld, covering software development and operations tools, machine learning, containerization, and reviews of products in those categories. Before joining InfoWorld, Serdar wrote for the original Windows Magazine, InformationWeek, the briefly resurrected Byte, and a slew of other publications. When he's not covering IT, he's writing SF and fantasy published under his own personal imprint, Infinimata Press.

More from this author