3. Docker Images

In this lab, we will write a Dockerfile and build a new Docker image.

Chapter Details
Chapter Goal Learn how to write Dockerfile and build Docker image
Chapter Sections

3.1. Build a Docker image

In the previous section (2. Docker Concepts), we learned how to use Docker images to run Docker containers. Docker images that we used have been downloaded from the Docker Hub, a registry of Docker images. In this section we will create a simple web application from scratch. We will use Flask (http://flask.pocoo.org/), a microframework for Python. Our application for each request will display a random picture from the defined set.

In the next session we will create all necessary files for our application. The code for this application is also available in GitHub:

3.1.1. Create project files

Step 1 Create a new directory flask-app for our project:

$ mkdir flask-app
$ cd flask-app

Step 2 In this directory, we will create the following project files:

flask-app/
    Dockerfile
    app.py
    requirements.txt
    templates/
        index.html

Step 3 Create a new file app.py with the following content:

from flask import Flask, render_template
import random

app = Flask(__name__)

# list of cat images
images = [
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Combing-Itself.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Hug.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Playing-Basketball.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Playing-Ping-Pong.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Using-Chopsticks.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Using-Computer.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-Wearing-Glasses.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-With-Beer.jpg",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cat-With-Teddy-Bear.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cats-Sitting-Like-Human.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cats-Using-iPad.gif",
"https://bitbucket.org/mirantis-training/public/raw/master/gifs/cats/Cats-Wearing-Party-Hats.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")

Step 4 Create a new file requirements.txt with the following line:

Flask==0.10.1

Step 5 Create a new directory templates and create a new file index.html in this directory with the following content:

<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}}" />
    </div>
  </body>
</html>

Step 6 Create a new file Dockerfile:

# our base image
FROM alpine:3.9

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

# upgrade pip
RUN pip3 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 ["python3", "/usr/src/app/app.py"]

3.1.2. Build a Docker image

Step 1 Now we can build our Docker image. In the command below, replace <user-name> with your user name. For a real image, the user name should be the same as you created when you registered on Docker Hub. Because we will not publish our image, you can use any valid name:

$ docker build -t <user-name>/myfirstapp .
Sending build context to Docker daemon 8.192 kB
Step 1 : FROM alpine:3.5
...
Successfully built f1277efd5a79

Step 2 Check that your image exists:

$ docker images
REPOSITORY     TAG    IMAGE ID     CREATED       SIZE
.../myfirstapp latest f1277efd5a79 6 minutes ago 56.34 MB

Step 3 Run a container in a background and expose a standard HTTP port (80), which is redirected to the container’s port 5000:

$ docker run -dp 80:5000 --name myfirstapp <user-name>/myfirstapp

Step 4 Use your browser to open the address http://<lab IP> and check that the application works.

Notes

<lab IP> is your public IP address you used to SSH into your environment. This application is reachable from your browser because your lab is accessible on the internet and the port 80 is mapped to the container port 5000 where the application is running.

Step 5 Stop the container and remove it:

$ docker stop myfirstapp
myfirstapp

$ docker rm myfirstapp
myfirstapp

Notes

docker stop takes a moment to return the prompt because this command performs graceful termination (SIGTERM). Try running the container again then run the command docker kill (SIGKILL).

3.2. Build a multi-container application

In this section, will guide you through the process of building a multi-container application. The application code is available at GitHub:

Step 1 Navigate to your home directory then clone the existing application from GitHub:

$ cd ~

$ git clone https://github.com/dockersamples/example-voting-app
Cloning into 'example-voting-app'...
...

$ cd example-voting-app

Step 2 Edit the vote/app.py file, change the following lines near the top of the file:

option_a = os.getenv('OPTION_A', "Cats")
option_b = os.getenv('OPTION_B', "Dogs")

Step 3 Replace “Cats” and “Dogs” with two options of your choice. For example:

option_a = os.getenv('OPTION_A', "Java")
option_b = os.getenv('OPTION_B', "Python")

Step 4 The existing file docker-compose.yml defines several images:

  • A voting-app container based on a Python image
  • A result-app container based on a Node.js image
  • A Redis container based on a redis image, to temporarily store the data.
  • A worker app based on a dotnet image
  • A Postgres container based on a postgres image

Note that three of the containers are built from Dockerfiles, while the other two are images on Docker Hub.

Step 5 Let’s change the default port to expose. Edit the docker-compose.yml file and find the following lines:

...
ports:
  - "5000:80"
...

Change 5000 to 80:

...
ports:
  - "80:80"
...

Step 6 Install the docker-compose tool:

$ sudo ~/k8s-examples/install/install-compose.sh

Step 7 Use the docker-compose tool to launch your application:

$ docker-compose up -d

Step 8 Check that all containers are running:

$ docker ps
CONTAINER ID        IMAGE                     COMMAND                  CREATED             STATUS              PORTS                                          NAMES
e29d586fa86f        examplevotingapp_worker   "/bin/sh -c 'dotne..."   21 seconds ago      Up 20 seconds                                                      examplevotingapp_worker_1
5fdad7b26fd3        postgres:9.4              "docker-entrypoint..."   24 seconds ago      Up 23 seconds       5432/tcp                                       db
145d0d583fb3        examplevotingapp_vote     "python app.py"          24 seconds ago      Up 20 seconds       0.0.0.0:80->80/tcp                             examplevotingapp_vote_1
7cc71a6c4bd6        redis:alpine              "docker-entrypoint..."   24 seconds ago      Up 23 seconds       0.0.0.0:32771->6379/tcp                        redis
ba4408f65906        examplevotingapp_result   "nodemon server.js"      24 seconds ago      Up 20 seconds       0.0.0.0:5858->5858/tcp, 0.0.0.0:5001->80/tcp   examplevotingapp_result_1

Check the NAMES of the created containers. As you can see the container names are prefixed by the project name examplevotingapp_ and postfixed by the container instance id, except in the cases where container_name: was specified in the docker-compose.yml file.

Step 9 Use your browser to open the address http://<lab IP> and check that the application works by voting for one of the languages.

Check the result of you vote using the exposed port of the result container/service by going to http://<lab IP>:5001.

Looks like our code change has a bug - do you see it? The result is still showing votes for CATS vs. DOGS.

Step 10 Stop the application:

$ docker-compose down
Stopping db ... done
Stopping redis ... done
...

Check to see if docker-compose cleaned up terminated containers:

docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

docker-compose down adds additional semantics where containers for terminated applications are automatically cleaned up.

3.3. Build a minimal container image

In this section we will use Docker Multi-Stage build to create a minimal container image, and also demonstrate the use of ENTRYPOINT (vs. CMD) using a simple golang calculator application.

Step 1 Create a directory to host our application:

$ mkdir ~/gocalc && cd ~/gocalc

Step 2 Create the gocalc application:

$ cat > gocalc.go <<TEXT
package main
import (
        "errors"
        "fmt"
        "os"
        "strconv"
)

func calculate(e []string) (float64, error) {
    result := 0.0
    num1, err := strconv.ParseFloat(e[0], 64)
    if err != nil {
            return 0.0, err
    }
    num2, err := strconv.ParseFloat(e[2], 64)
    if err != nil {
            return 0.0, err
    }
    switch e[1] {
    case "*":
        result = num1 * num2
    case "/":
        if num2 == 0.0 {
            return 0.0, errors.New("error: you tried to divide by zero.")
        }
        result = num1 / num2
    case "+":
        result = num1 + num2
    case "-":
        result = num1 - num2
    default:
        return 0.0, errors.New("error: don't know how to do that")
    }
    return result, nil
}

func main() {
    if len(os.Args) != 4 {
        fmt.Println("error: wrong number of arguments")
        return
    }
    res, err := calculate(os.Args[1:])
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(res)
    }
}
TEXT

Step 3 Create the gocalc Dockerfile:

$ cat > Dockerfile <<TEXT
FROM golang
RUN mkdir /build
ADD . /build/
WORKDIR /build
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o gocalc .
ENTRYPOINT ["./gocalc"]
TEXT

Step 4 Build and test the gocalc application:

$ docker build -t gocalc .
...

$ docker run gocalc 3 + 5
8

$ docker run gocalc 3 \* 5
15

Step 5 Check the gocalc image size:

$ docker image ls gocalc
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
gocalc              latest              5af95ee3d42c        17 seconds ago      813MB

Step 6 Create a new multi-stage-build Dockerfile to create a minimal image:

$ cat > Dockerfile.multi-stage <<TEXT
FROM golang:onbuild as builder
RUN mkdir /build
ADD . /build/
WORKDIR /build
ADD . ./
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-w -s" -o gocalc .

FROM scratch
COPY --from=builder /build/gocalc /
ENTRYPOINT ["/gocalc"]
TEXT

Step 7 Build and test the minimal gocalc application:

$ docker build -t gocalc:minimal -f Dockerfile.multi-stage .
...

$ docker run gocalc:minimal 3 \* 5
15

Step 8 Check the gocalc:minimal image size:

$ docker image ls gocalc:minimal
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
gocalc              minimal             23da855c0b35        About a minute ago  1.04MB

Checkpoint

  • Write Dockerfile
  • Build an image
  • Install Docker Compose
  • Write docker-compose.yml file
  • Use Docker Compose to build and run a multi-container application
  • Create a minimal image using Docker multi-stage-build