Skip to content

WorkBook

Alec Clews edited this page Nov 1, 2023 · 4 revisions

Docker Workbook

Publication Date: 01-Nov-2023

You can find up to date versions of this workbook and supporting material at https://github.com/dockerfornovices/DockerSimpleDemo

A copy of this workbook is also available on Google Drive. Feel free to make a copy and add your own notes.

https://drive.google.com/file/d/1LzB3hn4OecuNX0cJWKQ0nxKwkSxWOdJL/view?usp=sharing

All of the Docker images used in this workbook have been designed to be as small as possible so that they will be useful on congested or slow networks.

Introduction

This workbook is written for people attending the related “Docker for Novices” workshop. It may be useful as a stand-alone resource, but it will be useful to review the slides as well.

You will need a working Docker environment. Follow the instructions in the SetUp section.

NOTE: These examples do not show all the options or uses of Docker, and the explanations are not exhaustive. You should have the Docker documentation handy as you try to understand what is happening. For example https://docs.docker.com/engine/reference/commandline/container/ and https://docs.docker.com/engine/reference/commandline/image/

All these examples use the Docker command line interface and you will need to open a terminal with access to the docker command. Some tasks will require two terminals to be open.

Before starting the workbook download the files from the companion Git repo (you can clone the repo or download a zip version).

Basic operations

Task 1. Pull from Docker Hub

Command to run:

docker image pull alpine:latest

This will download the official image for Alpine Linux from Docker Hub, specifically the version tagged with the string “latest”.

Notes:

  • Docker Hub is a public registry of many, many, images you can download and use.
  • When pulling images from the command line, it’s common to omit the tag and assume the default latest. However when specifying images for deployment (e.g. in a Docker file) then the version tag should always be present.
  • The tag latest is not guaranteed to be the latest version. It’s just the default tag, and by convention it is usually the latest production version of a container image. But this is not a guarantee and you should check that the latest really is the correct version for your purposes, or specify a different tag.
  • In order to save people typing effort when doing these tasks we will often omit the tag and assume the default latest. That also means that in the later build exercises we use the latest tag to reduce image downloads.

Task 2. Contents of a Docker image

Command to run:

docker image inspect alpine|less

There is a lot of information, so it can be confusing, but as an example look for the values for Cmd and Env metadata.

(Remember the search command in less is the / character.)

Note: Passing value via the environment (Env metadata) is one of the ways we pass config information into a container at start-up. There are multiple values for Cmd and Env because of the way that images are build up from layers. The commands below resolve this ambiguity.

Some handy versions of the inspect command

docker image inspect alpine --format '{{.ContainerConfig.Cmd}}'
docker image inspect alpine --format '{{.ContainerConfig.Env}}'

More on the inspect command later on.

Task 3. Run a container

Command to run:

docker container run -it --name myAlpine alpine

The -it options (interactive and terminal) connect the container to your terminal session. Refer to the documentation for more details.

You can now type Linux shell commands at the container shell prompt. For instance get a list of every process running in the container with ps -ef (it’s a very short list!)

Type exit to stop the container and then type docker container rm myAlpine to remove the container.

Note: Many Linux images for Docker, by default, do not contain the usual full featured shell program you are probably used to (for example Bash or Zsh). Instead a smaller program such as Dash or BusyBox is installed. For many environments this should not be a problem, but be aware that your favourite advanced shell features may not the available “out of the box”. You can always build an image with your favourite tools installed – more in building images later.

TIP: Alpine Linux is a very small and useful Linux distro. However if it is too limited for your needs and you want to use Debian (for example) then consider using a *slim tag. For instance the debian:testing-slim image is approximately 80Mb, compared to 124Mb for debian:testing (at the time of writing).

Task 3.1. Run an image not pulled into the local cache

Note: This task should be skipped if short of time or you have limited network access.

  • Delete the image we downloaded

    docker image rm -f alpine:latest
    

    (the -f forces the image to be removed, which means any related containers are shut down first if needed, use with care)

  • Now try and run the image again

    docker run -it --name myAlpine alpine
    

If the image is not in our local cache then it will get automatically downloaded, in simple development environments we don’t often use docker image pull ... explicitly.

This container has been given a name with --name myAlpine so that we can refer to it in the net task.

Task 4. Stop a container

Note: We will look at the life cycles of containers in the next section. For now let’s just shut down the running container called myAlpine.

  • Stop the container with

    docker container stop myAlpine
    
  • The container still exists and could be restarted if needed. You can see this with

    docker container ls -a
    

    or

    docker container ps -a
    
  • Remove the container with

    docker container rm myAlpine
    

Some handy shell aliases

It’s tedious to keep typing docker container ... and docker image ... so let’s make life easier with some aliases.

alias doci="docker image"
alias docc="docker container"

In PowerShell you can create functions

Function docc {&"docker.exe" container $Args}
Function doci {&"docker.exe" image $Args}

PowerShell Notes

  1. When using these functions it’s important to quote any docker arguments that contain commas “,”. This stops PowerShell from turning them into an array. For example:

    docc run --mount "type=bind,source=$PWD\SQLite,destination=/code" --rm -it  ...
    
  2. These examples were tested using PowerShell 7.x

The rest of these examples assume you have created these aliases.

Basic container life cycle

Task 5. Run state

  • Tidy up from last task, just in case.

    docc rm -f myApline
    

    Note: Use the -f (force) option with care because it also shuts down any related containers that might be running (or stopped).

  • Let’s see what’s running

    docc ls -a
    

    should be an empty list (more on the -a option in a minute)

  • Start a container with

    docc run -d --name myAlpine alpine /bin/sh -c 'while echo $(( i += 1)) ; do sleep 5 ; done'
    

    This image will keep running in detached mode, printing out integers to stdout every five seconds. You can verify this by looking at the container log with the command

    docc logs --follow myAlpine
    

    Hit ctrl-C when you get bored to exit the log display.

What is the state of the container?

  • Run the commands

    docc inspect --format '{{.State.Status}}' myAlpine
    docc ls
    

In both cases the state should be running or up

Task 6. Exited state

  • Stop the container with

    docc stop myAlpine
    

    This may take a few seconds…

  • What is the state? Run the two commands (inspect and ls) we ran previously. We get different results

  • Try the -a option on docc ls, i.e. docc ls -a and we can see the stopped container.

So generally always use the -a option to ls, i.e. docc ls -a.

Why doesn’t the container just vanish when we stop it? There are a few useful things we can do with a stopped container:

  • Access the contents of the container’s modified file system. For example see the docker container export command.
  • Look at the logs docc logs ...
  • Restart the container with docc start ...

Let’s try some of those things on our stopped container

Task 7. Start a stopped container

docc start -ia myAlpine

This time we used the -ia options to attach the container to our terminal and the numbers start appearing this time, starting from one again. This leads to a couple of observations

  1. The docker container logs command provides access to the containers stdout (and stderr) fle streams.
  2. When the container is (re)started the running process is reset back to it’s initial state.

Now switch to second terminal and see the state of the container now

docc inspect --format "{{.State.Status}}" myAlpine

NOTE: Don’t confuse the start command and the restart command

We can even execute a new process in the container. e.g.

docc exec -it myAlpine /bin/ps -ef

or

docc exec -it myAlpine /bin/sh # Type exit when you get bored

NOTE exec only works with a container that is currently running.

TIP: With the exec command you may not need to install sshd in your images

TIP: After stopping a container with docc stop ... you can (usually) use docc start ... to make it active again. However if the container stopped because of a problem then start will probably not work.

Task 8. Remove the container

Now let’s really get rid of the container

docc rm -f myAlpine

Now docc ls -a and docc inspect myAlpine show the container really has gone. This includes any changes the container made to the file system, because container file systems are not persistent.

TIP: One we are happy we don’t need to access to the container after it’s stopped we can pass the --rm option to docc run .... For example

docc run -d --rm --name myAlpine alpine

The container has no command to run so it should exit immediately and the container removed. You can verify this with the inspect and ls -a commands

Containers communicating with the outside world

Because containers are isolated, and any changes made to the container file system are temporary, Docker needs to provide specific ways to get information into and out of containers at run time. Broadly these are:

  • Publishing Network Ports
  • Bind Mounts
  • Volume Mounts
  • Passing environment values when starting a container
  • Writing information in the container log

Task 9. Network Ports

Note the files for this task are located in the subdirectory APIserver. The files are not needed to complete the task, but are provided for later reference.

During this workshop we don’t have time to cover Docker networking, but it’s important to have a basic understanding of how to use port mapping.

Port Mapping allows a container to expose a network end point by publishing an IP port on the Docker container to a port on the Docker host. An example should help make this clear

I’ve created a simple Docker image that runs a Go API server.

The API server code responds to API calls on port 8000. So we can get an interesting result by running the API server in a container, on our local workstation, and then hitting the server with HTML GET request on the correct URL. For example

Pull and run the API container as follows

docc run -p 8089:8000 --rm -d --name myAPIserver dockerfornovices/api-server:latest

The Docker engine exposes the API on port 8089 on the host interface.

The -p option maps the host port 8089 to container port 8000. (use IP address 0.0.0.0. which is every network interface on the system).

Generally when we are mapping a container resource (e.g. a tcp port using -p, or a storage volume using -v) to an external entity the external description is on the left, then a : followed by the container resource.

Test this by running curl or your browser against the URL http://0.0.0.0:8089/people. For example with curl

curl -v http://0.0.0.0:8089/people

(Note: Depending on your Docker environment you may need to use

curl -v http://127.0.0.1:8089/people
```) instead

You can also test the API with a browser on the same URL.

Note that the Dockerfile for this image uses the
[`EXPOSE`](https://github.com/dockerfornovices/DockerSimpleDemo/blob/master/APIserver/Dockerfile#L21)
instruction for port 8000

#### Task 9.1. Check the health of a container.

Extra Bonus -- skip on 1st reading.

Side Note for later consumption:
the [Dockerfile](https://github.com/dockerfornovices/DockerSimpleDemo/blob/master/APIserver/Dockerfile)
for this example has a couple of more advanced features.

1. [Multi stage builds](https://docs.docker.com/develop/develop-images/multistage-build/)
to substantially reduce the final image size

2. A [HEALTHCHECK](https://docs.docker.com/engine/reference/builder/#healthcheck) command in the
[Dockerfile](https://github.com/dockerfornovices/DockerSimpleDemo/blob/master/APIserver/Dockerfile#L23)
to create a container healthcheck. You can see the current health
of the container with the `inspect` command. For instance

docc inspect –format ‘{{.State.Health.Status}}’ myAPIserver

Note: The values used for `--interval`, `--timeout`
and `--retries` are not typical or recommended for daily use.

**NOTE**:  After completing task Task 9 make sure
to remove you container with `docc rm -f myAPIserver`

### A note about Bind Mounts and Volumes

Bind Mounts and Volume Mounts are very similar, but they
serve different purposes. They both expose
persistent file storage to the container, and are both configured using
the `--mount` or `-v` option, so it can be confusing.

* [Volumes](https://docs.docker.com/storage/volumes/) are used to
provide persistent data storage for containers.
For example data files, database storage and configuration files, i.e. any information
that needs to survive across container restarts should be stored in a volume.
Volumes store data in data volumes, which are Docker "objects" in a similar
manner to images and containers. They are managed via Docker commands
and the underlying file system information cannot accesed except via a running container.

* [Bind mount](https://docs.docker.com/storage/bind-mounts/) allow the
container to access files located on the host file system.
They are useful for giving containers access to source code and related
resources during development. Generally bind mounts should **not** be used in production
systems.

**Note**: Most historical examples of Bind Mounts and Volumes use the Docker `-v` option,
but the `--mount` option is now the preferred option.

> Tip: New users should use the --mount syntax.
  Experienced users may be more familiar with the "-v"
  or "--volume" syntax, but are encouraged to use "
  --mount", because research has shown it to be easier to use.
>
> -- The [The Docker Docs website](https://docs.docker.com/storage/bind-mounts/#choosing-the--v-or---mount-flag)

We will use the `--mount` option in the following examples, but you will still frequently see the `-v` (`--volume`)
option used.

### Task 10. Bind Mounts



A custom [Lua](https://www.lua.org/) development image has been created
for you to use in the following tasj. We will look at how this image is build later,
at the moment you just need to know that by default this image provides access to Lua 5.3.
(If you already understand how Docker images are build, you can find the build files in `bindmounts`.)

Note an example code file for this task is located in the subdirectory `SQLite`.

If you have downloaded or cloned the demo repo to your local machine then make
the repository root
your current working directory, these commands will then work without change.

So let's use a very simple Lua development environment and start coding.

1. If you are not using the example files,
   on your work station operating system create a development directory, e.g. `mkdir <src>`
   (you can call your `src` directory anything you want)

2. Copy some Lua code into your new directory.
   This has already been done in the supplied example, so
   if you are in the root of the project repo the following commands should work "as is",
   otherwise modify to suite.

3. Start your development container (built with a simple Lua development environment)
   with the `--mount` parameter.
   If you have a different source directory then change the directory `SQLite` in the `--mount` option

docc run –rm -it –mount type=bind,source=$PWD/SQLite,destination=/code –name myLuadev dockerfornovices/lua-dev:latest ```

NOTE: the source directory name must be an absolute path, not something that be interpreted as a volume name. Absolute paths begin with a /, ‘.’ or ‘..’. Using the $PWD environment variable is often useful for this.

  1. On your workstation (not in your container) start up your favourite code editor so you edit the Lua code, e.g. code SQLite/example1.lua
  2. Run the the Lua code on the container (./example1.lua) and see the bug
  3. Fix the bug in your editor, save
  4. Run the the Lua code on the container (./example1.lua) again and the bug should be fixed
  5. Type exit at the container shell prompt when finished and verify (using docker container ls -a) that the container did remove itself immediately, because we passed the --rm on container start-up.

We just used our favourite native text editor to edit code which we then tested on a custom container image tailored for our specific needs. What a great way to set up a development environment!

Task 11. Named Volumes

NOTE: Depending on time pressure consider completing this task after the workshop is finished.

Volumes are an important and potentially complex topic, this task shows how to create and use named volumes only. More information on volumes and how to manage them at https://docs.docker.com/storage/volumes/.

This task uses Docker volumes to persist the contents of an SQL database. For speed and convenience we are using SQLite, however the principles are identical for all data storage methods.

By default the dockerfornovices/sqlite image will display the output from SQLite .schema command, but if a string is supplied it will execute that inside the SQLite command processor instead.

  1. First let’s just confirm what happens when we don’t use volumes

    1. Create a table
    docc run --rm -it dockerfornovices/sqlite:0.1 "create table t1  (c1 varchar(20));"
    
    1. Insert some data
    docc run --rm -it dockerfornovices/sqlite:0.1 "insert into t1 values ('hello');"
    

    Oops

    We’ve run the container twice, once to create a table and then again to insert data. But the changes are thrown away each time the container is removed, so the SQL insert failed.

    So let’s fix that with a named volume. (Note: the /data directory is the data directory set up in the Docker file)

  2. Let’s check to make sure we don’t have the named db-demo volume hanging around already

    docker volume ls
    

    We can use the docker volume rm db-demo command to remove if needed.

    DANGER: If you want to remove every volume on your machine try docker volume rm $(docker volume ls -q)

  3. Run the image three times (so a different container each time) and access our database stored on a persistent volume.

    docc run --rm -it --mount "type=volume,source=db-demo,target=/data" dockerfornovices/sqlite:0.1 "create table t1  (c1 varchar(20));"
    
    docc run --rm -it --mount "type=volume,source=db-demo,target=/data" dockerfornovices/sqlite:0.1 "insert into t1 values ('hello');"
    
    docc run --rm -it --mount "type=volume,source=db-demo,target=/data" dockerfornovices/sqlite:0.1 "select * from t1;"
    
  4. And we can see our new volume db-demo with

    docker volume ls
    

    and

    docker volume inspect db-demo
    

    (Use the docker volume rm db-demo command to remove when finished)

For you convenience after the workshop the Dockerfile used to build the image used in this task is located in the directory sqlite.

Note that the Dockerfile for this image uses the `VOLUME <https://github.com/dockerfornovices/DockerSimpleDemo/blob/master/sqlite/Dockerfile#L22>`__ instruction for the /data database location.

Task 12. Environment Values

The files for this task are located in the directory bindmounts.

Next we are going to re-use the image provided for Task 10 that will allow us to test our Lua code under different versions of the Lua interpreter.

The container takes an “argument” at startup in the form of a environment variable that configures the Lua version we need when the container starts. This configuration takes place when the shell starts and process the ENV environment.

… interactive shells expand the ENV variable and commands are read and executed from the file whose name is the expanded value.

– Bash man page

# Setup Lua version. Default to 5.3

THIS_LUA=${LUA_VERSION:-5.3}

mkdir /tmp/bin
PATH=/tmp/bin:$PATH  # Make sure our custom links are 1st on PATH

ln -s /usr/bin/luac${THIS_LUA} /tmp/bin/luac
ln -s /usr/bin/lua${THIS_LUA} /tmp/bin/lua

This has been setup for you already. You will learn more about how to do this when we build images later.

  1. We can see this by running the image as follows

    docc run --rm -it -e "LUA_VERSION=5.2" --name myLuaDev dockerfornovices/lua-dev:latest
    

    Type lua -v at the container prompt to see the currently active version, and then exit.

  2. Now try running the container again with different values for LUA_VERSION, and also omitting the environment setting completely, to see how the Lua version changes.

Task 13. Container logs

In Docker the container’s stdout and stderr streams are treated as the log and can viewed with the docker container logs ... command.

  • Start an image
docc run -d --name myAlpine alpine /bin/sh -c 'while echo $(( i += 1));do sleep 5;done'

This container keep printing out integers every five seconds to the container standard out (stdout).

But we started the container in detached mode (-d option), so we can’t see the logs.

But we can see the log output with the docker container logs command, .e.g.

docc logs --follow myAlpine

Hit <ctrl-c> to exit the log display and then remove the image with

docc rm -f myAlpine

NOTES:

  1. Because Docker expects containers to write log messages to stdout or stderr. Docker start up scripts or commands (configured by the CMD or ENTRYPOINT entries in the Dockerfile, see next task) should connect and application logs to stdout after starting the main container process.
  2. Container logs are stored in a circular FIFO buffer. After a while containers will overwrite older log data.

Creating our own custom Docker images

There are lots and lots of Docker images around the Internet. But sometimes you need something different specific to your needs.

Task 14. Building Images

Let’s build the image from task Task 12, an image contains multiple versions of Lua.

As well installing the different versions of Lua we will also need to copy over the custom $ENV script.

The example files for this activity are located in bindmounts.

Create a dockerfile, using the the default name Dockerfile.

FROM alpine:3

#Build this image with "docker image build -f lua.dockerfile  --tag lua-dev ."
#then run with "docker container run --rm -it --name myLuadev lua-dev"

LABEL maintainer  "Alec Clews <alecclews@gmail.com>"
LABEL description "Linux with Lua, Versions 5.1, 5.3 and 5.3"

RUN apk add --no-cache lua5.1 lua5.2 lua5.3

# Setup up file will configure the correct version of Lua
COPY lua.setup.sh /lua.setup.sh
ENV ENV /lua.setup.sh
RUN chmod 755 /lua.setup.sh

# Default starup command
CMD "/bin/sh"

# Add a volume to hold the development code
VOLUME "/code"

# Make the code directory the default on startup
WORKDIR "/code"

Tip: if the defaulltt

This docker file is very simple, but does include the the important commands you will see in most Docker files:

  • The FROM directive is the base image we are building on. Images are always built in layers on top of a base image.
  • LABEL inserts information in the image that can be retrieved with the docker image inspect ... command. These two examples are the very bare minimum expected in an image
  • The RUN command executes Linux shell commands (e.g. package management). This is how custom content is added to the image
  • COPY transfers files from the build context (the directory listed as the end of the docker build command) into the image.
  • ENTRYPOINT (or CMD) defines the command that will be run (by default) when the container starts.
  • VOLUME describes a volume or bind mount that will be configured at run time.
  1. Create a new empty directory that will contain only the files needed to build your container. For the workshop a directory has already been provided with the needed files (bindmounts)

  2. At run time we want a $ENV file that will set up the correct lua version depending on the environment variable $LUA_VERSION.

    That file needs to be in the bindmounts (the docker build context) directory along with our Docker file. It can then be copied into our image during the build.

  3. Make the docker build context the current default directory. If you have a copy of the repository locally then make bindmounts your current default directory.

    cd bindmounts
    
  4. Build

    docker image build --tag lua-dev:0.1 .
    

    TIP: Notice the build context (.) at the end of the command. Very important, and very easy to miss!

    TIP: This also works from the repo root directory as long as we provide the correct build context path

    doci build -f bindmounts/lua.dockerfile --tag lua-dev:0.1 bindmounts
    

    More details on the --tag option later, but for now just know that it assigned a user friendly “alias” to our new image.

  5. Use the docker image inspect ... to verify the value of the labels maintainer and description

  6. From the repository root (you might need to run cd ..) test the image with the LUA source code from our previous bind mount example (SQLite)

    docker container run --rm -it \
    --mount type=bind,source=$PWD/SQLite,target=/code \
    -e "LUA_VERSION=5.2" --name myLuaDev lua-dev:0.1
    

Congratulations! You created a custom Docker image for your specific requirements which you can run whenever you need it.

There are a few things that are not optimal with this build

  1. It uses the Lua versions available in the Alpine package repo, which may not contain the version one we need
  2. We will need to add some additional Lua libraries to do any useful work.
  3. Everything is done under the root account

Running under the root account

By default images run processes under the container root account. When using bind mounts to write content the file will belong to user id 0 on the host, which is the root account on the host as well. It’s then very hard to use these files, and it’s also bad security practice.

To fix that we can make the container run under the same user and group as the current user (--user option).

docker container run --rm -it \
--user=$(id -u):$(id -g) \
--mount type=bind,source=$PWD/SQLite,target=/code \
-e "LUA_VERSION=5.2" --name myLuaDev lua-dev:latest

NOTE: $(id -u) and $(id -g) return the current user id and group id for the user running Docker on the host. See Command Substitution(https://www.gnu.org/software/bash/manual/bash.html#index-command-substitution)

However this approach has limitations and it’s often better to create an unprivileged user account in a custom image. Refer to the example provided in the repo.

Task 15. Images and Docker files on Docker Hub

Browse Docker Hub were you can find lots of pre-build images ready for you to use.

You can search for images that have Lua and look at the Dockerfile used to create the image. For example this image, which is build with this Dockerfile by Nick Muerdter. It build the requested Lua version from source, a common pattern when creating custom images.

Task 16. Tagging builds and pushing to Docker Hub

Note: If short of time consider skipping this task

Every Docker image can be identified by a sha1 hash, which can be handy, but is not very user friendly. To see the sha1 ID of an image use the docker image ls command.

As well as the ID, images can be tagged with “aliases”. The term aliases is not generally used, but the commonly use term tag is misleading as it is also a specific sub field of the alias. Let’s look at an example

  • Create a local image and then run ls to see the details

Before starting You can remove any previous builds of these image

doci rm lua-dev:0.1

Now build the image

doci build -f lua.dockerfile --tag lua-dev:0.1 bindmounts
doci ls

and see something like

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
lua-dev                                    0.1                 7a1a8dbdbcfd        3 minutes ago       4.7MB

As you can see the alias consists of two fields, the repository name and the tag.

Run a container from the image using either the alias or the ID

docc run --rm -it lua-dev:0.1
docc run --rm -it 7a1a8dbdbcfd

TIP: When using the ID, only enough of the ID string is required to be unique. So this also works. This is often a lot easier than typing in the alias.

docc run --rm -it 7a1

However the second reason for using an alias is to push and pull images to and from Docker registry over the network.

For example I have an account on Docker Hub called dockerfornovices and I can push my new image to Hub.

Note: In these examples you will need to supply your own Docker Hub account name

  1. Give the image the correct alias that includes my account name
  2. Push using the new alias

Images can have multiple aliases so we don’t need to rebuilt. Just assign an additional alias (with the tag command) to the existing image

doci tag lua-dev:0.1  dockerfornovices/myimage:0.1

Notice how the in both cases it’s the same image (look at the ID), but an entry shows up for each alias.

REPOSITORY                                 TAG                 IMAGE ID            CREATED             SIZE
dockerfornovices/myimage                   0.1                 7a1a8dbdbcfd        17 minutes ago      4.7MB
lua-dev                                    0.1                 7a1a8dbdbcfd        17 minutes ago      4.7MB

This image can be pushed to Docker Hub, assuming I already have access to the correct account

At the Docker command line

docker login -u <username> #Enter password when prompted
doci push dockerfornovices/myimage:0.1

Docker Hub is not the only registry you can use. You just need to add extra information to the alias to reference different providers (including your own private local registries).

For example you download and run one of my images from the GitLab registry as follows

docker run -it --rm --name mySQLite registry.gitlab.com/alecthegeek/dockerimages/sqlite:0.1 $'create table t1  (c1 varchar(20));'

There can be variations on the way that different registries specify the name space containing the Docker image, but the basic layout is as follows:

<host-name><:port-number></namespace>/<repo-name>:<tag>

Everything but the repo-name will default to the following values:

  • host-name: Docker Hub
  • port-number: Docker default port
  • namespace: blank – for any images you need to push you will need to supply a value here. For example your Hub user account. When pulling an image a blank namespace will pull from the Hub default repositories
  • repo-name: Must be supplied
  • tag: “latest

TIP: GitLab provides Docker private registry services free of cost. See https://docs.gitlab.com/ee/user/project/container_registry.html

Side Note: It is possible, but not common, to transfer Docker images using Sneakernet as explained here (basically use the docker image save ... and docker image load ... commands).

Task 17. Using ARG during Docker build

Note: If short of time consider skipping this task

The files for this task are located in the directory buildargs for later reference.

In task Task 12 we looked at how to use environment values in a running container via the -e option.

It’s also possible to pass configuration values to an image at build time. Let’s modify our previous Lua development environment. Instead of passing in the required Lua version at run time, we’ll pass it in at build time and create an image with only the single required version of Lua.

In the example Docker file, luadev.dockerfile, the value of the build argument LUA_VERSION is used when installing Lua. A default value is also supplied if no value is supplied during the build.

Note: We can then reference this as am ARG value in the dockerfile.

  1. Build a new image passing in a Lua version with the --build-arg option

    docker image build -f buildargs/lua.dockerfile --build-arg LUA_VERSION=5.2 --tag lua-dev:local-5.2 buildargs
    

    or make sure buildargs is the current working directory and run

    docker image build -f lua.dockerfile --build-arg LUA_VERSION=5.2 --tag lua-dev:local-5.2 .
    

    Note the --tag option is used to identify the image, compared to other lua-dev images we may have

  2. Verify that the correct version has been installed with

    docker container run --rm -it --name myLuadev lua-dev:local-5.2 lua -v
    

    Note: The default command in this image (/bin/sh) has been over-ridden at the command line with lua -v.

  3. Now build images with different values for LUA_VERSION. For example

    docker image build -f buildargs/lua.dockerfile --build-arg LUA_VERSION=5.1 --tag lua-dev:local-5.1 buildargs
    

NOTE: The value of LUA_VERSION is not available to the Docker container at run time.

Task 18. Using ARG and ENV together during Docker build

By default the value of any build args (ARG in the Dockerfile) do not make it into the container runtime, although can find them with the command docker container inspect.

But we can create ENV values during the build which will appear in the container’s environment.

It is possible to set an ENV value from an ARG value. For example:

Let’s go back to our original Lua development environment. We had multiple Lua versions installed and the default version was hard coded in the shell’s $ENV file. Let’s allow the default version to be configured at build time. The files for this example are in envfromargs.

  1. Modify the dockerfile to to accept a build argument (and provide a default in case no value is provided)

    ARG LUA_VERSION=5.3
    
  2. Still in the dockerfile, create an environment value that will be seen at runtime

    ENV LUA_VERSION_ENV=${LUA_VERSION}
    

    In this case both the ARG and the ENV values have the same value and the same purpose. There would be no problem in giving them the same same name, but it is potentially confusing.

  3. Modify the $ENV script to make use of the correct environment name.

    ln -s /usr/bin/luac${LUA_VERSION_ENV} /usr/bin/luac
    ln -s /usr/bin/lua${LUA_VERSION_ENV} /usr/bin/lua
    
  4. Build the new image passing in the required default Lua version

    doci build --tag lua-dev-f:5.1 --build-arg "LUA_VERSION=5.1" envfromargs
    
  5. Test with

    docker container run --rm -it --name myLuadev -e LUA_VERSION_ENV=5.3 lua-dev-f:5.1
    

    and

    docker container run --rm -it --name myLuadev -e LUA_VERSION_ENV=5.1 lua-dev-f:5.1
    

Tying it all together with Docker Compose

`docker compose <https://docs.docker.com/compose/>`__ is a Docker tool that builds, provisions and starts multiple images that may be required to create a development environment.

docker compose can be be considered as high level wrapper around the various docker ... commands. You should be familiar with how the lower level commands work before you start using docker compose in earnest. Also Do not confuse docker compose with container-orchestration tools such as Kubernetes or Swarm.

Note: docker compose is the replacement for docker-compose. More information here

The details of docker-compose are beyond the scope of this workshop, but a short demonstration might be useful. For more details refer to the Docker docs linked above.

Task 19. Creating a local MongoDB development environment

This example provides a local developer easy access to a simple MongoDB development environment. It uses three three images:

Three containers for development

Three containers for development

  1. A MongoDB database server, created by the MongoDB team on Docker Hub, with persistent storage provided through a named volume.
  2. A DB shell so the developer can run DB admin scripts. The directory db-scripts is bind mounted into the running container
  3. A Python development environment with all needed dependencies installed. The src directory is bind mounted into the container

All the containers are run via a compose file docker-compose/docker/dev-compose.yaml

Let’s see it in action:

First run the MongoDB shell with the command

docker compose -f ./docker-compose/docker/dev-compose.yaml run shell

You will need to be patient when it first builds.

Once you are at the container prompt you can run any MongoDB admin functions you want, for example db.adminCommand("listDatabases"). Use the quit() function to exit the MongoDB shell.

Notice that the containers are not removed (docc ls -a)

Now run the Python environment with

docker compose -f ./docker-compose/docker/dev-compose.yaml run python

A couple of example Python scripts are provided for you to play with. Type exit when you get bored.

To tidy everything up use the command

docker compose -f ./docker-compose/docker/dev-compose.yaml down

So with a single YAML file (and two dockerfiles) we have provisioned a simple Python and MongoDB database development environment

Clone this wiki locally