Creating your own container images
Overview
Teaching: 45 min
Exercises: 0 minQuestions
How can I make my own Docker images?
Objectives
Explain the purpose of a
Dockerfile
and show some simple examples.Demonstrate how to build a Docker image from a
Dockerfile
.Demonstrate how to upload (‘push’) your container images to the Docker Hub.
Explain how you can include files within Docker images when you build them.
Explain how you can access files on the Docker host from your Docker containers.
Introduction to Dockerfiles
We have seen use of others’ Docker container images. Now we move on to describe how you can create your own images.
The specification for how Docker should build images that you design is contained within a file named Dockerfile
.
In a shell window:
cd
to yourcontainer-playground
;- create a new directory named
my-greeting-spec
withincontainer-playground
; cd
into yourmy-greeting-spec
directory.
Within the new my-greeting-spec
directory, use your favourite editor to create a file named Dockerfile
that contains the following:
FROM alpine
RUN /bin/echo "Greetings from my newly minted container." > /root/my_message
CMD [ "/bin/cat", "/root/my_message" ]
Building your Docker image
Run the following command to build your Docker image, noting that the period at the end of the line is important (it means “this directory”), and has a space before it.
$ docker build -t my-greeting .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM alpine
latest: Pulling from library/alpine
6c40cc604d8e: Pull complete
Digest: sha256:b3dbf31b77fd99d9c08f780ce6f5282aba076d70a513a8be859d8d3a4d0c92b8
Status: Downloaded newer image for alpine:latest
---> caf27325b298
Step 2/3 : RUN /bin/echo "Greetings from my newly minted container." > /root/my_message
---> Running in 87417f8733c4
Removing intermediate container 87417f8733c4
---> bf1b7fab7caa
Step 3/3 : CMD [ "/bin/cat", "/root/my_message" ]
---> Running in 6c44e5e59d9b
Removing intermediate container 6c44e5e59d9b
---> a6a95e96d0b8
Successfully built a6a95e96d0b8
Successfully tagged my-greeting:latest
OK, now let’s test that that container does what it’s supposed to!
$ docker run my-greeting
Greetings from my newly minted container.
Now use your favourite editor to make a change to the message that’s contained within the Dockerfile
. As the plan is to share your container online, it’s best to ensure that your message is suitable for public viewing, and you may want to avoid including your name, credit card numbers, etc.
Rerun the docker build -t my-greeting .
and docker run my-greeting
commands to ensure that your image builds and that your containers function as expected.
While it may not look like you have achieved much, you have already effected the combination of a lightweight Linux operating system with your specification to run a given command that can operate reliably on macOS, Microsoft Windows, Linux and on the cloud!
Pushing your container images to the Docker Hub
Images that you release publicly can be stored on the Docker Hub for free.
Let’s “push” to your account on the Docker Hub the image that you configured to output your chosen message, in the previous section.
Note that so far, the image name my-greeting
was used locally to your computer. On the Docker Hub, the name if your container must be prefixed by your user name (otherwise there would be many clashes when different users try to share images with the same name!).
You will need to run two commands that are similar to the ones included below, except that you need to replace the instance of “dme26” on each line with your Docker Hub username (dme26 is my Docker Hub username!). A potential source of confusion is that you typically use your email address and not your login name to access the Docker Hub, however once authenticated your user ID is shown on the Docker Hub web pages.
$ docker tag my-greeting:latest dme26/my-greeting
$ docker push dme26/my-greeting
The push refers to repository [docker.io/dme26/my-greeting]
503e53e365f3: Mounted from library/alpine
latest: digest: sha256:1d599b3e195e282648a30719f159422165656781de420ccb6173465ac29d2b7a size: 528
In a web browser, open https://hub.docker.com, and on your user page you should now see your container listed, for anyone to use or build on.
Now we can remove the local copy and get back the version on docker hub when ever we would like.
$ docker image rm dme26/my-greeting
Untagged: dme26/my-greeting:latest
Untagged: dme26/my-greeting@sha256:1d599b3e195e282648a30719f159422165656781de420ccb6173465ac29d2b7a
Check your local images using docker images
and then pull down the dockerhub version.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
$ docker pull dme26/my-greeting
Using default tag: latest
latest: Pulling from dme26/my-greeting
Digest: sha256:1d599b3e195e282648a30719f159422165656781de420ccb6173465ac29d2b7a
Status: Downloaded newer image for sstevens/py-greeting:latest
Let’s take a look at our list of images now.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dme26/my-greeting latest 1d599b3e195e 42 minutes ago 143MB
Copying files into your containers at build time
Let’s rework our container to run some code written in the Python programming language.
You will need two shell windows. You can continue using the shell you’ve used so far in this lesson, let’s call that the “image building shell”. Let’s refer to the new shell window that you open as the “testing shell”.
Open the Dockerfile
you created above with your favourite editor, and change its contents to match the following:
FROM python:3-slim
WORKDIR /usr/src/app
COPY test.py .
CMD [ "python", "./test.py" ]
In your image building shell run the command to try to build your image (this will fail!):
$ docker build -t py-greeting .
Sending build context to Docker daemon 2.048kB
Step 1/4 : FROM python:3-slim
3: Pulling from library/python
741437d97401: Pull complete
34d8874714d7: Pull complete
0a108aa26679: Pull complete
7f0334c36886: Pull complete
65c95cb8b3be: Pull complete
9107d7193263: Pull complete
dd6f212ec984: Pull complete
43288b101abf: Pull complete
cbc666ab4a65: Pull complete
Digest: sha256:d510b850194fab564abc3dfd22e626fa0c4ab3ce81118dab08a084ed3dcd24ac
Status: Downloaded newer image for python:3-slim
---> 338b34a7555c
Step 2/4 : WORKDIR /usr/src/app
---> Running in 838063c90080
Removing intermediate container 838063c90080
---> c350d738dec6
Step 3/4 : COPY requirements.txt ./
COPY failed: stat /var/lib/docker/tmp/docker-builder409382412/test.py: no such file or directory
Our Dockerfile
made reference to a file on the host test.py
that should have been in the directory that contained our Dockerfile
. It was not present, so the building of the image failed. It is the COPY test.py .
command in the Dockerfile
that is trying to copy the file test.py
into the working directory of the current image building operation.
Using your favourite editor, create the file “test.py” in the same directory as your Dockerfile, with the following contents:
print("Hello world from Python")
Let’s test run the python script before we re-bulid the image.
$ python test.py
I got the following message printing but if you don’t have python installed, you likely got an error about python not being installed on your machine. This is one of the wonderful things about docker containers!
Hello world from Python
Now let’s re-run the command to build our image.
$ docker build -t py-greeting .
Sending build context to Docker daemon 3.072kB
Step 1/4 : FROM python:3-slim
---> 2fbd95050b66
Step 2/4 : WORKDIR /usr/src/app
---> Using cache
---> ff69cfaac5e4
Step 3/4 : COPY test.py .
---> 515d20c9d5e8
Step 4/4 : CMD [ "python", "./test.py" ]
---> Running in 5ce48493838d
Removing intermediate container 5ce48493838d
---> a1d96db7bc5a
Successfully built a1d96db7bc5a
Successfully tagged py-greeting:latest
In your testing shell, within your container-playground
directory, create a directory test
and cd
into it.
Test your container using the following command, which should produce the output shown below.
$ docker run py-greeting
Hello world from Python
Sharing files with your containers at run time
Having shown how to include files of your choice into your image as it was built, we now move to another important form of sharing: allowing your container instances to read and write files on the host computer.
Being able to share files between the host and the container allows you to build images that process input data sitting in files on the host, and write results back to files on the host.
Let’s create an image for a container that uses Python to read in a CSV file from the Docker host, and write results back as a file on the Docker host. (As usual, the container itself, will be cleaned away when it finishes.)
In the same directory as your Dockerfile, use your favourite editor to create a file named csv-to-scatter-plot.py
containing the following Python code:
# Libraries to include
import matplotlib.pyplot as the_plot
import numpy as np
# Read a CSV file "data.csv" shared to the Docker container from the Docker host.
# The header line is skipped, and typically contains a description of the columns:
# [ x-coordinate, y-coordinate, colour, size ]
# Each row of the CSV file (other than the header) defines one point to plot.
the_data = np.genfromtxt('/data/data.csv',delimiter=',',skip_header=1)
transposed_data = the_data.transpose()
points_x = transposed_data[0]
points_y = transposed_data[1]
points_colour = transposed_data[2]
points_size = transposed_data[3]
# This code is not at all robust, but skipping error handling makes it more brief.
# Define the scatter plot
the_plot.style.use('seaborn-whitegrid')
f = the_plot.figure()
the_plot.scatter(points_x, points_y, c=points_colour, s=points_size, alpha=0.4, cmap='viridis')
the_plot.colorbar()
# Save the scatter plot to two output files (on the Docker host).
f.savefig("/data/output.pdf", bbox_inches='tight')
f.savefig("/data/output.png", bbox_inches='tight')
Your Dockerfile will need to be changed to refer to this new script, as follows:
FROM python:3-slim
WORKDIR /usr/src/app
RUN pip install --no-cache-dir numpy matplotlib
COPY csv-to-scatter-plot.py .
CMD [ "python", "./csv-to-scatter-plot.py" ]
Now build an image named “csv-to-scatter-plot” using the following command, which is followed by the output that that command produces for me.
$ docker build -t csv-to-scatter-plot .
Sending build context to Docker daemon 28.16kB
Step 1/5 : FROM python:3-slim
---> 2fbd95050b66
Step 2/5 : WORKDIR /usr/src/app
---> Using cache
---> 8988c231167c
Step 3/5 : RUN pip install --no-cache-dir numpy matplotlib
---> Running in a50a7468e3e6
Collecting numpy
Downloading https://files.pythonhosted.org/packages/61/57/07c49e1a6d2706fb7336b3fb11dd285c1e96535c80833d7524f002f57086/numpy-1.16.1-cp37-cp37m-manylinux1_x86_64.whl (17.3MB)
Collecting matplotlib
Downloading https://files.pythonhosted.org/packages/e7/f9/5377596cb1c035c102396f5934237a046f80da69974026f90bee5db8b7ba/matplotlib-3.0.2-cp37-cp37m-manylinux1_x86_64.whl (12.9MB)
Collecting kiwisolver>=1.0.1 (from matplotlib)
Downloading https://files.pythonhosted.org/packages/5c/7e/d6cae2f241ba474a2665f24b480bf4e247036d63939dda2bbc4d2ee5069d/kiwisolver-1.0.1-cp37-cp37m-manylinux1_x86_64.whl (89kB)
Collecting pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 (from matplotlib)
Downloading https://files.pythonhosted.org/packages/de/0a/001be530836743d8be6c2d85069f46fecf84ac6c18c7f5fb8125ee11d854/pyparsing-2.3.1-py2.py3-none-any.whl (61kB)
Collecting cycler>=0.10 (from matplotlib)
Downloading https://files.pythonhosted.org/packages/f7/d2/e07d3ebb2bd7af696440ce7e754c59dd546ffe1bbe732c8ab68b9c834e61/cycler-0.10.0-py2.py3-none-any.whl
Collecting python-dateutil>=2.1 (from matplotlib)
Downloading https://files.pythonhosted.org/packages/41/17/c62faccbfbd163c7f57f3844689e3a78bae1f403648a6afb1d0866d87fbb/python_dateutil-2.8.0-py2.py3-none-any.whl (226kB)
Requirement already satisfied: setuptools in /usr/local/lib/python3.7/site-packages (from kiwisolver>=1.0.1->matplotlib) (40.6.3)
Collecting six (from cycler>=0.10->matplotlib)
Downloading https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Installing collected packages: numpy, kiwisolver, pyparsing, six, cycler, python-dateutil, matplotlib
Successfully installed cycler-0.10.0 kiwisolver-1.0.1 matplotlib-3.0.2 numpy-1.16.1 pyparsing-2.3.1 python-dateutil-2.8.0 six-1.12.0
Removing intermediate container a50a7468e3e6
---> 1c9b9f73d774
Step 4/5 : COPY csv-to-scatter-plot.py .
---> 573ed66d57d1
Step 5/5 : CMD [ "python", "./csv-to-scatter-plot.py" ]
---> Running in 59f899e4197e
Removing intermediate container 59f899e4197e
---> 5647faea67fd
Successfully built 5647faea67fd
Successfully tagged csv-to-scatter-plot:latest
Now in your testing shell, use your favourite editor to create a file named data.csv
that contains, for example:
x-coordinate,y-coordinate,colour,size
1.76405235,1.8831507,0.96193638,392.67567700
0.40015721,-1.34775906,0.29214753,956.40572300
0.97873798,-1.270485,0.2408288,187.13089200
2.2408932,0.96939671,0.10029394,903.98395500
1.86755799,-1.17312341,0.01642963,543.8059500
-0.9772779,1.94362119,0.92952932,456.91142200
0.95008842,-0.41361898,0.66991655,882.0414100
-0.15135721,-0.74745481,0.78515291,458.60396200
-0.10321885,1.92294203,0.28173011,724.16763700
0.41059850,1.48051479,0.58641017,399.02532200
0.14404357,1.86755896,0.06395527,904.04439300
1.45427351,0.90604466,0.48562760,690.0250200
0.76103773,-0.86122569,0.9774951,699.62205400
0.12167502,1.91006495,0.87650525,327.72040200
0.44386323,-0.26800337,0.33815895,756.77864300
0.33367433,0.80245640,0.96157016,636.06105500
1.49407907,0.94725197,0.23170163,240.02027300
-0.20515826,-0.15501009,0.94931882,160.53882200
0.31306770,0.6140794,0.94137771,796.39147500
-0.85409574,0.92220667,0.79920259,959.16660300
-2.55298982,0.37642553,0.63044794,458.13882700
The -v
switch to the docker run
command allows us to specify a mapping between a directory on the host, followed by a colon, then the place that directory should be mapped to within any container that is created. Note that the default if for the container to both be able to read from and write to the mapped directory on the host. (This is a potential security risk: you should only run containers from images for which you trust the provenance.)
In your testing shell, create an instance of your “csv-to-scatter-plot” container.
For macOS, Linux or PowerShell:
$ docker run -v "${PWD}:/data" csv-to-scatter-plot
For cmd.exe
shells on Microsoft Windows:
> docker run -v "%CD%":/data csv-to-scatter-plot
If all goes well, you should now see, within the working directory of your testing shell, a PNG and a PDF file that plot the data from data.csv
.
Change your data.csv
file, and rerun the appropriate preceding docker run
invocation.
You should see the PDF and PNG file update appropriately.
You have now successfully implemented an image that creates containers that transform input data through a stable, reproducible computational environment into output, in the form of plot images.
Creating an image interactively
Occasionally you might need to work interactively with an docker container. We will work interactively and learn how to create an image from the container. This method isn’t as reproducible as using a Dockerfile
but can be useful in some circumstances. For this example we will build an image with the git repo for these lessons in it.
First let’s run a docker container interactively. We can do this by using the -i
for interactive mod and -t
flag for to alocate a pseudo-TTY.
$ docker run -it ubuntu
Now in the image we can see a new different looking prompt.
root@747fcdb9727a:/#
Next we will update apt-get
(the package management system in ubuntu) and install git
# apt-get update
Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [109 kB]
Get:2 http://archive.ubuntu.com/ubuntu xenial InRelease [247 kB]
Get:5 http://security.ubuntu.com/ubuntu xenial-security/universe Sources [129 kB]
Get:6 http://security.ubuntu.com/ubuntu xenial-security/main amd64 Packages [827 kB]
Get:7 http://security.ubuntu.com/ubuntu xenial-security/restricted amd64 Packages [12.7 kB]
Get:8 http://security.ubuntu.com/ubuntu xenial-security/universe amd64 Packages [553 kB]
Get:9 http://security.ubuntu.com/ubuntu xenial-security/multiverse amd64 Packages [6113 B]
Get:3 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]
Get:4 http://archive.ubuntu.com/ubuntu xenial-backports InRelease [107 kB]
Get:10 http://archive.ubuntu.com/ubuntu xenial/universe Sources [9802 kB]
Get:11 http://archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1558 kB]
Get:12 http://archive.ubuntu.com/ubuntu xenial/restricted amd64 Packages [14.1 kB]
Get:13 http://archive.ubuntu.com/ubuntu xenial/universe amd64 Packages [9827 kB]
Get:14 http://archive.ubuntu.com/ubuntu xenial/multiverse amd64 Packages [176 kB]
Get:15 http://archive.ubuntu.com/ubuntu xenial-updates/universe Sources [320 kB]
Get:16 http://archive.ubuntu.com/ubuntu xenial-updates/main amd64 Packages [1236 kB]
Get:17 http://archive.ubuntu.com/ubuntu xenial-updates/restricted amd64 Packages [13.1 kB]
Get:18 http://archive.ubuntu.com/ubuntu xenial-updates/universe amd64 Packages [965 kB]
Get:19 http://archive.ubuntu.com/ubuntu xenial-updates/multiverse amd64 Packages [19.1 kB]
Get:20 http://archive.ubuntu.com/ubuntu xenial-backports/main amd64 Packages [7942 B]
Get:21 http://archive.ubuntu.com/ubuntu xenial-backports/universe amd64 Packages [8532 B]
Fetched 4637 kB in 4s (1153 kB/s)
Reading package lists... Done
# apt-get instal -y git-core
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
ca-certificates git git-man ifupdown iproute2 isc-dhcp-client isc-dhcp-common krb5-locales less libasn1-8-heimdal libatm1 libbsd0
libcurl3-gnutls libdns-export162 libedit2 liberror-perl libexpat1 libffi6 libgdbm3 libgmp10 libgnutls30 libgssapi-krb5-2 libgssapi3-heimdal
libhcrypto4-heimdal libheimbase1-heimdal libheimntlm0-heimdal libhogweed4 libhx509-5-heimdal libidn11 libisc-export160 libk5crypto3
libkeyutils1 libkrb5-26-heimdal libkrb5-3 libkrb5support0 libldap-2.4-2
...
TRUNCATED
# git --version
git version 2.7.4
Now we can exit the container and commit it to an image.
# exit
Once the image is close we can list it to find the name of that image, in this case adoring_euclid
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
747fcdb9727a ubuntu "/bin/bash" 2 minutes ago Exited (0) 24 seconds ago adoring_euclid
Now we can commit it (like a git repo) and give the new image a name.
$ docker commit -m "installing git" adoring_euclid ubuntu_w_git
sha256:5023d817090cc96298a7e7206cac5bedb7787ad103faefde395e77741db68638
Now let’s look at our images to see the new one we just created
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu_w_git latest 5023d817090c 2 minutes ago 249MB
ubuntu latest 2a4cca5ac898 16 months ago 111MB
We can also look at the history of our image using the docker history
command. The output of this command shows our commit message under the COMMENT
column.
$docker history ubuntu_w_git
IMAGE CREATED CREATED BY SIZE COMMENT
5023d817090c 4 minutes ago /bin/bash 137MB installing git
2a4cca5ac898 16 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 16 months ago /bin/sh -c mkdir -p /run/systemd && echo 'do… 7B
<missing> 16 months ago /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$… 2.76kB
<missing> 16 months ago /bin/sh -c rm -rf /var/lib/apt/lists/* 0B
<missing> 16 months ago /bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B
<missing> 16 months ago /bin/sh -c #(nop) ADD file:affda766655e01cbd… 111MB
Next lets reattach to that same image and add the repository. We could have done this before we exited but now we can learn how to restart and attach to an exited container.
First we need to restart the container in question using the docker start
command.
$ docker start adoring_euclid
adoring_euclid
$ docker attach adoring_euclid
Now inside the image we can clone the repository.
# git clone https://github.com/UW-Madison-DataScience/docker-introduction.git
Output not shown.
# exit
Finally we can commit this new version of the container as it’s own image.
$ docker commit -m "cloned docker-intro repo" adoring_euclid docker-intro-repo
sha256:e3ff0de14f37c76d849d89225123e28ad97385fdba8b8ec51fdbcdfaddb4ceb3
Now we can list our images and see we have the two images save from different states of the container.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-intro-repo latest e3ff0de14f37 43 seconds ago 254MB
ubuntu_w_git latest 5023d817090c 9 minutes ago 249MB
ubuntu latest 2a4cca5ac898 16 months ago 111MB
Key Points
Dockerfiles
specify what is within Docker images.The
docker build
command is used to build an image from aDockerfile
You can share your Docker images through the Docker Hub so that others can create Docker containers from your images.
You can include files from your Docker host into your Docker images by using the
COPY
instruction in yourDockerfile
.Docker allows containers to read and write files from the Docker host.