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!
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.