Share



Dockerize Your Flask-MariaDB-Redis Application with Docker Compose


Chances are if you are building a Flask application you need more than just Flask. You might need a database of some sort, such as MariaDB and maybe even a cache such as Redis. It is possible that the developers on your team prefer different operating systems, whether it’s MacOS, Windows, or Linux. Maybe you even ran into a problem similar to one I encountered and you have an M1 Mac which requires you use Python 3.9 or newer but your application is still stuck on Python 3.7. Whatever the case may be, Dockerizing our app will help us ensure a consistent, isolated, easily-installable environment whether it be on a developer’s machine or all the way up to production. Let’s jump into Dockerizing a Flask application using Docker Compose!

Simpsons Containers

Why Docker Compose?

Docker containers should have just one responsibility. If we have a Flask application that requires a relational database, Redis cache, etc. we should use multiple Docker containers. Based on the official Docker Compose documentation, Docker Compose is just the tool for our needs:

Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.

Note: This post assumes you already have Docker installed. If you need to install Docker then I suggest going here and installing your operating system’s version of Docker.

Project Structure

Let’s lay out a pretty basic Flask application, creating a project structure like so:

flask_docker
├── Pipfile
├── config
│   ├── __init__.py
│   ├── gunicorn.py
│   └── settings.py
├── docker
│   └── flask_docker
│       └── Dockerfile
├── docker-compose.yaml
└── flask_docker
    ├── __init__.py
    ├── api
    │   ├── __init__.py
    │   └── hello_world.py
    └── app.py

Note: All code used in this post can be found here.

Our project contains a Pipfile, meaning we will be using Pipenv for all of our project’s packaging needs. Let’s go ahead and make this file:

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[packages]
flask = "==2.1.1"
flask-sqlalchemy = "==2.5.1"
gunicorn = "==20.1.0"
redis = "==4.2.2"
sqlalchemy = "==1.4.35"
werkzeug = "==2.1.1"

[requires]
python_version = "3.10"

The Flask Application

The Flask application resides in the flask_docker directory, found inside our project’s root directory. It contains our app.py file, as well as the api directory. Within this api directory we have a hello_world directory where the hello_world.py file is found. Any needed configuration is found in the config directory.

Note: This tutorial covers a basic Flask application, but does not get into the details of Flask itself. I suggest the official Flask quickstart if you need a proper introduction to Flask.

First, let’s implement hello_world.py. This module will serve as a basic Flask Blueprint with a single GET method that returns the string Hello there! when accessed:

from flask import Blueprint

hello_world = Blueprint('hello_world', __name__)


@hello_world.get("/")
def hello():
    """
    Returns the string "Hello there!"

    :return: Flask response
    """
    return 'Hello there!'

With our “Hello World” Blueprint out of the way it is now time to implement app.py. This module is responsible for starting up our Flask app:

from flask import Flask

from flask_docker.api.hello_world import hello_world_blueprint


def create_app():
    """
    Create a Flask application using the app factory pattern.

    :return: Flask app
    """
    app = Flask(__name__, instance_relative_config=True)
    
    app.config.from_pyfile("settings.py", silent=True)
    app.register_blueprint(hello_world_blueprint)
    
    return app

Here, in app.py, we create a new Flask instance with instance_relative_config set to True. Per the official Flask docs, this boolean parameter tells our Flask app that any configuration files are relative to our instance folder. The instance folder is located outside our flask_docker app’s main package and can hold local data that should not be committed to version control, such as configuration secrets, database files, etc.

On the next couple of lines we configure our Flask application by calling config.from_pyfile, and pointing it to settings.py. Lastly, we register our hello_world_blueprint Flask Blueprint and return our app.

Before moving onto our Docker setup, we need to add our settings.py file found in the config directory. This file is referenced above in the call to from_pyfile. To avoid over-complicating this tutorial, let’s just configure Flask’s DEBUG mode to on and set our applications logging level to DEBUG:

# This is where we could set various config options. 
DEBUG = True

# This can be DEBUG, INFO, WARNING, ERROR or CRITICAL
LOG_LEVEL = 'DEBUG'

Our Docker Setup

Our Docker setup requires just a couple of files. The first one is a Dockerfile found inside the docker/flask_docker directory. This Dockerfile is for our Flask application. This file will define our Python environment, set some environment variables, copy a few files and directories from our project to our Docker container, install Pipenv as well as all necessary Python packages and lastly run our Flask application with gunicorn:

FROM python:3.10.4-slim-buster

ENV INSTALL_PATH /flask_docker
RUN mkdir -p $INSTALL_PATH
WORKDIR $INSTALL_PATH

ADD Pipfile .
RUN mkdir -p .venv

ADD config ${INSTALL_PATH}/config
ADD flask_docker ${INSTALL_PATH}/flask_docker

RUN pip3 --no-cache-dir install pipenv==2022.1.8
RUN pipenv lock -r > requirements.txt
RUN pip3 install --no-cache-dir -r requirements.txt
RUN rm -rf ~/.cache
RUN rm -rf /tmp/*

CMD gunicorn -b 0.0.0.0:8000 --access-logfile - "flask_docker.app:create_app()"

Next, we need to create the docker-compose.yml file in our project’s root directory. Our compose file should contain three services, one service each for MariaDB, Redis, and our Flask API as defined in the Dockerfile above. Time to create our compose file:

version: '3.9'

services:
  mariadb:
    image: mariadb:10.8.3-jammy
    platform: linux/amd64
    ports:
      - 3306:3306
    volumes:
      - mariadb_data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=changeme
      - MYSQL_PASSWORD=changemeToo
      - MYSQL_USER=flask_docker
      - MYSQL_DATABASE=flask_docker

  redis:
    image: redis:7.0.4
    platform: linux/amd64
    ports:
      - "6379:6379"
  
  flask_docker:
    build:
      context: .
      dockerfile: docker/flask_docker/Dockerfile
    platform: linux/amd64
    ports:
      - "8000:8000"
    volumes:
        - type: bind
          source: ./flask_docker
          target: /flask_docker/flask_docker
        - type: bind
          source: ./config
          target: /flask_docker/config
    command: >
      gunicorn -c "python:config.gunicorn" --reload "flask_docker.app:create_app()"

volumes:
  mariadb_data:

There are various keywords used above. Let’s go over a few of them here:

  • image: The image or repository to build this service from. In both services we specify a specific version of MariaDB and Redis. These can all be found on DockerHub. For example, all of the valid Redis images are listed here.
  • platform: The target platform containers for this service will run on. Our example uses linux/amd64 for all services.
  • ports: The ports that will be exposed for this service.
  • environment: Adds the specified environment variables inside of the service’s container. We use this flag to set our MariaDB username and passwords and also specify the database name.
  • volumes: Used to mount host paths or named volumes. We use volumes to persist data which is quite useful for something like a relational database. For more on volumes read this.
  • command: Overrides the default command when starting up our service. We use this to start our Flask application via Gunicorn. The --reload option means any code changes we make to our Flask application can be seen by simply reaccessing our Blueprints. We do not need to rebuild our services each time we make a change.

Above, in the flask_docker service portion of our compose file we override the service’s default command. Here, Gunicorn is configured via "-c python:config.gunicorn". This tells Gunicorn to look for the Python file gunicorn.py in our config directory. Let’s add this gunicorn.py file there now, defining some basic access log formatting:

# -*- coding: utf-8 -*-

bind = '0.0.0.0:8000'
accesslog = '-'
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" in %(D)sµs'

Now, we just need to build and run our containers! If we want to build our containers but not start them we simply run the following command inside our project’s root directory:

$ docker-compose build

However, if we want to run our services too, then we should run the following command:

$ docker-compose up

This will bring up all of our containers, and any logs will go right to our terminal window.

Note: If we do not want to watch the logs and need to use our terminal window after starting our services then we should run docker-compose in detatched mode by providing the -d parameter like so: $docker-compose up -d.

We can check which services are running with the docker ps command (ps is short for process status). After a successful docker-compose up we should see similar output:

$ docker ps
CONTAINER ID   IMAGE                       COMMAND                  CREATED        STATUS        PORTS                    NAMES
6a95b3ec4ee6   mariadb:10.8.3-jammy        "docker-entrypoint.s…"   1 second ago   Up 1 second   0.0.0.0:3306->3306/tcp   flask_docker_mariadb_1
ddd6a69c724b   flask_docker_flask_docker   "gunicorn -c python:…"   2 days ago     Up 1 second   0.0.0.0:8000->8000/tcp   flask_docker_flask_docker_1
51006fb3b31b   redis:7.0.4                 "docker-entrypoint.s…"   3 days ago     Up 1 second   0.0.0.0:6379->6379/tcp   flask_docker_redis_1

Now, assuming everything is up and running, when we open up a browser and head over to http://127.0.0.1:8000/ we should see the message “Hello there!”. If so, everything is working!

Lastly, if we want to stop all services this can be done via $ docker-compose stop from the command line from our project’s root directory. Please note, this tutorial does not actually make use of MariaDB and Redis, but simply shows how to start them alongside Flask using Docker Compose.

Conclusion

That wraps up how to Dockerize a multi-service Flask application! With both Docker and Docker Compose installed it takes just a simple docker-compose up to run your Flask application and all needed services.

Easy

Subscribe

Get updates on new content straight to your inbox! Unsubscribe at anytime.

* indicates required