Uploading Image to Server That Is Too Big Flask Python
As a machine learning engineer and figurer vision expert, I find myself creating APIs and even spider web apps with Flask surprisingly often. In this post, I want to share some tips and useful recipes for building a complete production-ready Flask awarding.
We will cover the following topics:
- Configuration direction. Whatsoever real-life application has a lifecycle with specific stages—at the very least, information technology would exist development, testing, and deployment. On each stage, the application code should piece of work in a slightly different environs, which requires having a different set of settings, like database connectedness strings, external APIs keys, and URLs.
- Self-hosting Flask application with Gunicorn. Although Flask has a born web server, as we all know, information technology's not suitable for production and needs to be put behind a real web server able to communicate with Flask through a WSGI protocol. A common choice for that is Gunicorn—a Python WSGI HTTP server.
- Serving static files and proxying asking with Nginx. While being an HTTP web server, Gunicorn, in plow, is an application server not suited to face the web. That'south why we need Nginx as a reverse proxy and to serve static files. In case we demand to scale up our application to multiple servers, Nginx volition accept care of load balancing as well.
- Deploying an app inside Docker containers on a dedicated Linux server. Containerized deployment has been an essential part of software design for quite a long fourth dimension now. Our application is no different and will be neatly packaged in its own container (multiple containers, in fact).
- Configuring and deploying a PostgreSQL database for the application. Database structure and migrations will be managed by Alembic with SQLAlchemy providing object-relational mapping.
- Setting upwards a Celery task queue to handle long-running tasks. Every application will somewhen require this to offload fourth dimension or computation intensive processes—be it mail sending, automatic database housekeeping or processing of uploaded images—from web server threads on external workers.
Creating the Flask App
Let'due south start by creating an application code and assets. Please note that I will not address proper Flask application construction in this post. The demo app consist of minimal number of modules and packages for the sake of brevity and clarity.
Outset, create a directory structure and initialize an empty Git repository.
mkdir flask-deploy cd flask-deploy # init GIT repo git init # create folder structure mkdir static tasks models config # install required packages with pipenv, this volition create a Pipfile pipenv install flask flask-restful flask-sqlalchemy flask-migrate celery # create test static asset repeat "Hello Globe!" > static/hello-earth.txt
Adjacent, nosotros'll add the code.
config/__init__.py
In the config module, nosotros'll ascertain our tiny configuration direction framework. The idea is to make the app carry co-ordinate to configuration preset selected past the APP_ENV
environs variable, plus, add an option to override any configuration setting with a specific environment variable if required.
import os import sys import config.settings # create settings object corresponding to specified env APP_ENV = os.environ.become('APP_ENV', 'Dev') _current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))() # re-create attributes to the module for convenience for atr in [f for f in dir(_current) if not '__' in f]: # environs can override anything val = os.environ.get(atr, getattr(_current, atr)) setattr(sys.modules[__name__], atr, val) def as_dict(): res = {} for atr in [f for f in dir(config) if non '__' in f]: val = getattr(config, atr) res[atr] = val return res
config/settings.py
This is a ready of configuration classes, one of which is selected by the APP_ENV
variable. When the application runs, the code in __init__.py
volition instantiate 1 of these classes overriding the field values with specific environment variables, if they are present. We will use a concluding configuration object when initializing Flask and Celery configuration subsequently.
grade BaseConfig(): API_PREFIX = '/api' TESTING = False DEBUG = False class DevConfig(BaseConfig): FLASK_ENV = 'development' DEBUG = True SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@broker-rabbitmq//' course ProductionConfig(BaseConfig): FLASK_ENV = 'production' SQLALCHEMY_DATABASE_URI = 'postgresql://db_user:db_password@db-postgres:5432/flask-deploy' CELERY_BROKER = 'pyamqp://rabbit_user:rabbit_password@broker-rabbitmq//' CELERY_RESULT_BACKEND = 'rpc://rabbit_user:rabbit_password@banker-rabbitmq//' class TestConfig(BaseConfig): FLASK_ENV = 'evolution' TESTING = Truthful DEBUG = Truthful # make celery execute tasks synchronously in the same process CELERY_ALWAYS_EAGER = True
tasks/__init__.py
The tasks parcel contains Celery initialization code. Config package, which will already have all settings copied on module level upon initialization, is used to update Celery configuration object in case nosotros will accept some Celery-specific settings in the time to come—for example, scheduled tasks and worker timeouts.
from celery import Celery import config def make_celery(): celery = Celery(__name__, broker=config.CELERY_BROKER) celery.conf.update(config.as_dict()) return celery celery = make_celery()
tasks/celery_worker.py
This module is required to start and initialize a Celery worker, which volition run in a split Docker container. It initializes the Flask application context to have admission to the same surround equally the application. If that's not required, these lines tin can be safely removed.
from app import create_app app = create_app() app.app_context().button() from tasks import celery
api/__init__.py
Next goes the API package, which defines the Balance API using the Flask-Restful package. Our app is simply a demo and will take only two endpoints:
-
/process_data
– Starts a dummy long operation on a Celery worker and returns the ID of a new job. -
/tasks/<task_id>
– Returns the status of a task past task ID.
import time from flask import jsonify from flask_restful import Api, Resource from tasks import celery import config api = Api(prefix=config.API_PREFIX) form TaskStatusAPI(Resource): def become(self, task_id): task = celery.AsyncResult(task_id) return jsonify(task.consequence) form DataProcessingAPI(Resource): def mail service(cocky): task = process_data.delay() render {'task_id': chore.id}, 200 @celery.task() def process_data(): time.sleep(60) # data processing endpoint api.add_resource(DataProcessingAPI, '/process_data') # job status endpoint api.add_resource(TaskStatusAPI, '/tasks/<string:task_id>')
models/__init__.py
Now we'll add a SQLAlchemy model for the User
object, and a database engine initialization code. The User
object won't exist used by our demo app in any meaningful manner, but nosotros'll demand it to make certain database migrations piece of work and SQLAlchemy-Flask integration is set up correctly.
import uuid from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() form User(db.Model): id = db.Column(db.String(), primary_key=Truthful, default=lambda: str(uuid.uuid4())) username = db.Column(db.String()) email = db.Column(db.String(), unique=True)
Note how UUID is generated automatically equally an object ID past default expression.
app.py
Finally, let'due south create a primary Flask application file.
from flask import Flask logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s]: {} %(levelname)due south %(message)due south'.format(os.getpid()), datefmt='%Y-%m-%d %H:%M:%S', handlers=[logging.StreamHandler()]) logger = logging.getLogger() def create_app(): logger.info(f'Starting app in {config.APP_ENV} environs') app = Flask(__name__) app.config.from_object('config') api.init_app(app) # initialize SQLAlchemy db.init_app(app) # define hullo world page @app.route('/') def hello_world(): return 'Hello, Earth!' render app if __name__ == "__main__": app = create_app() app.run(host='0.0.0.0', debug=Truthful)</td> </tr> <tr> <td>
Hither we are:
- Configuring basic logging in a proper format with time, level and process ID
- Defining the Flask app creation function with API initialization and "Hi, world!" folio
- Defining an entry betoken to run the app during development time
wsgi.py
As well, we will need a separate module to run Flask application with Gunicorn. Information technology will take but two lines:
from app import create_app app = create_app()
The application code is ready. Our side by side pace is to create a Docker configuration.
Building Docker Containers
Our application will require multiple Docker containers to run:
- Awarding container to serve templated pages and expose API endpoints. Information technology's a good thought to split these 2 functions on the production, but we don't have whatsoever templated pages in our demo app. The container volition run Gunicorn spider web server which volition communicate with Flask through WSGI protocol.
- Celery worker container to execute long tasks. This is the same application container, but with custom run command to launch Celery, instead of Gunicorn.
- Celery vanquish container—similar to in a higher place, but for tasks invoked on a regular schedule, such every bit removing accounts of users who never confirmed their email.
- RabbitMQ container. Celery requires a message broker to communicate between workers and the app, and store task results. RabbitMQ is a common choice, merely you lot as well can use Redis or Kafka.
- Database container with PostgreSQL.
A natural way to easily manage multiple containers is to use Docker Compose. But first, nosotros will need to create a Dockerfile to build a container image for our application. Allow's put it to the project directory.
FROM python:3.7.2 RUN pip install pipenv Add . /flask-deploy WORKDIR /flask-deploy RUN pipenv install --arrangement --skip-lock RUN pip install gunicorn[gevent] EXPOSE 5000 CMD gunicorn --worker-grade gevent --workers 8 --bind 0.0.0.0:5000 wsgi:app --max-requests 10000 --timeout five --keep-live 5 --log-level info
This file instructs Docker to:
- Install all dependencies using Pipenv
- Add an application folder to the container
- Expose TCP port 5000 to the host
- Set the container's default startup command to a Gunicorn telephone call
Permit's discuss more what happens in the last line. It runs Gunicorn specifying the worker course every bit gevent. Gevent is a lightweight concurrency lib for cooperative multitasking. It gives considerable operation gains on I/O bound loads, providing better CPU utilization compared to Os preemptive multitasking for threads. The --workers
parameter is the number of worker processes. Information technology's a good idea to set it equal to an number of cores on the server.
In one case we accept a Dockerfile for application container, nosotros tin can create a docker-compose.yml
file, which volition define all containers that the application volition require to run.
version: '3' services: broker-rabbitmq: image: "rabbitmq:3.vii.14-direction" environment: - RABBITMQ_DEFAULT_USER=rabbit_user - RABBITMQ_DEFAULT_PASS=rabbit_password db-postgres: image: "postgres:11.2" environment: - POSTGRES_USER=db_user - POSTGRES_PASSWORD=db_password migration: build: . environment: - APP_ENV=${APP_ENV} command: flask db upgrade depends_on: - db-postgres api: build: . ports: - "5000:5000" surround: - APP_ENV=${APP_ENV} depends_on: - banker-rabbitmq - db-postgres - migration api-worker: build: . command: celery worker --workdir=. -A tasks.celery --loglevel=info environment: - APP_ENV=${APP_ENV} depends_on: - banker-rabbitmq - db-postgres - migration api-beat: build: . command: celery beat -A tasks.celery --loglevel=info surround: - APP_ENV=${APP_ENV} depends_on: - broker-rabbitmq - db-postgres - migration
We divers the following services:
-
banker-rabbitmq
– A RabbitMQ message broker container. Connection credentials are defined by environment variables -
db-postgres
– A PostgreSQL container and its credentials -
migration
– An app container which will perform the database migration with Flask-Migrate and get out. API containers depend on information technology and volition run afterwards. -
api
– The main application container -
api-worker
andapi-beat
– Containers running Celery workers for tasks received from the API and scheduled tasks
Each app container volition too receive the APP_ENV
variable from the docker-etch upwards
command.
Once nosotros have all application assets ready, permit'south put them on GitHub, which will help us deploy the code on the server.
git add * git commit -a -m 'Initial commit' git remote add together origin git@github.com:your-proper noun/flask-deploy.git git push button -u origin master
Configuring the Server
Our lawmaking is on a GitHub at present, and all that's left is to perform initial server configuration and deploy the awarding. In my example, the server is an AWS instance running AMI Linux. For other Linux flavors, instructions may differ slightly. I also assume that server already has an external IP accost, DNS is configured with A record pointing to this IP, and SSL certificates are issued for the domain.
Security tip: Don't forget to let ports 80 and 443 for HTTP(Due south) traffic, port 22 for SSH in your hosting console (or using iptables
) and close external admission to all other ports! Be sure to practise the same for the IPv6 protocol!
Installing Dependencies
First, we'll need Nginx and Docker running on the server, plus Git to pull the code. Permit's login via SSH and use a bundle director to install them.
sudo yum install -y docker docker-compose nginx git
Configuring Nginx
Side by side stride is to configure Nginx. The chief nginx.conf
configuration file is often good equally-is. Withal, exist certain to check if information technology suits your needs. For our app, we'll create a new configuration file in a conf.d
folder. Top-level configuration has a directive to include all .conf
files from it.
cd /etc/nginx/conf.d sudo vim flask-deploy.conf
Hither is a Flask site configuration file for Nginx, batteries included. It has the following features:
- SSL is configured. Yous should have valid certificates for your domain, e.g., a free Allow's Encrypt certificate.
-
www.your-site.com
requests are redirected toyour-site.com
- HTTP requests are redirected to secure HTTPS port.
- Reverse proxy is configured to pass requests to local port 5000.
- Static files are served past Nginx from a local binder.
server { listen fourscore; listen 443; server_name world wide web.your-site.com; # bank check your certificate path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers Loftier:!aNULL:!MD5; # redirect to not-www domain return 301 https://your-site.com$request_uri; } # HTTP to HTTPS redirection server { heed fourscore; server_name your-site.com; return 301 https://your-site.com$request_uri; } server { mind 443 ssl; # check your document path! ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt; ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers High:!aNULL:!MD5; # affects the size of files user tin can upload with HTTP Mail client_max_body_size 10M; server_name your-site.com; location / { include /etc/nginx/mime.types; root /home/ec2-user/flask-deploy/static; # if static file not found - laissez passer asking to Flask try_files $uri @flask; } location @flask { add_header 'Access-Control-Let-Origin' '*' ever; add_header 'Admission-Control-Allow-Methods' 'Go, Mail, PUT, DELETE, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,10-Requested-With,If-Modified-Since,Enshroud-Control,Content-Type,Range,Authorisation'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; proxy_read_timeout 10; proxy_send_timeout 10; send_timeout threescore; resolver_timeout 120; client_body_timeout 120; # set up headers to pass request info to Flask proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $remote_addr; proxy_redirect off; proxy_pass http://127.0.0.ane:5000$uri; } }
After editing the file, run sudo nginx -s reload
and see if there are any errors.
Setting Up GitHub Credentials
It is a proficient practise to take a separate "deployment" VCS account for deploying the projection and CI/CD organization. This way yous don't adventure by exposing your own account'south credentials. To further protect the project repository, you can likewise limit permissions of such business relationship to read-only access. For a GitHub repository, you'll need an arrangement account to do that. To deploy our demo application, we'll just create a public key on the server and register it on GitHub to get access to our project without entering credentials every time.
To create a new SSH fundamental, run:
cd ~/.ssh ssh-keygen -b 2048 -t rsa -f id_rsa.pub -q -N "" -C "deploy"
Then log in on GitHub and add your public key from ~/.ssh/id_rsa.pub
in business relationship settings.
Deploying an App
The concluding steps are pretty straightforward—we demand to go awarding code from GitHub and commencement all containers with Docker Compose.
cd ~ git clone https://github.com/your-name/flask-deploy.git git checkout master APP_ENV=Production docker-etch upwardly -d
It might be a proficient idea to omit -d
(which starts the container in discrete style) for a first run to see the output of each container right in the terminal and check for possible bug. Another option is to inspect each individual container with docker logs
afterward. Let's see if all of our containers are running with docker ps.
Great. All 5 containers are upward and running. Docker Etch assigned container names automatically based on the service specified in docker-compose.yml. At present information technology's time to finally exam how the entire configuration works! It's all-time to run the tests from an external machine to brand certain the server has correct network settings.
# test HTTP protocol, you lot should get a 301 response gyre your-site.com # HTTPS request should return our Hello World message curl https://your-site.com # and nginx should correctly send test static file: curl https://your-site.com/hello-world.txt
That's it. We have a minimalistic, just fully production-ready configuration of our app running on an AWS example. Hope information technology will aid you to start building a existent-life application apace and avoid some mutual mistakes! The complete code is available on a GitHub repository.
Conclusion
In this article, nosotros discussed some of the best practices of structuring, configuring, packaging and deploying a Flask application to production. This is a very big topic, impossible to fully embrace in a single blog mail service. Here is a list of important questions we didn't accost:
This article doesn't cover:
- Continuous integration and continuous deployment
- Automatic testing
- Log shipping
- API monitoring
- Scaling up an awarding to multiple servers
- Protection of credentials in the source lawmaking
However, you can larn how to do that using some of the other dandy resource on this blog. For example, to explore logging, run across Python Logging: An In-Depth Tutorial, or for a general overview on CI/CD and automated testing, meet How to Build an Constructive Initial Deployment Pipeline. I go out the implementation of these as an exercise to you, the reader.
Thanks for reading!
Source: https://www.toptal.com/flask/flask-production-recipes