Getting Started with Docker by Practice | 2. Building a Docker Image

The previous section introduced the Docker overview and basic hands-on operations for managing images and containers step by step. So far, we have used public images, but to distribute an application you are developing as a Docker image, you also need to know how to create an image yourself. This section explains how to create an image directly and start a container from it. The sample introduced here is based on Docker’s beginner tutorial.

How Docker image builds work

First, let’s look at how Docker images are created.

Building a Docker image means adding features to a base image and creating a custom image of your own.

Build structure

The base image is usually an OS image provided by Docker or the Docker community. You create a custom image by installing various dependent libraries and middleware, or by installing and configuring your own application. You can distribute this custom image to third parties in a form that includes libraries, middleware, applications, and so on.

The configuration file that describes the operations to perform on the base image is the Dockerfile. The way to write a Dockerfile is defined by Docker. For details, refer to the documentation.

Build a sample image

Here, we will build a Docker image that deploys a web application using Flask, a web application framework for Python. The application displays a random GIF image. Details about Flask itself are omitted here.

Prepare the files needed for the build

Create a directory and download the four files needed to build the image as follows.

% mkdir flask-app
% cd flask-app
% wget https://raw.githubusercontent.com/docker/labs/master/beginner/flask-app/app.py
% wget https://raw.githubusercontent.com/docker/labs/master/beginner/flask-app/requirements.txt
% wget https://raw.githubusercontent.com/docker/labs/master/beginner/flask-app/templates/index.html -P templates
% wget https://raw.githubusercontent.com/docker/labs/master/beginner/flask-app/Dockerfile

Next, let’s look at the contents of each file.

The app.py file is the source code that becomes the entry point of the web application. It searches for GIF images on the web and returns a random URL.

from flask import Flask, render_template
import random

app = Flask(__name__)

# list of cat images
images = [
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-26388-1381844103-11.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-31540-1381844535-8.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-26390-1381844163-18.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-1376-1381846217-0.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-3391-1381844336-26.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-29111-1381845968-0.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-3409-1381844582-13.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-19667-1381844937-10.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-26358-1381845043-13.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-18774-1381844645-6.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-25158-1381844793-0.gif",
    "https://raw.githubusercontent.com/devkuma/docker-tutorial/main/flask-app/image/anigif_enhanced-buzz-11980-1381846269-1.gif"
]

@app.route('/')
def index():
    url = random.choice(images)
    return render_template('index.html', url=url)

if __name__ == "__main__":
    app.run(host="0.0.0.0")

The requirements.txt file lists the required Python modules. It is used when installing Flask with pip, the package manager.

Flask==1.0

The templates/index.html file is the HTML template output by the web application. It only displays the image.

<html>
  <head>
    <style type="text/css">
      body {
        background: black;
        color: white;
      }
      div.container {
        max-width: 500px;
        margin: 100px auto;
        border: 20px solid white;
        padding: 10px;
        text-align: center;
      }
      h4 {
        text-transform: uppercase;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h4>Cat Gif of the day</h4>
      <img src="{{url}}" />
      <p><small>Courtesy: <a href="http://www.catshaming.co.uk/20-best-cat-gif-posts">Catshaming</a></small></p>
    </div>
  </body>
</html>

The Dockerfile is a file that summarizes the commands used by the Docker daemon when creating an image. The sample Dockerfile performs the following operations. Dockerfile syntax is explained later.

  • Specify Alpine Linux, a lightweight Linux distribution developed for Docker, as the base image.
  • Install Python and pip, the package manager.
  • Install the Python modules required by the application with pip.
  • Copy app.py and index.html to the specified locations.
  • Expose port 5000 externally.
  • Run the web application.
# our base image
FROM alpine:3.5

# Install python and pip
RUN apk add --update py2-pip

# upgrade pip
RUN pip install --upgrade pip

# install Python modules needed by the Python app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt

# copy files required for the app to run
COPY app.py /usr/src/app/
COPY templates/index.html /usr/src/app/templates/

# tell the port number the container should expose
EXPOSE 5000

# run the application
CMD ["python", "/usr/src/app/app.py"]

Supplement: commands used in a Dockerfile

At first glance, a Dockerfile looks like a shell script, but it actually uses Dockerfile-specific commands. The commands used in the sample Dockerfile are shown in the following table. For detailed syntax, refer to the manual.

Command Meaning Notes
FROM Specifies the base image
RUN Specifies a command for building the image In the case of RUN apk add -update py-pip, the apk add -update py-pip part is actually executed.
COPY Copies files from the host to the container.
EXPOSE Specifies the container port to expose externally. Container ports are closed by default.
CMD Specifies the command to run when starting a container from the image. Only one CMD can be specified in a Dockerfile.

Dockerfile best practices are summarized in Best practices for writing Dockerfiles, so refer to that as well.

Build the image

You cannot build unless the Docker daemon is running. Start the daemon in advance.

Run the following command in a directory that contains a Dockerfile. You should see the contents of the Dockerfile being executed in order.

Note: No sample proxy configuration is provided, so run this in an environment without an HTTP proxy.

% docker build -t myfirstapp .
Sending build context to Docker daemon  8.192kB
Error response from daemon: dial unix docker.raw.sock: connect: connection refused
kimkc@kimkcui-MacBookPro flask-app % docker build -t myfirstapp .
[+] Building 16.1s (12/12) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                 0.0s
 => => transferring dockerfile: 571B                                                                                                                 0.0s
 => [internal] load .dockerignore                                                                                                                    0.0s
 => => transferring context: 2B                                                                                                                      0.0s
 => [internal] load metadata for docker.io/library/alpine:3.5                                                                                        4.1s
 => [internal] load build context                                                                                                                    0.0s
 => => transferring context: 2.46kB                                                                                                                  0.0s
 => [1/7] FROM docker.io/library/alpine:3.5@sha256:66952b313e51c3bd1987d7c4ddf5dba9bc0fb6e524eed2448fa660246b3e76ec                                  0.5s
 => => resolve docker.io/library/alpine:3.5@sha256:66952b313e51c3bd1987d7c4ddf5dba9bc0fb6e524eed2448fa660246b3e76ec                                  0.0s
 => => sha256:66952b313e51c3bd1987d7c4ddf5dba9bc0fb6e524eed2448fa660246b3e76ec 433B / 433B                                                           0.0s
 => => sha256:f7d2b5725685826823bc6b154c0de02832e5e6daf7dc25a00ab00f1158fabfc8 528B / 528B                                                           0.0s
 => => sha256:f80194ae2e0ccf0f098baa6b981396dfbffb16e6476164af72158577a7de2dd9 1.51kB / 1.51kB                                                       0.0s
 => => sha256:8cae0e1ac61cead281f41115cc0ebd39117f7e54dffc8fd5e05a7590dca3cd4e 1.97MB / 1.97MB                                                       0.3s
 => => extracting sha256:8cae0e1ac61cead281f41115cc0ebd39117f7e54dffc8fd5e05a7590dca3cd4e                                                            0.2s
 => [2/7] RUN apk add --update py2-pip                                                                                                               4.9s
 => [3/7] RUN pip install --upgrade pip                                                                                                              2.8s
 => [4/7] COPY requirements.txt /usr/src/app/                                                                                                        0.0s
 => [5/7] RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt                                                                            3.0s
 => [6/7] COPY app.py /usr/src/app/                                                                                                                  0.0s
 => [7/7] COPY templates/index.html /usr/src/app/templates/                                                                                          0.0s
 => exporting to image                                                                                                                               0.7s
 => => exporting layers                                                                                                                              0.7s
 => => writing image sha256:b012c41a03101ad398f22ed50e569d3b5277f608847ca3275a555d6d21babf0c                                                         0.0s
 => => naming to docker.io/library/myfirstapp                                                                                                        0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

Start the container

After the image build is complete, start a container from the image with the following command.

% docker run -p 8888:5000 --name myfirstapp myfirstapp
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

In this command, the option maps container port 5000 to host port 8888. Open a web browser and access http://localhost:8888; the web application introduced above should be displayed.

Stop and delete the container

Run the following command to stop the running container.

% docker stop myfirstapp

Then run the following command to delete the stopped container.

% docker rm myfirstapp

Summary

This section introduced how to use a Dockerfile to build an image and start a container from that image through a concrete sample application. It covered the basics of image building, so you should now have an overview of Docker image builds. Because this topic is well covered in Docker’s official documentation, reading that documentation as well will help you handle many other cases.

This time we built locally and started the container locally. To allow other people to use the image, you need to publish it in a form that third parties can access. The next section introduces how to publish images.