In this day and age it is nice to deploy your services in containers. It takes most of the headache of configuring servers and environments away. But sometimes things just don't work the way you expect them to and sometimes it's hard to find answers to the questions you have.

That's what happened to me when I was trying to deploy one small web service built on Flask to AWS ECS service. It just would not work in a way I wanted it to. So, let's talk about it!

All the code I'm showing is available on GitHub here.

Let's assume some basic Hello World Flask app like this:

from flask import Flask
app = Flask(__name__)

app.config['DEBUG'] = True

@app.route("/")
def hello_world():
    return "Hello, World!"

if __name__ == "__main__":
    # use 0.0.0.0 to use it in container
    app.run(host='0.0.0.0')
app/app.py
flask==1.0.2
requirements.txt

It's simple enough. For fast and easy development we can use built-in development server from Flask, however it is not production-ready server. Let's see how we can make a Dockerfile that would use this built-in server:

FROM python:3.8-slim
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app
RUN pip3 install -r requirements.txt
ADD . /app
EXPOSE 5000
ENTRYPOINT ["python", "app/app.py"]
Dockerfile

You can see the code for this here. If we run the container and send requests to it, we can see it is working:

docker build --tag flask-app .
docker run -p 5000:5000 flask-app
curl http://localhost:5000

This is good enough for development purposes, but let's say you need to take this app to production. This server simply cannot handle the workload, it works on just one thread and multiple users using application that is a bit more complicated, for example accesses a database, will be extremely slow, will hang and eventually might crash.

What you want to use in this case is WSGI server. You can read more about WSGI servers here. We will be using Gunicorn as our WSGI server for this example. Let's rework out application now to use it.

First suggestion I have is to make a separate file where we will load our application and prepare it for running in production. That way it's detached from your app and it avoids name space confusion, since almost everything is named "app" 😄. Let's make new file named "wsgi.py":

from .app import app

# do some production specific things to the app
app.config['DEBUG'] = False
app/wsgi.py

And we should also add Gunicorn to our requirements.txt, create Gunicorn config file and update Dockerfile to  run the app on Gunicorn.

flask==1.0.2
gunicorn==20.0.4
requirements.txt
bind = "0.0.0.0:5000"
workers = 4
threads = 4
timeout = 120
gunicorn_config.py
FROM python:3.8-slim
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app
RUN pip3 install -r requirements.txt
ADD . /app
EXPOSE 5000
ENTRYPOINT ["gunicorn", "--config", "gunicorn_config.py", "app.wsgi:app"]
Dockerfile

You can get the code for this right here. We will build it and will run it the same way as before. Now we have python application that is much sturdier in production environment, it's much faster  serving responses when there are more users using your app since it is using more threads and has multiple workers to serve those requests.

However this is not perfect solution and it has some issues that will have to be solved:

No SSL support

This is a big issue in day of modern web development. Everything should be encrypted. Everything. It is no one's business to see unencrypted data traversing The Net. Gunicorn supports SSL, but what I like to do is to use NGINX as a reverse proxy in front of Gunicorn. This way Nginx can handle web server tasks and Gunicorn can handle application tasks. You will have to configure Nginx to use SSL, but Nginx gives you so much more freedom and much more configuration options to configure everything the way you want it. Keep an eye on this blog, I have an article in the works about how to configure Nginx with Let's Encrypt certificates in Docker to make it easy to deploy.

Weird issues with running Gunicorn in Docker

When I was deploying my last app, I found some inconsistent behavior in my Docker container. That was what has prompted this article. I just could not for the life of me get Gunicorn working in the container. It just would not start. After hours of trying almost everything and googling around, I found a way to fix it.

We need to put the command of starting Gunicorn to separate script file and then just invoke that script. It seems to solve all the issues. Let's make entrypoint.sh:

#!/bin/bash
exec gunicorn --config /app/gunicorn_config.py app.wsgi:app
entrypoint.sh

And update our Dockerfile:

FROM python:3.8-slim
RUN mkdir /app
WORKDIR /app
ADD requirements.txt /app
RUN pip3 install -r requirements.txt
ADD . /app
EXPOSE 5000
RUN chmod +x ./entrypoint.sh
ENTRYPOINT ["sh", "entrypoint.sh"]

You can see the code for this here. After this change, Gunicorn now works properly and without any issues.

If you know why this is helping with running Gunicorn in Docker container, feel free to leave comment down below!

M.

Update: Redditor skiutoss pointed out some awesome ready-mage images from tiangolo on GitHub, specifically image meinheld-gunicorn-flask-docker could be a great starting point for porting your Flask app into Docker container.