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.
- prerequisites
- this docker thing, what is it
- wtf did that docker run command do
- hello world is boring, let’s try busy box instead
- how did it know what to run
- right, how do I build my own image then
- ok ok, let’s build the image
- but it just exits
- why did the apt update && update install not run
- so this is cool, but how do i reach this web server
- ok the hype train is starting to make sense, but the default page sucks
- using add
- using a volume
- recap
- right right, but what can i do to a running container
- what now
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.
- https://hub.docker.com/editions/community/docker-ce-desktop-windows/
- https://hub.docker.com/editions/community/docker-ce-desktop-mac/
apt
/yum
/pacman
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 ...]
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.
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.
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
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 runsh
(which is why we need stdio) - Somehow all of that gets uploaded to DockerHub
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
.
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 withdebian:stable-slim
.- What commands to
RUN
to installnginx
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
... :)
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!
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 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?
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>
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!
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!
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
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!
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!