Pooya Mohammadi Kazaj
Pooya Mohammadi Kazaj

Reputation: 153

Why running flask app.run with gunicorn and uwsgi is problematic?

There is an idea that indicates not to run flask app in production with gunicorn or uwsgi. Tiangolo has mentioned in one of his repositories that app.run should be used just for development, not deployment or production. Link to Tiangolo's comment on this topic His code is as follows:

from flask import Flask

app = Flask(__name__)

from .core import app_setup


if __name__ == "__main__":
    # Only for debugging while developing
    app.run(host="0.0.0.0", debug=True, port=80)

My Question is why running flask app can be problematic(the last line of the aforementioned code) and should be removed or commented out. I carried out a series of tests and found out that with or with running flask app at production the number of generated processes are the same. Here are the outputs of my tests with gunicorn.

Here is my docker-compose. At line 12 you can check how I run gunicorn.

version: "3.7"
services:
  face:
    build: ./app
    container_name: face
    restart: always
    expose:
      - 660
    environment:
      - ENDPOINT=/face
      - FACE_DETECTION_MODEL=MTCNNTorchFaceDetector
    command: gunicorn --workers=2 --threads 1 -b 0.0.0.0:660 entry_point:app --worker-class sync

  nginx:
    build: ./nginx
    container_name: nginx
    restart: always
    ports:
      - 8000:80
    depends_on:
      - face

flask gunicorn 2 processes with app.run:

root@e6c7d9ef03cc:/app# cat entry_point.py 
from endpoints import FaceDetection
from base_app import app, api, ENDPOINT

api.add_resource(FaceDetection, ENDPOINT)

if __name__ == '__main__':
    app.run("127.0.0.1", port=3000)
root@e6c7d9ef03cc:/app# lsof -i
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
gunicorn   1 root    5u  IPv4 471109      0t0  TCP *:660 (LISTEN)
gunicorn   7 root    5u  IPv4 471109      0t0  TCP *:660 (LISTEN)
gunicorn   8 root    5u  IPv4 471109      0t0  TCP *:660 (LISTEN)
root@e6c7d9ef03cc:/app#

As you can see in the code, there are 3 processors which one of them is the master and the other two are the workers.

flask gunicorn 2 processes without app.run(it's commented out):

root@e6c7d9ef03cc:/app# cat entry_point.py 
from endpoints import FaceDetection
from base_app import app, api, ENDPOINT

api.add_resource(FaceDetection, ENDPOINT)

# if __name__ == '__main__':
#     app.run("127.0.0.1", port=3000)
root@e6c7d9ef03cc:/app# lsof -i
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
gunicorn   1 root    5u  IPv4 466580      0t0  TCP *:660 (LISTEN)
gunicorn   8 root    5u  IPv4 466580      0t0  TCP *:660 (LISTEN)
gunicorn   9 root    5u  IPv4 466580      0t0  TCP *:660 (LISTEN)
root@e6c7d9ef03cc:/app#

The outputs are the same and discarding the master processor there are only 2 main workers up and running.
The same happens for gunicorn applications with 4 processors.
With app.run:

root@f1d9f2d3a5d0:/app# cat entry_point.py 
from endpoints import FaceDetection
from base_app import app, api, ENDPOINT

api.add_resource(FaceDetection, ENDPOINT)

if __name__ == '__main__':
    app.run("127.0.0.1", port=3000)
root@f1d9f2d3a5d0:/app# lsof -i
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
gunicorn   1 root    5u  IPv4 484435      0t0  TCP *:660 (LISTEN)
gunicorn   7 root    5u  IPv4 484435      0t0  TCP *:660 (LISTEN)
gunicorn   8 root    5u  IPv4 484435      0t0  TCP *:660 (LISTEN)
gunicorn   9 root    5u  IPv4 484435      0t0  TCP *:660 (LISTEN)
gunicorn  10 root    5u  IPv4 484435      0t0  TCP *:660 (LISTEN)
root@f1d9f2d3a5d0:/app#

Without app.run:

root@f1d9f2d3a5d0:/app# cat entry_point.py 
from endpoints import FaceDetection
from base_app import app, api, ENDPOINT

api.add_resource(FaceDetection, ENDPOINT)

# if __name__ == '__main__':
#     app.run("127.0.0.1", port=3000)
root@f1d9f2d3a5d0:/app# lsof -i
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
gunicorn   1 root    5u  IPv4 476011      0t0  TCP *:660 (LISTEN)
gunicorn   8 root    5u  IPv4 476011      0t0  TCP *:660 (LISTEN)
gunicorn   9 root    5u  IPv4 476011      0t0  TCP *:660 (LISTEN)
gunicorn  10 root    5u  IPv4 476011      0t0  TCP *:660 (LISTEN)
gunicorn  11 root    5u  IPv4 476011      0t0  TCP *:660 (LISTEN)
root@f1d9f2d3a5d0:/app#

To reproduce the issue clone face-detection-flask-gunicron-docker-compose and run the following commands:

# get the project
git clone https://github.com/pooya-mohammadi/face-detection-flask-nginx-gunicorn-docker.git
# cd to project's root 
cd ace-detection-flask-nginx-gunicorn-docker
# build the images and run the project
sudo docker-compose up --build
# open a new terminal
sudo docker ps -a 
# get the container-id for face-detection_face
# open a bash command with that container-id
sudo docker exec -it <container-i> bash # This opens a new command line
# install lsof to view listening services
apt-get install lsof
# view app.run condition
cat entry_point.py | grep app.run # The default is not commented. 
lsof -i  

Comment the app.run in the entry_point.py and run the process again.
To change the number of workers, modify line 12 in docker-compose.yml.

Upvotes: 4

Views: 1226

Answers (1)

Pooya Mohammadi Kazaj
Pooya Mohammadi Kazaj

Reputation: 153

After digging around with gunicorn library for a while, I noticed that gunicorn uses import.import_module to import the entrypoint module(The module that contains the app, in my case entry_point.py) and the codes under if __name__ == '__main__': won't be executed and it's pretty safe to put anything there. Link to import_app method in gunicorn library. This method is called from method load_wsgiapp link to load_wsgiapp inside the primary runner class WSGIApplication Link to WSGIApplication class.
As I noticed Tiangolo meant that using flask app directly for production is not safe because:

The flask application server is not developed or tested for production performance or security.

Answer from Justin Triplett(discord)

Upvotes: 4

Related Questions