Docker Build

Apr 9, 2020

If you need a refresher here are the first two posts in the series, Linux Containers and Docker and Docker Compose and Volumes.

Before moving on to other Linux container platforms, I’d like to focus on one more Docker command, docker build. Sometimes it’s helpful to slightly alter existing images, or create entirely new images not available on the Docker Hub. Building images is relatively straight forward once you get a feel for the syntax of Docker images. The build system is included in the base Docker packages.

The docker build command creates a Docker image from a Dockerfile. Files used for the build can be specified in a “context”, either a PATH or URL. For this example we’ll be using the PATH context to create a Dockerfile on our own system.

While you could grab an official Python 3.8.2 image from the Docker Hub, we’re going to create our own image from an Ubuntu 18.04 base.


Let’s create our Dockfile in the compose directory of home.

$ mkdir -p ~/compose/python-3.8 && cd ~/compose/python-3.8

Here is the Dockerfile we will be building towards in our example:

FROM ubuntu:18.04

# set up environment
ENV DEBIAN_FRONTEND=noninteractive

# install dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
                ca-certificates \
                build-essential \
                git \
                curl \
                libssl-dev \
                tk-dev \
        && rm -rf /var/lib/apt/lists/*

# download python from git
RUN git clone --branch ${PYTHON_VERSION} --depth 1
WORKDIR /tmp/cpython

# compile python
RUN ./configure \
        --enable-optimizations \
        --enable-loadable-sqlite-extensions \
        --enable-option-checking=fatal \
        --with-system-expat \
        --with-system-ffi \

RUN make -j "$(nproc)" && make install

# download and set up pip
RUN curl -o
RUN python3 \
        --disable-pip-version-check \

# install dependencies and copy binaries to fresh ubuntu image
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y --no-install-recommends \
        expat \
        openssl \
        && rm -rf /var/lib/apt/lists/*

COPY --from=0 /usr/local/bin /usr/local/bin
COPY --from=0 /usr/local/lib /usr/local/lib
CMD ["python3"]


Before we examine the Dockerfile in detail, let’s go ahead and build our image with the docker build command. Compiling Python will take a few minutes, potentially longer based on your system:

$ docker build -t petebuffon/python-3.8.2 .

You can give your image any name you want. Here to differentiate images from the Docker Hub and images I have created, I am preceding container names with my Github profile name and a slash. The build command ends with a space a period indicating docker should look for a Dockerfile to build from in the current directory.

Instructions and Layers

Each action in a Dockerfile is preceded by an INSTRUCTION argument all in UPPERCASE. Instructions in a Dockerfile are run in order during build time. Each instruction represents a read-only layer which are stacked at build time to form a complete Docker image. The RUN, COPY, and ADD instructions create layers while all other instructions create temporary intermediate images that do not increase the size of the build.

When a docker image is run and a container is generated, a new writable layer is added on top of the read-only layers. All changes made to the container during its lifetime are written to this writable container layer.

Let’s take a look at all of the layers of our image:

$ docker history petebuffon/python-3.8
IMAGE               CREATED              CREATED BY                                      SIZE                COMMENT
b406a2d3f589        About a minute ago   /bin/sh -c #(nop)  CMD ["python3"]              0B                  
7f21428b44b9        About a minute ago   /bin/sh -c #(nop) COPY dir:c33605b876f922410…   221MB               
31934cf85de9        About a minute ago   /bin/sh -c #(nop) COPY dir:de52b8b155bffd961…   17.3MB              
b683d2cbe1de        About a minute ago   /bin/sh -c apt-get update && apt-get install…   6.35MB              
4e5021d210f6        2 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
<missing>           2 weeks ago          /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B                  
<missing>           2 weeks ago          /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B                
<missing>           2 weeks ago          /bin/sh -c [ -z "$(apt-get indextargets)" ]     987kB               
<missing>           2 weeks ago          /bin/sh -c #(nop) ADD file:594fa35cf803361e6…   63.2MB 

A Dockerfile must begin with a FROM instruction, which specifies the parent image. Docker treats lines that begin with # as a comment. Add any necessary environment variables with the ENV instruction and set the working directory with the WORKDIR instruction.

Each RUN instruction precedes a single command to the base OS of the container image. Multiple commands can be strung together with && in order to reduce the number layers in the final image. If any changes are made to the Dockerfile and the build command is run again, only the changed layers will have to be rebuilt.

multi-stage builds and CMD

Invoking a second FROM instruction in a Dockerfile sets up a multi-stage build. Here our final container image is based on a fresh Ubuntu 18.04 image. The COPY instruction copies files or directories from one location to another. Here we are copying the /usr/local/bin and /usr/local/lib directories from our first stage (denoted with –from=0) to our fresh image.

$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
petebuffon/python-3.8   latest              b406a2d3f589        21 hours ago        309MB
<none>                  <none>              e7e5b2133bf4        21 hours ago        874MB
ubuntu                  18.04               4e5021d210f6        2 weeks ago         64.2MB

We can see here that using a multi-stage build reduced our image size from 874MB in our build image to 309MB in our final image.

Note: Base images such as Alpine can reduce image sizes even further. The current Alpine base image is a lean 5.95MB.

There can be only one CMD instruction in a Dockerfile. A CMD instruction often specifies an executable, in our case, python3. The preferred form of the CMD instruction is: CMD [“executable”,“param1”,“param2”]. When a new container is spun up without a command specified, the container will default to running the contents of the CMD instruction:

$ docker run -it --rm petebuffon/python-3.8
Python 3.8.2+ (heads/3.8:6318e45, Apr  8 2020, 19:35:50) 
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

When a new container is spun up with a command specified, the CMD instruction is not run and replaced by the specified command.

$ docker run -it --rm petebuffon/python-3.8 /bin/bash

We now have a fully functional python-3.8 container image ready for use. You can just use it yourself or even upload it to the Docker Hub. Creating functional images can take some work and perhaps require many iterations to get them just right.

Leveraging layers in the images can help reduce build time between steps. Sometimes it can help to run a container from a base image directly to get a more immediate and interactive response to troublesome commands.


Over time both your parent image and the packages installed on your image will become outdated. In order to update your own containers you will have to re-run the build command, stop any running containers, and re-create them. If you utilize Compose, this process is fairly streamlined.

You can set up your own images in Compose with the following syntax in your docker-compose.yaml file:

version: '2'
    build: ./python-3.8
    restart: unless-stopped

In order to update your own images with Compose, run:

$ docker-compose build --pull 
$ docker-compose up -d

You should now be relatively comfortable with building Docker images. Be sure to check out Docker’s Dockerfile reference and Best practices for writing Dockerfiles documentation. Well that should just about wrap it up for Docker. Next time we’ll be looking at another Linux Container implementation, LXD.