Linux Containers and Docker
Mar 6, 2020
Containers have radically changed how computer systems and applications are built, managed, and deployed. At home I deploy a combination of containers and VMs for my own personal use.
While similar to virtualization, containers have several key differences. Each virtual machine created by a hypervisor has its own kernel and emulated hardware. Containers share the same kernel as the host and isolate application processes from the rest of the system. This isolation is possible with two key kernel features, Namespaces and Control Groups (Cgroups).
Namespaces segregate kernel resources from collections of processes. Resources may exist in multiple spaces. There are seven different kinds of Namespaces including mount points, process IDs, network stacks, memory, hostnames, user IDs, and Cgroups.
Cgroups allow for resource limiting, prioritizing, accounting, and control of resource usage (CPU, memory, disk I/O, network, etc.) of a collection of processes.
All the files necessary to run a container are provided in a distinct image. Containers have an ephemeral nature, being quickly and easily built, destroyed, and rebuilt from their base image. All of these properties of containers allow for increased portability, speed, and security compared to other deployment methods.
This is the first post in a multi-part container series. There are many different implementations of Linux containers and I thought I would first start with the low hanging fruit, Docker.
Docker has essentially become synonymous with containers. Docker debuted to the public in 2013 and rapidly grew to have over a one billion dollar valuation in 2015. Originally Docker used LXC as its execution environment (I will go into detail about LXC in a future post), but in 2014 Docker replaced LXC with its own component written in Go.
Docker consists of three different pieces:
- A server daemon, dockerd
- A REST API which specifies interfaces that programs can use to talk to the daemon and instruct it what to do
- A command line interface (CLI) client docker
# apt update && apt install -y docker.io
Note: The Docker package in Ubuntu 18.04, currently version 18.09.7, lags behind the current release, 19.03.07. If you would prefer a newer version of Docker, I would recommend using Ubuntu 19.10 as a base, which currently includes Docker version 19.03.2. Alternatively if you would like the absolute latest version of Docker you can set up Docker repositories and install from them.
The Docker daemon always runs as the root user. I find it convenient to add my user to the docker group so that sudo is not required when running Docker commands.
# usermod -aG docker $USER
Logging out and back in again in will be required for the changes to go into effect.
Verify Docker is working by firing up an Ubuntu container:
$ docker run -it ubuntu bash
This command is our first use of the Docker CLI. Run specifies that we want to create and run a container, -it specifies an interactive pseudo-TTY, ubuntu specifies the image we want to use, and finally bash specifies the command to run once the container has been created.
As bash was invoked we now have a root bash shell inside the new Ubuntu container. Go ahead and explore a bit. Almost all of the commands you can run in Ubuntu you can run in your Ubuntu container, except to minimize file size, many programs will not be installed by default. Once you’re done you can leave the container with the exit command.
To see what containers are currently running:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
No containers were returned because docker ps only returns running containers on the system and our new container was stopped on being exited. To see all containers on the system use:
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 6637b187fe0e ubuntu "bash" 12 minutes ago Exited (127) 11 minutes ago affectionate_shamir
We can access this container again with the start and exec commands. The container is referenced by its unique container ID (6637b187fe0e) or randomly generated name (in this example affectionate_shamir).
$ docker start affectionate_shamir affectionate_shamir $ docker exec -it 6637b187fe0e bash
To delete the container (the –force flag is needed for running containers):
$ docker rm --force affectionate_shamir affectionate_shamir
Similar syntax can be used to list and remove images
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu latest 72300a873c2c 12 days ago 64.2MB $ docker image rm ubuntu Untagged: ubuntu:latest Untagged: ubuntu@sha256:04d48df82c938587820d7b6006f5071dbbffceb7ca01d2814f81857c631d44df Deleted: sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c Deleted: sha256:d3991ad41f89923dac46b632e2b9869067e94fcdffa3ef56cd2d35b26dd9bce7 Deleted: sha256:2e533c5c9cc8936671e2012d79fc6ec6a3c8ed432aa81164289056c71ed5f539 Deleted: sha256:282c79e973cf51d330b99d2a90e6d25863388f66b1433ae5163ded929ea7e64b Deleted: sha256:cc4590d6a7187ce8879dd8ea931ffaa18bc52a1c1df702c9d538b2f0c927709d
Images and Docker Hub
Docker Hub is a service provided by Docker for finding and sharing container images. Docker Hub can be accessed from the web or the Docker CLI. Docker Hub pages often contain documentation and other helpful information.
Note: As with downloading and installing any software and on your system, there is potential for malicious code from Docker Hub. Exercize caution and generally stick to using official Docker images or images created by a project’s authors when possible.
Let’s download the official Nginx docker image to our system with the pull command:
$ docker pull nginx
If you scroll down to the Image Variants section we can see there are multiple OS base options. If you want a smaller image base for your nginx container then specify the Alpine image with a colon. This is one instance of an image tag.
$ docker pull nginx:alpine
Looking at our images again we can see that we can have multiple nginx images. Another thing to note was that when we pulled the nginx image, the default flag was latest.
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx alpine 377c0837328f 47 hours ago 19.7MB nginx latest 6678c7c2e56c 47 hours ago 127MB
If we navigate to the supported tags of the nginx Docker Hub page we can see there are even more tag options available. We can also use tags to specify a specific image version.
$ docker pull nginx:1.16
Flags and Web Servers
Next we’ll host a static html file with an nginx container to showcase other docker run flags and a common docker use case, as a web server. First let’s create simple html file to host:
$ cd ~/ && echo 'Hello world!' > index.html
Now this time we’ll run our nginx container in the background:
$ docker run -d --name nginx -p 8080:80 -v ~/index.html:/usr/share/nginx/html/index.html:ro nginx
Note: When issuing a docker run command, Docker will first try to find the image locally. If an image isn’t found, Docker will then search the Docker Hub. In this case a separate docker pull command isn’t necessary.
Let’s unpack these last few lines. First we created a html file in our user’s home directory. Then we started a new container using several new flags. The -d flag ran the container in detached mode, the –name nginx flag ran the container with the name nginx, the -p 8080:80 flag published port 80 on the container to port 8080 on the the host, and the -v ~/index.html:/usr/share/nginx/html/index.html:ro flag mounted the index.html file we created on the host system as a volume in the default nginx static content directory in the container in read only mode.
In a browser navigate to localhost:8080 or host_ip_address:8080 and you should find the Hello world! page we created earlier.
Note: We just touched the surface on the flags you can use with docker run. Check out the other options available by exploring the Docker man pages, e.g. man docker run.
The process of updating containers illustrates their ephemeral nature. First let’s imagine an updated version of the latest nginx image has been released. In order to update our container we must first stop and delete the container. We then pull the new version of the image and start a new instance of the container using our original docker run command.
$ docker stop nginx && docker rm nginx $ docker pull nginx $ docker run -d --name nginx -p 8080:80 -v ~/index.html:/usr/share/nginx/html/index.html:ro nginx
Note: the only data that will carry over into our updated container will be the data stored in volumes.
You can imagine keeping track of Docker run parameters and checking to see if each container needs updating could easily turn into a daunting and tedious task. Next time we’ll introduce a tool that makes Docker administration easy and straightforward, Docker Compose. In the mean time feel free to check out Docker’s great documentation to dive deeper into topics introduced today.