Skip to content

Instantly share code, notes, and snippets.

@leonjza
Last active January 30, 2023 10:55
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save leonjza/c4fb7c1b5949763f4878d1360bc951c3 to your computer and use it in GitHub Desktop.
Save leonjza/c4fb7c1b5949763f4878d1360bc951c3 to your computer and use it in GitHub Desktop.
Docker in an hour Workshop

docker-in-an-hour

@leonjza

Welcome to docker-in-an-hour! This is a "JIT" for docker, with many explanations being just enough to defend yourself. It is highly recommended that you go and at least Google some of the stuff here after doing the workshop. Read the official docs with real explanations.

toc

  1. prerequisites
  2. this docker thing, what is it
  3. wtf did that docker run command do
  4. hello world is boring, let’s try busy box instead
  5. how did it know what to run
  6. right, how do I build my own image then
  7. ok ok, let’s build the image
  8. but it just exits
  9. why did the apt update && update install not run
  10. so this is cool, but how do i reach this web server
  11. ok the hype train is starting to make sense, but the default page sucks
  12. using add
  13. using a volume
  14. recap
  15. right right, but what can i do to a running container
  16. what now

prerequisites

You must be able to successfully run docker run --rm hello-world. If that does not work, you are not ready!

"What do I need to get that to work?"

Depending on your OS/distro, you need to install docker. Many distro's package it as the docker.io package. On Windows and macOS, I suggest you install docker for Windows and docker for Mac. Stay away from docker-machine. Just trust me.

Once installed, the run command should work. If it doesn't, google more! ;)

$ docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:fc6a51919cfeb2e6763f62b6d9e8815acbf7cd2e476ea353743570610737b752
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

[... snip ...]

this docker thing, what is it

For starters, it is not a Virtual Machine. The best thing you can do is try and ignore what you know about VM's for this workshop and allow yourself to learn about containers as another form of isolation technology.

To compare the two technologies, you need to try and understand the following two statements:

  • Virtual Machines emulate hardware (lets ignore acceleration technologies for now).
  • Docker containers share your computer’s hardware.

Both of these technologies provide some form of software isolation, where docker is the lighter technology of the two.

wtf did that docker run command do

If this was the first time you ran anything docker on your computer, that docker run command just:

  • Pulled the latest hello-world image from Dockerhub to your computer. The image lives here.
  • Ran the latest, local hello-world image.
  • Exited when it was done.
  • Cleaned up the container after itself when it was done.

Let's look at that command again, makes sense now, right?

docker run         --rm                                     hello-world
#      run image.  remove the container when you are done.  run the hello-world image.

hello world is boring, let’s try busybox instead

While its common place to hello world when trying out a new programming language, it is a bit boring. Instead, let’s check out what it looks like if we were to run busybox in a docker container. You know how to do this now, right?

docker run --rm -ti busybox

Wait, what’s that -ti flag now!? With -ti we are basically asking docker to attach interactively to the container so that we can see stdout/stderr. Without it, the docker run command would just exit.

$ docker run --rm -ti busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
0669b0daf1fb: Pull complete
Digest: sha256:b26cd013274a657b86e706210ddd5cc1f82f50155791199d29b9e86e935ce135
Status: Downloaded newer image for busybox:latest
/ #

Play around! Checkout who you are, what your filesystem looks like and what network configuration you have. :D

how did it know what to run

By default, docker uses DockerHub as an image registry, but many others exist! You can even configure private registries.

The busybox image that was pulled from DockerHub, pulled the latest tag because we did not specify a version (this is the default). You can specify a version to use with a :<version> specification after the image name. It is possible to query registries like DockerHub for image versions using extra tools (or just plain curl), but you can also just use the web interface. The busybox container has versions going back four years, so you can run busybox version 1.24.0!

$ docker run --rm -ti busybox:1.24.0
Unable to find image 'busybox:1.24.0' locally
1.24.0: Pulling from library/busybox
Image docker.io/library/busybox:1.24.0 uses outdated schema1 manifest format. Please upgrade to a schema2 image for better future compatibility. More information at https://docs.docker.com/registry/spec/deprecated-schema-v1/
1b373b69cd34: Pull complete
a3ed95caeb02: Pull complete
Digest: sha256:fdc25416595aa8f71d2b92d6c5e3efbea3dc34557ce27078b606c9537b70327f
Status: Downloaded newer image for busybox:1.24.0
/ #

You can see which images you have downloaded locally on your computer with:

docker images

The output should be something like:

docker images
REPOSITORY                                     TAG                 IMAGE ID            CREATED             SIZE
hello-world                                    latest              fce289e99eb9        14 months ago       1.84kB
busybox                                        latest              83aa35aa1c79        18 hours ago        1.22MB
busybox                                        1.24.0              e4a4ff8a080d        4 years ago         1.11MB

That busybox image had to be built and uploaded to DockerHub. Some googling/clicking around DockerHub tags should reveal the repository with the source for the container. Some images on DockerHub include links to the source code.

Explore the busybox image repository here: https://github.com/docker-library/busybox/tree/9bb427f4dcafc97cf3f8b523fc8ed147d8bba7d8/uclibc.

We should learn from the repository that:

  • The container is based on scratch (more on this later)
  • The busybox.tar.xz program gets compiled in a container
  • The CMD directive is set to run sh (which is why we need stdio)
  • Somehow all of that gets uploaded to DockerHub

right, how do I build my own image then

There are many prebuilt containers on DockerHub. If you can imagine the software, chances are there is a container for it already. Just search! (try stuff like python, nginx, golang, etc)

Of course, a big part of docker is the fact that you can create your own images. Lets do that!

Building images really means you need to write a Dockerfile. All a Dockerfile file is is a set of commands that should be run to build the container. The commands themselves are also really simple. These Dockerfiles can be checked into version control and shared with others, granting everyone the ability to recreate complete environments from just a few simple text files.

Taking a look at the busybox Dockerfile as an example, we have

FROM scratch
ADD busybox.tar.xz /
CMD ["sh"]

Lets look at a commented version of that file.

# FROM sets the base image we want to use.
# 'scratch' is a special empty image, but this could be anything.
# You can build on top of other custom images
FROM scratch

# ADD adds files from outside of the container, in.
ADD busybox.tar.xz /

# CMD sets the command to run when we issue docker run for example
CMD ["sh"]

If you wanted to build this Dockerfile, you would issue the build command. The command typically looks like this:

docker build -t myimage .

What that is saying is:

  • Please build a new docker image
  • Give the image the tag myimage
  • Use . as the build context

The build context really just tells the build command where the Dockerfile lives. This will also be the root path used for directives like ADD.

ok ok, lets build the image

Cool, so lets build a dead simple nginx container. The way we are going to build it wont really be the same as the official nginx container on DockerHub for various reasons, but just stick it out for a bit.

We know we need a Dockerfile, so in a new, empty folder, start a Dockerfile in a text editor. We need to specify a few things:

  • FROM which base image are we going build on? Let's choose a slim Debian image with debian:stable-slim.
  • What commands to RUN to install nginx for us.
  • Finally, what CMD to invoke when we run the container.

Your turn: Build the Dockerfile and build the image from it.

The resultant Dockerfile should look something like this:

FROM debian:stable-slim

RUN apt update && \
    apt install -y nginx

CMD nginx

Building the image from your Dockerfile can be done with:

docker build -t diah:nginx .

The end of the build process should show something like:

Removing intermediate container 35eb79a02ac0
 ---> 0ad48ff2832b
Successfully built 0ad48ff2832b
Successfully tagged diah:nginx

You can now run the container (update the tag with the one you used!):

docker run --rm -ti diah:nginx

... :)

but it just exits

Yeah, about that. We are not running nginx in the foreground. Instead, nginx by default runs in the background.

Typically, your container should have a single responsibility. In other words, an nginx container should just run an nginx daemon. Even though hacks exist to run multiple processes as daemons, this is not best practice.

A container will only stay alive for as long as a process is running in the foreground. Once a process exits, the container exits. This will happen regardless if a process is still running in the background of a container. So, we need to reconfigure nginx to run in the foreground!

To run nginx in the foreground, you can set a config option at start up with nginx -g 'daemon off;'. Update your Dockerfile to run this CMD instead.

Your turn: Rebuild the Dockerfile with the updated command and run it.

The new Dockerfile should now look something like this:

FROM debian:stable-slim

RUN apt update && \
    apt install -y nginx

CMD nginx -g 'daemon off;'

When you run it now, the docker run should not immediately exit!

why did the apt update && update install not run

Noticed how the second rebuild was... much faster? Yay caching!

When you build a Dockerfile, the first thing that will happen is the base image used in the FROM statement will be downloaded and stored locally on your computer. Every other container that uses that same image in a FROM statement can just reuse that.

The next magical thing is that every line in a Dockerfile will get cached, referred to as a "layer". When you ran the apt update... line and it completed, the layer got cached so that in future it can simply be applied to the next build. Only if that line (or any line before it changes), will it be run again. Pause for blown minds to settle...

In our case, we simply edited the CMD line, meaning that was the only layer that needed rebuilding.

Bazinga.

so this is cool, but how do i reach this web server

So far we have a web server without a way to reach it. By default, nginx listens on TCP port 80, but we need to tell docker that.

This is where the -p flag comes in. When you issue a docker run command, you need to tell docker what the external port is to listen on, and then where to forward an incoming connection to inside the container. For example:

-p 8088:80

This tells docker to forward incoming connections to the docker host on TCP port 8088 to port 80 in the docker container we are about to start. A full example would therefore be:

docker run --rm -ti -p 8088:80 diah:nginx

In another shell you should now be able to reach the container's web server:

$ curl localhost:8088
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

[... snip ...]

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Pretty cool huh?

ok the hype train is starting to make sense, but the default page sucks

Our web server container thus far is simply serving the default pages that come with nginx. Which isn't very exciting. Let's fix that.

Before we get ahead of ourselves, let’s consider another concept about containers.
Containers should be as ephemeral as possible. This means, a container should be able to be destroyed and recreated with as minimal setup as possible.

With that in mind, we have two options to add content to our web server to serve:

  • Use an ADD directive to add the source code for our website at build time.
  • Use a VOLUME mount point to load content at runtime.

Choosing the best option really depends on the use case. We will play with both.

First, lets prepare a simple HTML page that will be served by nginx and save it as index.html next to our Dockerfile.

<html>
   <head></head>
   <body>
      <marquee>1986 baby yeah!</marquee>
   </body>
</html>

using add

To add our web site to the container that we have built, we can add an ADD directive to add the file from the outside of the container (aka: our host), to the inside of the container at a specific path. The path we will choose will be the document root for nginx.

For example:

ADD index.html /var/www/html

Add this line before the CMD directive, rebuild and test your container.

Your turn: Add your HTML page, rebuild the container, run it and test!

using a volume

The ADD method kinda makes this container very specific to our use case. In many cases this won’t be ideal. What if we wanted to have a more generic container (much like the official nginx container on DockerHub)?

One options is to configure a volume to mount at runtime. Instead of the ADD directive explicitly adding file to the final image, replace the line with a VOLUME directive. For example:

VOLUME /var/www/html

Next, we need to change how we issue the docker run command a bit. As a test, run the container like you did in the previous example, and checkout what page nginx serves. The default page again!

Now, add the -v flag. Much like the -p flag, the -v flag takes a value that specifies which local directory should be mounted into which container directory. For example:

-v /home/leon/files:/var/www/html

This would take files from my local home directory and mount it to the /var/www/html directory in the container. If we wanted to mount a directory that has our web page in this example, we would do something like:

docker run --rm -p 8088:80 -v $(pwd):/var/www/html diah:nginx

Now we have a much more generic container that could be re-used in different ways.

Your turn: Configure the volume, mount it at runtime and test!

recap

So far, you have built your own docker image and ran it as a container. The container was a web server, serving a custom website.

We learnt that containers should ideally run a single process and be built to be ephemeral. We also learnt how to improve our image in a way that makes it more reusable and not specific to a project.

Not bad :D

right right, but what can i do to a running container

Excellent question! Let's take a look. Keep your web server container running, and run the docker ps command. The output should look something like this:

docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                                                      NAMES
6bd46f7adeb2        diah:nginx                "/bin/sh -c 'nginx -…"   2 seconds ago       Up 2 seconds        0.0.0.0:8088->80/tcp                                       peaceful_goldberg

With docker ps we can see all of the currently running container that we have. When you add the -a flag, you will also see containers that were started, but weren't removed. This typically happens when you don't specify the --rm command when you issue a docker run.

Now, notice the NAME field in the docker ps output? In my case, the name was peaceful_goldberg. Unless you specify the --name flag when you docker run, this name will be autogenerated and random.

An exec command exists that lets you execute commands inside of a container. For example:

docker exec peaceful_goldberg whoami

This command will issue the whoami command inside of the peaceful_goldberg container. But that is not all you can do! What about an interactive shell? Well, that is possible too, with a slight modification.

docker exec -it peaceful_goldberg /bin/bash

The main difference this time is we added the -it flag (basically saying we want an interactive terminal), and issued the /bin/bash command. You should now have a shell inside a container. Checkout /var/www/html, you should see your custom website we added there!

what now

If none of that made sense, maybe give this resource a go to get to grips with it?

Hopefully you are a lot less scared of docker now, and you are ready to dive into more advanced topics!
With these basics sorted, I suggest you checkout topics like:

  • Docker volumes (note: this is not the same as the VOLUME directive!)
  • Docker networks (note: this is not the same as the -p flag!)
  • Multi-stage builds
  • Orchestrating multiple container with docker-compose
  • Kubernetes!

Last but not least, read as many Dockerfiles as you can! I have personally learnt many tricks this way!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment