Docker is an open-source platform for packaging applications into containers. Ever since I discovered Docker some years ago, I have been using it every day.

Docker allows you to easily develop and run applications without having to worry anymore about their dependencies. It’s the next big thing that every developer should familiarise with after version-control.

What is Docker?

Docker is a software and a platform for containerization. In modern Software Development the vast majority of applications, especially open source ones, has a multitude of intricate dependencies. The cohexistence of multiple packages on the same system is often a challenge and can lead to very hard to debug problems. Reproducibility of builds is critical and difficult to achieve as every development machine may have different configurations and characteristics. Think also about situations such as wanting to use applications developed for a different OS from yours or, alternatively, to have to develop applications that will run on a different OS. Docker solves all these problems.

For what concerns its main functionality, it’s possible to say that Docker serves a similar purpose to a virtual machine. The most important advantage of Docker is that it’s extremely more fast, portable and lightweight.

The two main concepts of Docker are images and containers. Docker images are snapshots of a system: they include its OS, its file-system and its applications. Docker containers allow to build and run processes in complete isolation from each other. Starting a container is equivalent to run an application from one of the snap-shotted systems provided by a Docker image. Creating new containers is seamlessly fast and light-weight and provides the same functionalities such as starting a virtual machine.

Setup Docker on your machine

Docker can be used and installed in a variety of systems: Windows, MacOS and Linux are all supported. Check out the Docker website for instructions on how to get and install Docker. The official website is the only reliable source for getting Docker.

On Linux systems it’s recommended to go through the suggested post-installation steps, in particular for managing Docker as a non-root user.

Docker basic usage

Get an image and run a container


docker pull ubuntu:20.04
docker run ubuntu:20.04 echo hello world

The first command will download the Docker image ubuntu with release tag 20.04. The second command will create and run a container from the specified image (ubuntu:20.04) and will execute in it the provided additional command (echo hello world). The output (i.e. hello world) will be printed on the screen and then the container will exit.

The ubuntu:20.04 image is a snap-shot from a bare-minimum system running that Linux distribution.

Try to provide as command cat /etc/os-release and compare how the output from the Docker container is different from what you see if you run it on your machine.

Run container interactive bash session


docker run -it IMAGE_NAME bash

Here we provide bash as command to the container and we add the -it flag. The container will be run in interactive mode and it will allocate a tty attached to its standard input. This effectively creates a new container and gives you complete access to a terminal in it. This is the command that you will run the most if your main purpose for using Docker is to develop using an OS different from yours.

Once you are within the container you can use any number of commands you want and exit to stop the container.

Note that every time you run this command a new container will be created. Containers are completely isolated from each others. So they are effectively different machines.

Enter a running container from a new terminal


docker exec -it CONTAINER_ID bash

Sometimes one terminal is not enough and this command will allow you to enter in a running Docker container from a second terminal.

Run container with a mounted volume


docker run -it -v /path/host:/path/image IMAGE_NAME bash

Mounts the directory on your computer at /path/host in the container. The directory is accessible from the terminal of the container at /path/image and changes to its files are reflected on your system. You can have the container access your files or create new files that then will be on your system.

Containers are extremely useful for software development. You can mount the directory containing your project as a volume in the container. This allows you to build and run your code from within the container. At the same time, you can use an editor to modify your project and the container will automatically see the updated version.

Create your Docker image

Write a Dockerfile


FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
    net-tools \
    vim

The Dockerfile is a text file that contains ordered instructions that are used as the blueprint for a Docker image. The snippet above can be used to generate a Docker image that, besides having Ubuntu 20.04 file-system, also installs some utilities on top of it. All the containers created from this image will have those additional packages already installed.

You can have any number of instructions in your Dockerfile. Instructions are always on a new line and start with a capitalized keyword that denotes their type. The keyword FROM defines from which “initial state” the following instructions will be applied , it can be used only once and at the beginning of the file. The keyword RUN allows to run shell commands, for example for downloading files or building and installing packages. See best practices for writing Dockerfiles for more details and the other keywords.

Build Docker image from Dockerfile


docker build -t IMAGE_NAME .

This shell command will create a Docker image with the specified name from the Dockerfile included in the working directory. Note that you can specify a path to a file by using the -f PATH_TO_DOCKERFILE syntax in the command.

Advanced commands and tips

All the following commands are not required for a basic Docker usage. Make sure that you are comfortable with all the previously shown commands and concepts before diving into them.

Copy between container and host


docker cp CONTAINER_ID:/path/container /path/host

The syntax is similar to using scp with the CONTAINER_ID as hostname. It allows also to copy from host to container. Note that it’s not needed to use -r argument when copying directories.

Enter stopped container


docker start CONTAINER_ID
docker attach CONTAINER_ID

When you exit from a container, this is stopped. You can start again a stopped container to keep working from were you have been.

Get CONTAINER_ID from within a container


head -1 /proc/self/cgroup | cut -d / -f 3

This is particularly helpful when you have several running containers from the same image and you need to identify one.

Add volume to existing container


docker commit CONTAINER_ID NEW_IMAGE_NAME
docker run -it -v /path/host:/path/image NEW_IMAGE_NAME bash

What this command is actually doing is to create a new image from a snapshot of the current container. Then a new container is run from that image, mounting a volume.

Run container with GUI


docker run -it \
	  --net=host \
	  --privileged \
	  --volume=/tmp/.X11-unix:/tmp/.X11-unix \
	  --device=/dev/dri:/dev/dri \
	  --env="DISPLAY" \
	  IMAGE_NAME \
	  bash

For more details check out this tutorial from ROS.

Run containers under VPN


cat << EOF | sudo tee /etc/docker/daemon.json > /dev/null
{
    "dns": ["10.0.0.2", "8.8.8.8"],
}
EOF

Running a container while connected to VPN may result in the container to not be able to access the network, due to dns issues. The /etc/docker/daemon.json file on your computer should contain the default Google dns "8.8.8.8". The fix consists in prepending your dns. You can obtain your dns with nmcli dev show | grep 'IP4.DNS' and you should use that instead of the "10.0.0.2" in the example. After running the command it’s recommended to restart the Docker daemon with sudo service docker restart.

Reduce size of Docker images

There is an important note to remember about Dockerfiles. Docker images are made by layers.

Be careful in how you write them, otherwise the generated Docker images may result huge. Every command such as RUN that you write in the file will create a layer in the Docker image history.

Clean unused containers and images


docker rm $(docker ps -q -f 'status=exited')
docker rmi $(docker images --quiet --filter "dangling=true")
docker images -a | grep none | awk '{ print $3; }' | xargs docker rm

All the commands, in particular the first one, are also very helpful when used individually.

The first command removes all exited containers. The second command removes all dangling images. The third command removes those none:none images. These which could be intermediate images created by Docker while building another image which can be removed even if they do not occupy space in your disk, but definitely clutter your screen. However they could also be dangling images which need to be pruned as they have a relevant size. Dangling images are caused by re-building an image after the base image (FROM XXX) has changed.

Remember that Docker is lightweight, but it’s space requirements can’t be neglected if an intensive usage is combined with limited cleanup.

Use bash scripts to run Docker

build.sh

docker build -t IMAGE_NAME -f DOCKERFILE_NAME .
run.sh

docker run -it IMAGE_NAME bash 

I always recommend to have a couple of bash scripts associated with each Dockerfile, in order to simplify the most frequent operations. These scripts are very useful as they allow to not have to always remember which flags and options are required for each Docker image.

The above examples are very simplistic, but in the case of running a container with GUI access, as shown above, having a script will make your life much easier. I often use the --rm flag in the run.sh script, to make sure that my container are removed by default when I exit from them.

Scripts are also useful if you are sharing Dockerfiles with other people. This way they will be able to use them right away.

Allow docker to build and run ARM containers

This solves the standard_init_linux.go:211: exec user process caused "exec format error" error shown using ARM executables and instructions on x86_64.


sudo apt-get install qemu-user-static
sudo systemctl restart systemd-binfmt.service

If an image still shows problems, you can run the following commands in the directory where its Dockerfile is located.


mkdir qemu-user-static
cp /usr/bin/qemu-*-static qemu-user-static

Then add the following line to the Dockerfile immediately after the FROM XXX command.


FROM YOUR_ARM_BASE_IMAGE
COPY qemu-arm-static /usr/bin

Running ARM containers on x86_64 machines can be useful, for example as a quick solution to cross-compilation.