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 |
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.
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"]
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).
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:
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.
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