Activity 1: Building and running Linux containers
Contents
- What you’ll need
- Building a simple container
- Running a simple container
- Packaging a Python webserver application
- Setting entrypoints and exposing ports
- Cleanup
What you’ll need
To build and run Linux containers, you’ll need one of the following system configurations:
-
A 64-bit version of one of Docker’s supported Linux distributions (CentOS 7+, Debian 7.7+, Fedora 26+, Ubuntu 14.04+) with Docker Community Edition (CE) installed and non-root access enabled so commands don’t need to be prefixed with sudo
-
A 64-bit version of Windows 10 Pro/Enterprise/Education (Version 1607 or newer) with Docker Desktop for Windows installed and configured to use Linux containers
-
A 64-bit version of Windows 10 Home (Version 2004 or newer) with the Windows Subsystem for Linux (WSL2) and Docker Desktop for Windows installed, and Docker Desktop configured to use the WSL2 backend for Linux containers
-
macOS 10.14.0 Mojave or newer with Docker Desktop for Mac installed
Building a simple container
Create a file called Dockerfile
with the following contents:
# Use the filesystem layers and default configuration from the Ubuntu 18.04 base image
FROM ubuntu:18.04
# Create a new filesystem layer with an empty directory
RUN mkdir /mydir
Run the docker build command to build a container image from the Dockerfile:
# Build an image using the Dockerfile in the current directory and tag it as "test:latest"
docker build -t "test" .
If the ubuntu:18.04
base image is not already present on your system, you will see Docker downloading the filesystem layers for that image, followed by the execution of the RUN
directive.
Once the image has been built, you can inspect its filesystem layers using the docker history command:
docker history "test"
The layers will be listed in reverse-chronological order, with your directory creation layer at the top of the list and the filesystem layers from the ubuntu:18.04
base image below it. Each layer lists its size, creation time, and the details of the directive that created it. This varies based on the type of directive:
-
For
RUN
directives, the command that was executed is displayed. -
For other directives that do not have a command associated with them (such as
ADD
orCMD
), the text#(nop)
is listed, followed by the raw directive itself.
Running a simple container
Now that you have a container image built, you can start a container using the docker run command:
# Start a container with an interactive bash shell
docker run -ti "test" bash
This will start a container using the container image built in the previous section and run the bash
shell inside it. The -ti
flag instructs Docker to run the container in foreground mode with interactive input, which allows you to run commands in the bash shell just as you would if it were running outside a container. (The other supported mode is detached mode, specified by the -d
flag, whereby the container runs in the background with no user input.)
Once the container starts you will see that you are running bash
as the root user, with the current working directory set to the root of the container’s filesystem.
If you run the ps
command to list running processes, you will see only the processes running inside the container:
PID TTY TIME CMD
1 pts/0 00:00:00 bash
10 pts/0 00:00:00 ps
If you run the ls
command then you will see the list of directories under the root of the container’s filesystem, including the mydir
directory that was created in the Dockerfile’s RUN
directive.
Use the exit
command to terminate the interactive shell and stop the container. By default, Docker will not delete stopped containers, instead simply leaving them inactive so that they can be started again if desired. You can see the list of all containers (including the stopped container) using the docker ps command:
# List all containers, including stopped containers
docker ps --all
You should see the stopped container listed with its exit code, exit time, and the command that was originally used to start the container.
If you want Docker to automatically delete containers when they stop then you can use the --rm
flag when invoking the docker run
command:
# Automatically delete the container when it exits
docker run --rm -ti "test" bash
If you run docker ps
in a separate command prompt or terminal window you will see the container listed with a state of “Up N seconds” (where N is the time since the container started), which indicates that it is currently running. Once you have stopped the container, you will see that it has been removed and is no longer listed in the output of docker ps
.
Packaging a Python webserver application
Create a file called app.py
in the same directory as the Dockerfile from the previous sections with the following contents:
#!/usr/bin/env python3
from flask import Flask
# Create our Flask application
app = Flask(__name__)
# Wire up our endpoint
@app.route('/')
def endpoint():
return 'Hello World!\n'
# Start our Flask webserver
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80, debug=True)
Update the Dockerfile so that it contains the following directives:
# Use the filesystem layers and default configuration from the Ubuntu 18.04 base image
FROM ubuntu:18.04
# Create a new filesystem layer with an empty directory
RUN mkdir /mydir
# Install Python and the Flask package (plus curl for testing purposes)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl python3 python3-pip && \
pip3 install flask
# Copy our Python webserver app into the image
COPY app.py /app/app.py
Once the files have been modified, rebuild the container image to use them:
docker build -t "test" .
There are a couple of things to note during the build process:
-
Docker’s build cache avoids the need to re-run directives that have not changed since the last time they were built. The
RUN
directive that creates our empty directory will use the cache instead of being run again:Step 2/4 : RUN mkdir /mydir ---> Using cache
-
RUN
directives are executed non-interactively, which means they cannot rely on user input to function. Theapt-get install
command only works correctly because the-y
flag is passed to suppress interactive prompts. If you remove the-y
flag then the command will attempt to prompt the user for confirmation and fail because no user input is available.
Once the updated container image is built, start a new container with the Python application as the entrypoint process:
# Start a container with our Python webserver as the entrypoint
# (Note that we explicitly set a name here so the instructions below can reference it)
docker run --name="server" --rm -ti "test" python3 /app/app.py
This starts a container and immediately launches the Python webserver. To test that the server is working we can attach an additional shell to the container using the docker exec command and perform a test HTTP request with curl
:
# Attach an interactive bash shell to the running container
# (Note that we are using the container name we set in the `docker run` command)
docker exec -ti "server" bash
# This should print "Hello World!"
curl "http://127.0.0.1/"
Shift focus to the command prompt or terminal window running the Python webserver and terminate it by pressing Ctrl-C. You will note that this not only stops the webserver process, but also the additional bash
process that was attached using the docker exec
command. This is because Docker containers always stop when their entrypoint process terminates, even if there are additional processes running inside the container at the time. Any additional processes will be forcibly terminated when the container stops.
Setting entrypoints and exposing ports
In the previous section the Python webserver application was explicitly specified as the container entrypoint at the time when the container was started, but this information can be included directly in the metadata of the container image itself. Update the Dockerfile from the previous section so that it contains the following directives:
# Use the filesystem layers and default configuration from the Ubuntu 18.04 base image
FROM ubuntu:18.04
# Create a new filesystem layer with an empty directory
RUN mkdir /mydir
# Install Python and the Flask package (plus curl for testing purposes)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl python3 python3-pip && \
pip3 install flask
# Copy our Python webserver app into the image
COPY app.py /app/app.py
# Set our entrypoint command to the Python webserver app
ENTRYPOINT ["python3", "/app/app.py"]
Rebuild the container image and then start the container again, this time omitting the entrypoint information from the docker run
command:
# Start a container using the entrypoint specified in the image metadata
docker run --rm -ti "test"
You will see that the Python webserver application starts automatically, just as it did in the previous section when the entrypoint was specified manually. Press Ctrl-C to stop the container.
In order to access the server from outside of the container it is necessary to expose TCP port 80 (used for HTTP traffic) to the host system. There are two steps required for this to work:
-
Mark the port as exposed in the Dockerfile using the
EXPOSE
directive -
Map the exposed port to a host port when starting the container (unless you want Docker to choose a random port for you automatically, but that’s typically inconvenient because you will then need to query Docker to determine which port number it selected)
First, update the Dockerfile so that it contains the following directives:
# Use the filesystem layers and default configuration from the Ubuntu 18.04 base image
FROM ubuntu:18.04
# Create a new filesystem layer with an empty directory
RUN mkdir /mydir
# Install Python and the Flask package (plus curl for testing purposes)
RUN apt-get update && \
apt-get install -y --no-install-recommends curl python3 python3-pip && \
pip3 install flask
# Copy our Python webserver app into the image
COPY app.py /app/app.py
# Set our entrypoint command to the Python webserver app
ENTRYPOINT ["python3", "/app/app.py"]
# Export TCP port 80
EXPOSE 80
Now rebuild the container image and specify a port mapping when starting the container:
# Start a container with port 80 mapped to port 8080 on the host system
docker run --rm -ti -p 8080:80 "test"
You should now be able to view the server output by opening a web browser on the host system and navigating to http://127.0.0.1:8080/. If everything is working correctly then you should see the text “Hello World!” displayed. When you are done, focus on the command prompt or terminal window running the container and press Ctrl-C to stop it.
Cleanup
To remove the container image and stopped containers from the previous sections, run the following commands:
# Untag the "test:latest" image
docker rmi "test"
# Clean up our stopped containers and orphaned filesystem layers
docker system prune --force