Docker Config

Part 1, Chapter 4


Let's containerize the Flask app.


Start by ensuring that you have Docker and Docker Compose:

$ docker -v
Docker version 24.0.6, build ed223bc

$ docker-compose -v
Docker Compose version v2.23.0-desktop.1

Make sure to install or upgrade them if necessary.

Add a Dockerfile to the project root, making sure to review the code comments:

# pull official base image
FROM python:3.12.0-slim-bookworm

# set working directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# add and install requirements
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# add app
COPY . .

# run server
CMD python manage.py run -h 0.0.0.0

Here, we started with a slim-bookworm-based Docker image for Python 3.12.0. We then set a working directory along with two environment variables:

  1. PYTHONDONTWRITEBYTECODE: Prevents Python from writing pyc files to disc (equivalent to python -B option)
  2. PYTHONUNBUFFERED: Prevents Python from buffering stdout and stderr (equivalent to python -u option)

Depending on your environment, you may need to add RUN mkdir -p /usr/src/app just before you set the working directory:

# set working directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

Add a .dockerignore file to the project root as well:

env
Dockerfile
Dockerfile.prod

Like the .gitignore file, the .dockerignore file lets you exclude specific files and folders from being copied over to the image.

Review Docker Best Practices for Python Developers for more on structuring Dockerfiles as well as some best practices for configuring Docker for Python-based development.

Then add a docker-compose.yml file to the project root:

version: '3.8'

services:

  api:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/usr/src/app
    ports:
      - 5004:5000
    environment:
      - FLASK_APP=src/__init__.py
      - FLASK_DEBUG=1
      - FLASK_ENV=development

This config will create a service called api from the Dockerfile.

The volume is used to mount the code into the container. This is a must for a development environment in order to update the container whenever a change to the source code is made. Without this, you would have to re-build the image each time you make a change to the code.

Take note of the Docker compose file version used -- 3.8. Keep in mind that this version does not directly relate back to the version of Docker Compose installed; it simply specifies the file format that you want to use.

Build the image:

$ docker-compose build

This will take a few minutes the first time. Subsequent builds will be much faster since Docker caches the results. If you'd like to learn more about Docker caching, review the Order Dockerfile Commands Appropriately section.

Once the build is done, fire up the container in detached mode:

$ docker-compose up -d

Navigate to http://localhost:5004/ping. Make sure you see the same JSON response as before:

{
  "message": "pong!",
  "status": "success"
}

If you run into problems with the volume mounting correctly, you may want to remove it altogether by deleting the volume config from the Docker Compose file. You can still go through the course without it; you'll just have to re-build the image after you make changes to the source code.

Windows Users: Having problems getting the volume to work properly? Review the following resources:

  1. Docker on Windows — Mounting Host Directories
  2. Configuring Docker for Windows Shared Drives

You also may need to add COMPOSE_CONVERT_WINDOWS_PATHS=1 to the environment portion of your Docker Compose file. Review Declare default environment variables in file for more info.

Next, add an environment variable to the docker-compose.yml file to load the app config for the development environment:

version: '3.8'

services:

  api:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/usr/src/app
    ports:
      - 5004:5000
    environment:
      - FLASK_APP=src/__init__.py
      - FLASK_DEBUG=1
      - FLASK_ENV=development
      - APP_SETTINGS=src.config.DevelopmentConfig  # new

Then update src/__init__.py, to pull in the environment variables:

# src/__init__.py


import os  # new

from flask import Flask, jsonify
from flask_restx import Resource, Api


# instantiate the app
app = Flask(__name__)

api = Api(app)

# set config
app_settings = os.getenv('APP_SETTINGS')  # new
app.config.from_object(app_settings)      # new


class Ping(Resource):
    def get(self):
        return {
            'status': 'success',
            'message': 'pong!'
        }


api.add_resource(Ping, '/ping')

Update the container:

$ docker-compose up -d --build

Want to ensure the proper config was loaded? Add a print statement to __init__.py, right before the route handler, as a quick test:

import sys
print(app.config, file=sys.stderr)

Then view the Docker logs:

$ docker-compose logs

You should see something like:

<Config {
    'ENV': 'development', 'DEBUG': True, 'TESTING': False,
    'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': None,
    'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
    'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/',
    'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None,
    'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True,
    'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None,
    'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None,
    'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None,
    'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False,
    'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': None, 'JSON_SORT_KEYS': None,
    'JSONIFY_PRETTYPRINT_REGULAR': None, 'JSONIFY_MIMETYPE': None,
    'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093,
    'RESTX_MASK_HEADER': 'X-Fields', 'RESTX_MASK_SWAGGER': True,
    'RESTX_INCLUDE_ALL_MODELS': False
}>

Make sure to remove the print statement before moving on.




Mark as Completed