Dockerizing Flask App
Written: 09/10/2024
I’m capturing some of my findings, configuration and work on getting a Flask app (Github: am401/jsondate) up and running on an AWS EC2 using Docker to run both the application and NGINX.
After posting this, I plan to modify this approach slightly as I plan to run multiple applications on the same EC2 instance and currently NGINX is tied to the Docker Makefiles in a way that I don’t feel this is a flexible approach.
The primary resource that I used to Dockerize the app as well as NGINX is the article Dockerizing Flask with Postgres, Gunicorn, and Nginx, which was a great resource.
After completing the setup I had the following directory structure. In this scenario I have NGINX, certbot
and my application running as a service, controlled by a Docker Makefile:
.
├── docker-compose.prod.yml
└── services
├── certbot
│ ├── conf
│ │ ├── accounts [error opening dir]
│ │ ├── archive [error opening dir]
│ │ ├── live [error opening dir]
│ │ ├── renewal
│ │ │ └── json.date.conf
│ │ └── renewal-hooks
│ │ ├── deploy
│ │ ├── post
│ │ └── pre
│ └── www
├── nginx
│ ├── Dockerfile
│ └── nginx.conf
└── web
├── Dockerfile.prod
├── manage.py
├── project
│ └── __init__.py
└── requirements.txt
Most of the configuration and setup for the app is from the page I’ve shared. Some of the changes that I made to get things running that were not documented or clear in the original guidance I was following:
My application had run into some issues where NGINX and Docker were not communicating correctly. To resolve this I created a Docker network and specifying this network in the docker compose file for both my web application and NGINX. The docker compose file I’ve created for the project where all the components are running via Docker:
version: '3.8'
services:
web:
build:
context: ./services/web
dockerfile: Dockerfile.prod
command: gunicorn --bind 0.0.0.0:5000 manage:app
networks:
my-network:
aliases:
- flask-app
expose:
- 5000
env_file:
- ./.env.prod
nginx:
build: ./services/nginx
depends_on:
- web
ports:
- 80:80
- 443:443
volumes:
- ./services/certbot/www:/var/www/certbot/
- ./services/certbot/conf/:/etc/nginx/ssl/
networks:
- my-network
certbot:
image: certbot/certbot:latest
volumes:
- ./services/certbot/www/:/var/www/certbot/
- ./services/certbot/conf/:/etc/letsencrypt/
networks:
my-network:
The above Docker Compose file is great as I’m able to start all the services that I need with the following command:
docker compose -f docker-compose.prod.yml up -d --build
The snag I ran into while trying to build the production Dockerfile
was the linting of the Flask app. The instructions use Flake8 for linting the code and what the guide did not mention is how to exclude the virtual environment that is created for the project.
This caused all sorts of linting failures while trying to run the command. Using --exclude=env
with the linter, I was able to exclude this and spin up the container without issue. The full Dockerfile
is below:
###########
# BUILDER #
###########
# pull official base image
FROM python:3.11.3-slim-buster AS builder
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONBUFFERED 1
# install system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc
# lint
RUN pip install --upgrade pip
RUN pip install flake8==6.0.0
COPY . /usr/src/app/
RUN flake8 --ignore=E501,F401 --exclude=env .
# install python dependencies
COPY ./requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels -r requirements.txt
#########
# FINAL #
#########
# pull official base image
FROM python:3.11.3-slim-buster
# create directory for the app user
RUN mkdir -p /home/app
# create the app user
RUN addgroup --system app && adduser --system --group app
# create the appropriate directories
ENV HOME=/home/app
ENV APP_HOME=/home/app/web
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# install dependencies
COPY --from=builder /usr/src/app/wheels /wheels
COPY --from=builder /usr/src/app/requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache /wheels/*
# copy project
COPY . $APP_HOME
# chown all the files to the app user
RUN chown -R app:app $APP_HOME
# change to the app user
USER app
I had some issues getting certbot
running initially but I realized that this was an error in the way I was configuring NGINX. I left out ssl
when configuring the listener for port 443
, which caused a decryption issue for requests and also had an upstream proxy error. After fixing the NGINX config file, this started to work as expected.
Incorrect:
listen 443;
listen [::]:443;
Correct:
listen 443 ssl;
listen [::]:443 ssl;
Example output of the broken data:
11.22.33.44 - - [18/Aug/2024:23:08:14 +0000] "\x16\x03\x01\x06\xF2\x01\x00\x06\xEE\x03\x03\xBCP\xCB\xEC\xEB\xA4\x894<\xC4?\x03\xDA\xFB!\x95}l\xD69\xFA\x1C\xCE@\x8F\xF5i\xBC\x9Dt!? o\xC3T\x9D\xC0\xAC7\xA4\x14=D\xF2\xDB\x0F\xE4T\x019\xBE\x9F\xD0\x0B3D|?#\x10p\xF4\xB6\x94\x00 JJ\x13\x01\x13\x02\x13\x03\xC0+\xC0/\xC0,\xC00\xCC\xA9\xCC\xA8\xC0\x13\xC0\x14\x00\x9C\x00\x9D\x00/\x005\x01\x00\x06\x85\xDA\xDA\x00\x00\x00\x0B\x00\x02\x01\x00\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00+\x00\x07\x06zz\x03\x04\x03\x03\xFE" 400 157 "-" "-" "-"
11.22.33.44 - - [18/Aug/2024:23:08:24 +0000] "GET / HTTP/1.1" 301 162 "-" "curl/8.5.0" "-"
11.22.33.44 - - [18/Aug/2024:23:08:31 +0000] "\x16\x03\x01\x02\x00\x01\x00\x01\xFC\x03\x03(0l7U\x84d~\xDB@\x9FY\xB2Ed\xB3\x19W\xB3F\xC90" 400 157 "-" "-" "-"
Some additional references that I found useful while creating this setup: