I've been exposed to docker on and off and every time I see it, I seem to need a refresher. In this article we will go through everything you need to know about Docker in order to either jump into an existing project or get started with it.
Basic concepts
Docker is basically a system of running processes on the host machine in an isolated way, using several Linux kernel features. Thus, Docker is more lightweight than a full-blown virtual machine. The disadvantage of a Docker container vs. a virtual machine is that multiple containers share the same underlying OS kernel. While the concept of jailed processes is not new, Docker's popularity was essentially due to the tooling that it provided to which made it really straightforward to spin up and manage containers.
Docker is made up of various components. The main component is the the docker engine, which consists of a lightweight runtime that manages containers, images, builds, and more. It runs natively on Linux systems and is made up of:
- Docker daemon that runs on the host machine.
- Docker client that communicates with the Docker daemon to execute commands.
- A REST API for interacting with the Docker daemon remotely.
The Docker client is what you, as the end-user use to communicate with the Docker daemon, e.g.
docker run hello-world
.
The Docker daemon is what actually executes commands like building and running containers on the host machine. The Docker Client can run on the same machine as well, but it does not have to. It can also communicate with the Docker Daemon running on a different host.
We will look at other Docker components like the Docker hub, etc. later in this article.
Images, containers and volumes
A Docker image can be though of as a recipe for setting up a machine with all required software and dependencies
installed. Apart from installing software, images can also define what processes to run when launched. Docker images
are created via instructions written in a Dockerfile
. Images are built on the concept of layers. There is
always a base layer, potentially followed by additional layers that represent file changes. Each layer is stacked on
top of the others, consisting of the differences between it and the previous layer. This is achieved via a
Union file system.
A Docker container is the running instance of an image. This includes the operating system, application code, runtime, system tools, system libraries, etc. A Docker image can be thought of as an executable and a container can be thought of as the running application. Note that in this analogy each running application is its own instance and independent of the others.
The general idea is that once you have successfully created a container, you can then run it in any environment without having to make changes.
A Docker volume is the "data" part of a container, initialized when a container is created. Volumes allow you to persist and share a container's data. Docker volumes are separate from the default Union File System and exist as normal directories and files on the host filesystem.
Docker comands
As part of the docker installation process, a hello world image was downloaded and executed via:
docker run hello-world
List all images with docker images
:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest 94e814e2efa8 2 weeks ago 88.9MB
hello-world latest fce289e99eb9 2 months ago 1.84kB
Run a command interactively from an image in a new container: docker run -it ubuntu bash
List all running containers: docker ps
. To list all previously run containers use
docker ps -a
:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d2d651741317 ubuntu "bash" 45 minutes ago Exited (0) 43 minutes ago suspicious_kalam
36813bdb6434 ubuntu "bash" About an hour ago Exited (0) About an hour ago inspiring_banach
46f352a0cde9 hello-world "/hello" 2 hours ago Exited (0) 2 hours ago thirsty_clarke
Note that in the output above, for each docker run
command, a new container was created. As mentioned
previously, each container has it's own data volume and changes to one do not affect the others. To run an existing
container: docker run [container-name
. This will start the container and
docker attach [container-name]
will jump into it.
At this point, you should have enough to get started with an existing docker project. Read on if you're looking to develop with docker.
Custom images
As noted previously, Docker images are specified via a Dockerfile
. Here's an extremely basic example that
uses a ubuntu base image and copies an executable called sysinfo
from the current directory into the
container and executes it:
FROM ubuntu:18.04
COPY sysinfo /
CMD ["/sysinfo"]
Let's see how we can get this image up and running via the docker build
command (note that gcc is
required to compile the binary):
$ cd ~
$ mkdir -p docker/sysinfo
$ cd docker/sysinfo
$ vim sysinfo.cpp
#include <iostream>
#include <sys/utsname.h>
using namespace std;
int main() {
struct utsname sysinfo;
uname(&sysinfo);
cout << "System Name: " << sysinfo.sysname << endl;
cout << "Host Name: " << sysinfo.nodename << endl;
cout << "Release(Kernel) Version: " << sysinfo.release << endl;
cout << "Kernel Build Timestamp: " << sysinfo.version << endl;
cout << "Machine Arch: " << sysinfo.machine << endl;
cout << "Domain Name: " << sysinfo.domainname << endl;
return 0;
}
$ g++ sysinfo.cpp -o sysinfo
$ ./sysinfo
System Name: Linux
Host Name: coolbeans
Release(Kernel) Version: 4.18.0-16-generic
Kernel Build Timestamp: #17-Ubuntu SMP Fri Feb 8 00:06:57 UTC 2019
Machine Arch: x86_64
Domain Name: (none)
$ vim Dockerfile
FROM ubuntu:18.04
COPY sysinfo /
CMD ["/sysinfo"]
$ docker build . -t sysinfo
$ docker run sysinfo
System Name: Linux
Host Name: d8e53b009d72
Release(Kernel) Version: 4.18.0-16-generic
Kernel Build Timestamp: #17-Ubuntu SMP Fri Feb 8 00:06:57 UTC 2019
Machine Arch: x86_64
Domain Name: (none)
Note in the difference in hostname between local system (coolbeans) and the running container (d8e53b009d72) in the above output.
If you make a mistake, you can remove an image via docker rmi [image-name] --force
. Cleaning up unused
containers and volumes related to the image can be accomplished via docker system prune --volumes
.
In the above example, we created a custom image using the standard Ubuntu image as our base image, before we go further with creating custom images it would be good to note that the docker hub provides lots of free pre-configured images for various software. This is the second Docker component and is also sometimes called the Docker registry (one can also have private registries).
Networking
In most cases, we would like to run a service via Docker. Let is look at how we can accomplish this by using a very simple web server as an example. Create the image as follows (note that the example below uses Go to create the binary):
$ cd ~
$ mkdir -p docker/webapp
$ cd docker/webapp
$ vim webapp.go
package main
import (
"io"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello from webapp!")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe(":8000", nil)
}
$ go build webapp.go
$ vim Dockerfile
FROM ubuntu:18.04
COPY webapp /
CMD ["/webapp"]
$ docker build . -t webapp
$ docker run -d webapp
cfab907c828a40ce4cc53b88b26badabf8fa6672fd538d0c072fd0947f36d650
In the above example we built a webapp image and started the docker container with the -d
flag. This
started the container in detached mode and printed the container id so that we could interact with it. We can confirm
it is running via docker ps
:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cfab907c828a webapp "/webapp" 4 minutes ago Up 4 minutes zealous_herschel
At this point we have our web application running in a docker container but we have no way to communicate with it. Run
docker inspect cfab907c828a
to output the container configuration in json format. We are interested in
the NetworkSettings.Networks.bridge.IPAddress
property. Let's try connecting to the provided ip address,
http://172.17.0.2:8000 (in my case) and we can see our web application in action!
It is also possible to bind ports on from the docker container to the host machine so that we can access services as
if they were running locally, docker run -d -p3000:8000 webapp
. Thus our web application is now available
on http://localhost:3000!
Persistent storage
By default Docker containers come with their own storage which lives as long as the container is running. If we would like to persist data across containers, we can either bind a local file/directory to our container or create and mount a named Docker volume. The added benefit of using a Docker volume is that it does not necessarily have to be a resource on the host file system, it can also be an external cloud storage service depending upon the driver.
A very practical example of using a postgres docker image with persistent data storage can be found here.
Summary
We looked at Docker basic concepts, created a few containers, ran some services and even persisted data across machine restarts! This was longer than 10 minutes but it should be enough to get going with Docker.