Super-simple Docker Tutorial

Russell Bateman
October 2016
last update:

Tutorial guided by Creating your first Docker image.

Every Docker container is an instance of a Docker image. Docker images are built from layers, starting with a base layer. A common base layer is Ubuntu; another is CentOS. Note that CentOS is what ELK stack Docker images begin with. This example uses Ubuntu because Debian and because the Debian base layer is much smaller than others.

Our super-simple demonstration!

Start a Docker container based on something called Ubuntu:latest image. :latest is called the image tag and refers to the latest version of Ubuntu bash layer.

If you don't have the image locally, which will be the case the first time run, it will download automatically. Here we go...

russ@nargothrond:~$ docker run --name my-redis -it ubuntu:latest bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
124c757242f8: Pull complete
9d866f8bde2a: Pull complete
fa3f2f277e67: Pull complete
398d32b153e8: Pull complete
afde35469481: Pull complete
Digest: sha256:de774a3145f7ca4f0bd144c7d4ffb2931e06634f11529653b23eba85aef8e378
Status: Downloaded newer image for ubuntu:latest
root@178ff3c44fc2:/#

Above, you see we started a Docker container...

At the end of this, we have a command prompt inside the Docker container (container doesn't mean it's invisible or that you can't see inside). We happen to be root, but, despite this awesome power, it's inside a container that protects the host we're running on—as well as any other containers running on our host—from ill effects including making mistakes using root and from what we'll do with root.

Since this demonstration uses Redis, we'll need a few things added into our container (which is Debian—Ubuntu). The first thing we need is a program called wget. To show that this container has nothing to do with the actual host, nargothrond, I'm running on, compare these two sessions. Throughout this tutorial, fluorescent green is the sign that we're doing something in our host operating environment.

nargothrond my-redis (178ff3c33fc2)
russ@nargothrond:~$ hostname
nargothrond
russ@nargothrond:~$ sudo bash
root@nargothrond:~# which wget
/usr/bin/wget
root@nargothrond:~# apt-get update
Hit:1 http://archive.canonical.com/ubuntu bionic InRelease
Ign:2 http://mirrors.xmission.com/linuxmint tara InRelease
Get:3 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Hit:4 http://mirrors.xmission.com/linuxmint tara Release
Ign:6 http://dl.google.com/linux/chrome/deb stable InRelease
Hit:7 http://dl.google.com/linux/chrome/deb stable Release
Hit:9 http://ubuntu.cs.utah.edu/ubuntu bionic InRelease
Get:10 http://ubuntu.cs.utah.edu/ubuntu bionic-updates InRelease [88.7 kB]
Get:11 http://ubuntu.cs.utah.edu/ubuntu bionic-backports InRelease [74.6 kB]
Fetched 247 kB in 2s (103 kB/s)
Reading package lists... Done
root@178ff3c44fc2:/# hostname
178ff3c44fc2
root@178ff3c44fc2:/# which wget
root@178ff3c44fc2:/# apt-get update
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Get:2 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
Get:3 http://security.ubuntu.com/ubuntu bionic-security/universe Sources [21.8 kB]
Get:4 http://security.ubuntu.com/ubuntu bionic-security/multiverse amd64 Packages [1364 B]
Get:5 http://security.ubuntu.com/ubuntu bionic-security/universe amd64 Packages [104 kB]
Get:6 http://security.ubuntu.com/ubuntu bionic-security/main amd64 Packages [229 kB]
Get:7 http://archive.ubuntu.com/ubuntu bionic-updates InRelease [88.7 kB]
Get:8 http://archive.ubuntu.com/ubuntu bionic-backports InRelease [74.6 kB]
Get:9 http://archive.ubuntu.com/ubuntu bionic/universe Sources [11.5 MB]
Get:10 http://archive.ubuntu.com/ubuntu bionic/multiverse amd64 Packages [186 kB]
Get:11 http://archive.ubuntu.com/ubuntu bionic/universe amd64 Packages [11.3 MB]
Get:12 http://archive.ubuntu.com/ubuntu bionic/restricted amd64 Packages [13.5 kB]
Get:13 http://archive.ubuntu.com/ubuntu bionic/main amd64 Packages [1344 kB]
Get:14 http://archive.ubuntu.com/ubuntu bionic-updates/universe Sources [115 kB]
Get:15 http://archive.ubuntu.com/ubuntu bionic-updates/restricted amd64 Packages [10.8 kB]
Get:16 http://archive.ubuntu.com/ubuntu bionic-updates/universe amd64 Packages [711 kB]
Get:17 http://archive.ubuntu.com/ubuntu bionic-updates/multiverse amd64 Packages [6161 B]
Get:18 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 Packages [519 kB]
Get:19 http://archive.ubuntu.com/ubuntu bionic-backports/universe amd64 Packages [2975 B]
Fetched 26.6 MB in 28s (936 kB/s)
Reading package lists... Done

So, in order to continue the tutorial, we'll have to get sofware—wget and other programs—for our container even though our host operating system already has that software:

root@178ff3c44fc2:/# apt-get install wget
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  ca-certificates libpsl5 libssl1.1 openssl publicsuffix
The following NEW packages will be installed:
  ca-certificates libpsl5 libssl1.1 openssl publicsuffix wget
0 upgraded, 6 newly installed, 0 to remove and 4 not upgraded.
Need to get 2266 kB of archives.
After this operation, 6341 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 libssl1.1 amd64 1.1.0g-2ubuntu4.1 [1128 kB]
.
.
.
Updating certificates in /etc/ssl/certs...
133 added, 0 removed; done.
Processing triggers for libc-bin (2.27-3ubuntu1) ...
Processing triggers for ca-certificates (20180409) ...
Updating certificates in /etc/ssl/certs...
0 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.

To boot, we need a few other things too (like Redis), so let's go get them. build-essential is a really huge pile of stuff, enough to build some serious Linux applications (make) including the ability to handle all sorts of stuff like locale and timezones. Here, we're going to download Redis source code and build it. (No, I didn't know Redis didn't have binary distributions either.) So, we'll build Redis, install it, then run it as a server, including reserving and opening a port, all activites that we might not want to undertake lightly were this sample software going to be unceremoniously dumped into our very reliable and productive operating system on which we daily depend!

root@178ff3c44fc2:/# apt-get install build-essential tcl8.5 -y
root@178ff3c44fc2:/# wget http://download.redis.io/releases/redis-stable.tar.gz
root@178ff3c44fc2:/# tar xzf redis-stable.tar.gz
root@178ff3c44fc2:/# cd redis-stable
root@178ff3c44fc2:/redis-stable# make
root@178ff3c44fc2:/redis-stable# make install
root@178ff3c44fc2:/redis-stable# ./utils/install_server.sh
Welcome to the redis service installer
This script will help you easily set up a running redis server

Please select the redis port for this instance: [6379]
Selecting default: 6379
.
.
.
Is this ok? Then press ENTER to go on or Ctrl-C to abort.
Copied /tmp/6379.conf => /etc/init.d/redis_6379
Installing service...
Success!
Starting Redis server...
Installation successful!
root@178ff3c44fc2:/redis-stable# service redis_6379 start
/var/run/redis_6379.pid exists, process is already running or crashed
root@178ff3c44fc2:/redis-stable# ps -ef | grep [r]edis
root      6858     1  0 14:57 ?        00:00:00 /usr/local/bin/redis-server 127.0.0.1:6379

Saving our image...

Now Redis is running in our Docker container, but let's assume that we're so happy about this that we want to save it off in order to use it elsewhere or even distribute it to friends for their use. Remember that we're layered upon the work of a nice container that our Ubuntu friends set up for us.

First, we exit our container—back to our host.

root@178ff3c44fc2:/redis-stable# exit
exit

Next, we make a list of all of our Docker containers, whether running or stopped. (Because I've been creating containers and experimenting, I had a big pile of them, but I've cut them down to just the one we care about here.)

russ@nargothrond:~$ docker ps -a
CONTAINER ID   IMAGE           COMMAND   CREATED             STATUS                      PORTS   NAMES
178ff3c44fc2   ubuntu:latest   "bash"    About an hour ago   Exited (0) 50 seconds ago           my-redis

We'll commit out container as an image (compiling its changes into an image).

russ@nargothrond:~$ docker commit -m "Added Redis" -a "Russell Bateman" my-redis windofkeltia/my-redis:latest

This means...

  1. We're committing this image; this feature of Docker is built upon the git version-control system (if you know what that is).
  2. We're commenting it to say what we did ("added Redis"). This might be helpful in recognizing a set of changes later.
  3. We're identifying ourselves (this is my name). If we're on a team, this might be helpful in tracking down the culprit who broke something.
  4. my-redis is the name of our container image.
  5. windofkeltia is (my) Docker Hub username. The repository where this image will be is Docker Hub. (It's like GitHub. I had to register this username formally on Docker Hub.)
  6. my-redis/latest is the image name and version.

Wrapping up details...

Because of how git works, we've committed to a local repository belonging to Docker. Our image won't go up to Docker Hub until we do this:

russ@nargothrond:~$ docker push

...which we may not want to do because this is only a small, mostly useless demonstration. (Again, if you know anything about git, you understand what's happening here.

A Docker image isn't the whole elephant that's created. In this case, the elephant is created one layer at a time. We've just added only a tiny layer amounting to a) installing wget so that we can copy down Redis as source code to build, b) building Redis and, finally, c) launching Redis as a service. What goes into our container image is all the cool stuff the Ubuntu guys did (the lion's share of what's in there) plus our special modifications.

Simply put, this way of building containers makes most container images very small things indeed to pull down and use.

However, this is very messy since your container is little more than a black box. For someone else to know what you've done would require documentation, which is very annoying and possibly inaccurate.

There is a better way.

Dockerfile, the right way to do this...

It's possible to specify the contents and procedures of a container in a formal, reproduceable way. You make statements in a prescribed fashion in a file named, Dockerfile (dockerfile is also recognized and accepted).

If what you create needs to run on, for example, a distinctly different Linux distribution, let's say CentOS instead of Ubuntu, then you absolutely do not want your image to be done based on Debian because the result would useless on Red Hat, SuSE and other Linuces. You should use a Dockerfile to specify images instead.

Here is the Dockerfile that describes the image we created earlier:

FROM        ubuntu:latest
ARG         DEBIAN_FRONTEND=noninteractive
RUN         apt-get update -y
RUN         apt-get install -y --no-install-recommends apt-utils
RUN         apt-get install -y wget
RUN         apt-get install -y build-essential tcl8.5
RUN         wget http://download.redis.io/releases/redis-stable.tar.gz
RUN         tar xzf redis-stable.tar.gz
RUN         cd redis-stable && make && make install
RUN         ./redis-stable/utils/install_server.sh
EXPOSE      6379
ENTRYPOINT  [ "redis-server" ]

Here's what's going on...

  1. FROM tells Docker which image to start from.
  2. ARG is used to set a Dockerfile environment variable, in this case, it signals that this process cannot be interactive. Measures to deal with that will be taken by Docker. For one thing, installing timezone software requires interaction; Docker finds suitable defaults.
  3. The RUN commands say what's got to happen to install software.
  4. EXPOSE informs Docker that the container is going to listen on a particular port. The result is never a port accessible from outside the container (you must do more than just this).
  5. ENTRYPOINT designates the command (or application) to be run once the container has been loaded from our image.
  6. You'll notice that, despite my insistance on this container being portable, Dockerfile still uses Ubuntu/Debian package management. This means that it's still built on Ubuntu not on CentOS. It will likely run on CentOS, but only because a) it remains atop the Ubuntu base layer and b) nothing else is going on between it and the host OS. If there were extensive cooperation between CentOS and our container, we might discover incompatibilities that keep it from running. Typically, there aren't extensive interactions of this sort.

Upon creating Dockerfile in a subdirectory, make that subdirectory your current working directory and then run Docker to build it. Then you can create a running container:

root@nargothrond:~/dev/docker-dev/redis$ vim dockerfile
(insert contents above)
root@nargothrond:~/dev/docker-dev/redis# docker build -t redis .
russ@nargothrond:~/dev/docker-dev/redis$ docker run -d -p 6379:6379 redis
ebeee42372a5029e4cbb8287bb9312540bad9db02e71c9e1b8fd407622f733e8
russ@nargothrond:~/dev/docker-dev/redis$ ps -ef | grep [d]ocker
root      2950  3800  0 17:05 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 6379 \
    -container-ip 172.17.0.2 -container-port 6379
root      2958  3826  0 17:05 ?        00:00:00 docker-containerd-shim -namespace moby \
    -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/ebeee42372a5029e4...07622f733e8 \
    -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd \
    -runtime-root /var/run/docker/runtime-runc
root      3800     1  0 Oct10 ?        00:07:20 /usr/bin/dockerd -H fd://
root      3826  3800  0 Oct10 ?        00:05:13 docker-containerd --config /var/run/docker/containerd/containerd.toml

At this point, the container is running as you can recognize by familiar numbers in the processor status command issued after launching: