Exercise 1: Running images

Objective

Learn how to run images, either one-off or as a running service

Terminology

In docker-speak, an image is a file (or set of files) that contain the application you want to run. You can copy images around, upload them, download them etc.

A container is a process that is started from an image. You can use the same image to run multiple containers, in the same way that you can use the same executable with many different arguments.

So, an image corresponds to files, a container corresponds to processes.

Running a simple command in a container

Docker images have a name and a tag. The default for the tag is ‘latest’, and can be omitted. If you ask docker to run an image that is not present on your system, it will download it from hub.docker.com first, then run it.

Most Linux distributions have pre-built images available on dockerhub, so you can readily find something to get you started. Let’s start with the official Ubuntu linux image, and run a simple ‘hello world’. The docker run command takes options first, then the image name, then the command and arguments to run follow it on the command line:

> docker run ubuntu /bin/echo 'hello world'
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
af49a5ceb2a5: Pull complete 
8f9757b472e7: Pull complete 
e931b117db38: Pull complete 
47b5e16c0811: Pull complete 
9332eaf1a55b: Pull complete 
Digest: sha256:3b64c309deae7ab0f7dbdd42b6b326261ccd6261da5d88396439353162703fb5
Status: Downloaded newer image for ubuntu:latest
hello world

Note docker uses the ‘ubuntu:latest’ tag, since we didn’t specify what version we want.

If we run the same command again, docker will find the image cached on our local disk, so it will run much faster:

> docker run ubuntu /bin/echo 'hello world'
hello world

Running an interactive command in an image

To do something more exciting, you might want an interactive shell, so you can poke around and do stuff. That’s easy:

> docker run -t -i ubuntu /bin/bash
root@c69d6f8d89bd:/# id
uid=0(root) gid=0(root) groups=0(root)
root@c69d6f8d89bd:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
> exit # or hit control-D

The -t -i options make sure we can attach a terminal to the container, and we tell it to run our favourite shell as the application.

As you can see, you have root access in your container, and you are in what looks like a normal linux system. Now you can do whatever you like, e.g. install software and develop applications, all within the container of your choice.

This is a useful way of practicing and debugging the build process for an application while building a Dockerfile. Start with the base image, enter it with a shell, and learn what commands you need to run to build your package. Then capture those commands in the Dockerfile.

When the container environment gets messy, perhaps because you’ve installed stuff you didn’t really want, you quit the container, build your image with what you’ve learned so far, and start again, from that image. This way, you progress step by step to a working image. Then, you optimise it!

Making changes to a container

In an interactive shell in a container, you can change the container contents. But the changes do not persist once you exit the container. If you re-run the image, you get a new container, not a re-run of the one you modified. Let’s create a file in /tmp, then exit and restart the image, and look for the file:

> docker run -t -i ubuntu /bin/bash
root@1688f55c3418:/# touch /tmp/a-file.txt
root@1688f55c3418:/# ls -l /tmp/a-file.txt 
-rw-r--r-- 1 root root 0 Nov 30 17:50 /tmp/a-file.txt
root@1688f55c3418:/# exit
exit
> docker run -t -i ubuntu /bin/bash
root@97b1e86df1f1:/# ls -l /tmp
total 0
root@97b1e86df1f1:/# exit

There are, of course, ways to make persisitent changes to a container. More on that later.

So, can you get back to a container you modified before? Yes, actually, you can! Once a container exits docker keeps it cached for a while, so you can recover it. The docker ps command will tell you which containers are running now, and if you give it the --all flag, it tells you about containers that have exited, but still exist in the cache:

> docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
> docker ps --all
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
97b1e86df1f1        ubuntu              "/bin/bash"              5 minutes ago       Exited (0) 5 minutes ago                        backstabbing_brown
1688f55c3418        ubuntu              "/bin/bash"              5 minutes ago       Exited (0) 5 minutes ago                        ecstatic_hugle
c69d6f8d89bd        ubuntu              "/bin/bash"              9 minutes ago       Exited (0) 9 minutes ago                        sad_keller
960588723c36        ubuntu              "/bin/echo 'hello wor"   13 minutes ago      Exited (0) 13 minutes ago                       suspicious_mestorf
ffbb0f60bda6        ubuntu              "/bin/echo 'hello wor"   19 minutes ago      Exited (0) 19 minutes ago                       mad_booth

N.B. the container names, such as sad_keller above, are made up by docker at random. Use --name xyz to set the name explicitly yourself.

N.B. Since you’re using the same virtual machine for the docker and kubernetes exercises, if someone has already started the kubernetes system there may be a lot of containers running. Filter them out with grep: docker ps --all | grep -v k8s

we know the container we modified was the one before last, with ID 1688f55c3418. We can start it again, then attach a terminal to it, and look for the file we created in /tmp:

> docker start 1688f55c3418
1688f55c3418
> docker attach 1688f55c3418
root@1688f55c3418:/# ls -l /tmp
total 0
-rw-r--r-- 1 root root 0 Nov 30 17:50 a-file.txt

In general, you don’t want to do this much, it’s messy if you run lots of containers. But it’s useful to know, just in case you need it.

If you know you don’t want to re-start a container afer you’ve run it, you can tell docker to clean it up automatically when it exits, with the --rm flag. E.g.:

> docker run --rm ubuntu echo 'hello world'

Starting a long-running service in a container

Containers are useful for running services, like web-servers etc. Many come packaged from the developers, so you can start one easily, but first you need to find the one you want to run. You can either search on hub.docker.com, or you can use the docker search command. Nginx is a popular web-server, let’s look for that:

> docker search nginx
NAME                      DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                     Official build of Nginx.                        4719      [OK]       
jwilder/nginx-proxy       Automated Nginx reverse proxy for docker c...   877                  [OK]
richarvey/nginx-php-fpm   Container running Nginx + PHP-FPM capable ...   311                  [OK]
million12/nginx-php       Nginx + PHP-FPM 5.5, 5.6, 7.0 (NG), CentOS...   76                   [OK]
webdevops/php-nginx       Nginx with PHP-FPM                              63                   [OK]
maxexcloo/nginx-php       Framework container with nginx and PHP-FPM...   58                   [OK]
bitnami/nginx             Bitnami nginx Docker Image                      20                   [OK]
gists/nginx               Nginx on Alpine                                 8                    [OK]
evild/alpine-nginx        Minimalistic Docker image with Nginx            8                    [OK]
million12/nginx           Nginx: extensible, nicely tuned for better...   8                    [OK]
maxexcloo/nginx           Framework container with nginx installed.       7                    [OK]
webdevops/nginx           Nginx container                                 7                    [OK]
1science/nginx            Nginx Docker images based on Alpine Linux       4                    [OK]
ixbox/nginx               Nginx on Alpine Linux.                          3                    [OK]
drupaldocker/nginx        NGINX for Drupal                                3                    [OK]
yfix/nginx                Yfix own build of the nginx-extras package      2                    [OK]
frekele/nginx             docker run --rm --name nginx -p 80:80 -p 4...   2                    [OK]
servivum/nginx            Nginx Docker Image with Useful Tools            2                    [OK]
dock0/nginx               Arch container running nginx                    2                    [OK]
blacklabelops/nginx       Dockerized Nginx Reverse Proxy Server.          2                    [OK]
xataz/nginx               Light nginx image                               2                    [OK]
radial/nginx              Spoke container for Nginx, a high performa...   1                    [OK]
tozd/nginx                Dockerized nginx.                               1                    [OK]
c4tech/nginx              Several nginx images for web applications.      0                    [OK]
unblibraries/nginx        Baseline non-PHP nginx container                0                    [OK]

The official build of Nginx seems to be very popular, let’s go with that:

> docker run -p 8080:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
386a066cd84a: Pull complete 
386dc9762af9: Pull complete 
d685e39ac8a4: Pull complete 
Digest: sha256:3861a20a81e4ba699859fe0724dc6afb2ce82d21cd1ddc27fff6ec76e4c2824e
Status: Downloaded newer image for nginx:latest

Note the -p 8080:80 option. That tells docker to map port 80 in the container to port 8080 on the host, so you can communicate with it.

Note also that we didn’t tell docker what program to run, that’s baked into the container in this case. More on that later.

The next step depends on if you’re running on your own laptop, or on a remote virtual machine.

  • If you’re running docker on your laptop, go to your browser and enter localhost:8080 in the address bar, you should see a page with a Welcome to nginx! message.
  • If you’re running docker on a remote virtual machine, create another terminal session and SSH into the machine again, and run curl -s http://localhost:8080 | grep Welcome. You’ll see the source-code, but also the Welcome to nginx! embedded in there

On your terminal where you ran the docker command, you’ll see some log information:

172.17.0.1 - - [30/Nov/2016:18:07:59 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:49.0) Gecko/20100101 Firefox/49.0" "-"
2016/11/30 18:07:59 [error] 7#7: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "localhost:8080"
172.17.0.1 - - [30/Nov/2016:18:07:59 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:49.0) Gecko/20100101 Firefox/49.0" "-"
172.17.0.1 - - [30/Nov/2016:18:07:59 +0000] "GET /favicon.ico HTTP/1.1" 404 169 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:49.0) Gecko/20100101 Firefox/49.0" "-"
2016/11/30 18:07:59 [error] 7#7: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "localhost:8080"

That’s a good start, but you now have a terminal tied up with nginx, and if you hit CTRL-C in that terminal, your web-server dies. We can run it in the background instead:

> docker run --detach -p 8080:80 nginx
48a2dca14407484ca4e7f564d6e8c226d8fdd8441e5196577b2942383b251106

Go back to your browser or command line, reload localhost:8080, and you should get the page loaded again.

That nice long hexadecimal string is the container-ID, which we can use to get access to its logfiles with the docker logs command:

> docker logs --follow 48a2dca14407484ca4e7f564d6e8c226d8fdd8441e5196577b2942383b251106
172.17.0.1 - - [30/Nov/2016:18:18:40 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:49.0) Gecko/20100101 Firefox/49.0" "-"

If you hit CTRL-C now, your container is still running, in the background. You can see this with the docker ps command:

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                           NAMES
48a2dca14407        nginx               "nginx -g 'daemon off"   3 minutes ago       Up 3 minutes        443/tcp, 0.0.0.0:8080->80/tcp   pensive_booth

You can open a shell into the running container, if you wish. This can be handy for debugging. Let’s take a look at the processes running in our nginx server. We have to install the procps package first, because the official nginx container is very lightweight, and doesn’t have that in it by default:

> docker exec -t -i pensive_booth /bin/bash
root@48a2dca14407:/# apt-get update -y && apt-get install -y procps
root@48a2dca14407:/# ps auxww | grep ngin                                                                                                                
root         1  0.0  0.0  31764  5164 ?        Ss   18:58   0:00 nginx: master process nginx -g daemon off;
nginx        7  0.0  0.0  32156  2900 ?        S    18:58   0:00 nginx: worker process
root        15  0.0  0.0  11128  1036 ?        S+   18:59   0:00 grep ngin

Note that although you’ve installed the procps package in the container while it was running, we haven’t installed it in the image, only into this running instance of the image. So if you kill this container and start it again, it won’t have the procps package installed.

So now you’ve started nginx, how do you stop it? Use the docker stop command! You can give it either the container-ID or the name. You’ll notice docker has made up a name for you, pensive_booth in this case. You may have to scroll the window below to the right so you can see it, depending on your browser.

> docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                           NAMES
48a2dca14407        nginx               "nginx -g 'daemon off"   3 minutes ago       Up 3 minutes        443/tcp, 0.0.0.0:8080->80/tcp   pensive_booth

> docker stop pensive_booth
pensive_booth

> docker ps # check that it's gone
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Conclusion

You now know how to find a container that you want to run. You can start it, re-start it, run it as a detached service, attach a terminal to it for debugging, view the logs externally and even map ports between it and your host machine.

Best practices

  • avoid making too many interactive changes to containers, see later exercises for how to modify and build your own containers
  • prefer official images over those built by third-parties. Docker runs with privileges, so you have to be a bit careful what you run.