You will learn:

  • Create your own image using docker build
  • Specify the execution order
  • Specify the system to configure

Create a simple web application using the Python scripting language and the Flask framework. We define several functions in the application that can process HTTP requests and return an HTML response.

In our case, we will create an application that can say hello and show the current date:

Application source: app.py

from datetime import date
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "<h1 style = 'color: blue'> Hello There! </h1> <a href='/date'> Date </a>"

@app.route("/date")
def today():
    today = date.today()
    return "<p> {} </p>".format(today.strftime("%d.%m.%Y"))

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

If we want to run such an application in the classic way, we need:

  • have the Python3 interpreter installed,
  • install Flask framework,
  • run the application from the command line to listen on the specified port,
  • Use a web browser to make a request and view the result.

This requires executing multiple commands to get the application to the desired state. If we have a Debian or Ubuntu Linux OS available:

# If necessary, we will install the virtualenv package
sudo apt-get install python3-virtualenv
# Let's create a virtual environment
python3 -m virtualenv ./venv
# We are activating a virtual environment
source ./venv/bin/activate
# Install dependencies into the virtual environment
pip install flask
# Launch the application (Flask 2.x+)
flask --app app.py run --host=0.0.0.0 --port=5000

Docker image preparation

If we want to distribute such an application, we must prepare for different situations and ensure that all dependencies are met - Python is installed and the necessary libraries installed. The procedure for fulfilling dependencies differs depending on the environment we use - it will be different on the Windows environment, different on different Linux environments.

To make it easier, we will prepare a Docker image with the application and all the dependencies.

Docker image consists of several layers. At the lowest level are the operating system files, and above it are the applications. The new layer always covers the old one. Image layering allows us to easily modify existing images without having to do everything again.

We will create a new Docker image using the docker commands. Follow the instructions in the special Dockerfile file to take the existing image, modify it, compile it and save it in the local registry.

Dockerfile

The resulting application image is described in the Dockerfile file. It consists of a set of instructions for assembling the image. The instructions in Dockerfile allow us to easily repeat and modify the application build process.

The advantage is that we can use existing images when creating new images. The image will contain the operating system, necessary libraries and other dependencies. We don't have to start from scratch, but we will take advantage of existing image with Python interpreter installed.

Create requirements.txt (example):

Flask==3.0.2

(Optional) Create .dockerignore to speed up builds:

venv/
__pycache__/
*.pyc
.git/

Write the following instructions in Dockerfile:

# Basic image from Docker Hub (Python + Alpine)
FROM python:3.12-alpine

# Container working directory settings
WORKDIR /app

# Install application dependencies first (better build cache)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application files to the image
COPY ./app.py /app

# Document the port Flask will listen on
EXPOSE 5000

# Run as a non-root user
RUN adduser -D appuser
USER appuser

# Program to run
ENTRYPOINT [ "flask" ]
# Arguments (Flask 2.x+)
CMD ["--app", "app.py", "run", "--host", "0.0.0.0", "--port", "5000"]

The ENTRYPOINT command is the name of the command to be executed. CMD is a list of default arguments to the command.

Image compilation

There should be at least three files in the current directory:

  • app.py
  • requirements.txt
  • Dockerfile

(Optional): .dockerignore

When we have the Dockerfile instructions ready, we will try to compile the image:

docker build -t flaskapp:dev .

The -t (tag) argument specifies the name of the image in the registry. The part before the colon is the name of the image. The part behind the colon is the version of the image.

Verify that the new image is in the list of locally available images:

docker images

Launch your own container

If the image is prepared in the list of available images, we can create a new container:

docker run -it --rm -p 5000:5000 flaskapp:dev

Open http://localhost:5000 in your browser.

Docker takes the image and starts the application according to the instructions in ENTRYPOINT and CMD.

Startup conditions (program name, arguments, environment variables, etc.) defined in the image can be changed during startup with docker run by entering additional arguments.

                       ENV                    ENTRYPOINT  CMD
                        |                              |   |
                        v                              v   v
docker run -it --rm -e VARIABLE=hello --entrypoint echo flaskapp:dev hello world

Note:

In some images, the following shell script is used as ENTRYPOINT:

#!/bin/bash
set -e

# If the first argument is an application name
if [ "$1" = 'postgres' ]; then
    # I'll do some configuration
    exec gosu postgres "$@"
fi
# Otherwise, I will execute the arguments as a shell command
exec "$@"

In this case, when starting with docker run, we do not have to change the parameter --entrypoint, because the whole run command is read from the CMD arguments.

More about what is the difference between CMD, ENTRYPOINT and RUN.

Configure the application using environment variables

Edit the application source text to take into account the environment variable you defined. If the variable MY_NAME is defined, the application will greet you nicely according to the configured name. Otherwise it will greet our friend Ferko.

Application source: app.py

import os
from flask import Flask
from datetime import date

app = Flask(__name__)

@app.route("/")
def hello():
    name = "Ferko"
    if "MY_NAME" in os.environ:
        name = os.environ["MY_NAME"]
    return "<h1 style = 'color: blue'> Hello There {}! </h1>".format(name)

@app.route("/date")
def show_date():
    today = date.today()
    return "<p> {} </p>".format(today.strftime("%d.%m.%Y"))

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

Create a new image with an improved application.

The environment variable can be set just before the container is launched:

docker run -it --rm -p 5000:5000 -e MY_NAME=Daniel flaskapp:dev

The run of the image-ready application can also be modified with command line arguments.

If we want to get help in the classic command line, we write:

flask --help

In our image it will be:

docker run -it --rm -e MY_NAME=Daniel flaskapp:dev --help

The arguments in the last place replace the arguments entered with CMD.

To run the command line instead of flask, use the --entrypoint parameter, which will overlay the instructions in Dockerfile.

docker run -it --rm --entrypoint /bin/sh flaskapp:dev

Homework

Prepare an application that will count accesses (note: a simple in-memory counter resets when the container restarts; think about persistence).

Will you be able to find out and record the type of browser from the User-Agent header?

Additional information

Previous Post Next Post

Week 3: Creating your own container images