How to spin up a simple Flask app using NGINX — in 15 minutes or less

Prerequisites:

  1. Linux machine
  2. Docker Engine & Docker Compose
  3. Domain name pointed to your server
  4. Optional: Certificate, Private Key and Intermediate Certificate

Objective

Have you ever tried using NGINX? If so, then you’ll probably be familiar with the headache of issuing a certificate using Certbot as well. Thankfully, Zombie NGINX can cure such pains for you!

Zombie NGINX turns NGINX into easier-to-read configuration files, then issues a new certificate, all by itself.

The following article shows you how to create a simple Flask application, alongside static files, all using the Zombie NGINX docker container.

Step 1: Application Preparation

Let’s kick things off with a simple Flask application (which we will modify later to handle production status).

app.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Now, wouldn’t it be cool if we could distinguish each environment in our app? We think so, which is why we create a specific configuration for production — and another for development — in file config.py:

config.py

import logging
from os import environ, urandom

class Config(object):
   def __init__(self):
       logging.basicConfig(level=logging.DEBUG)

   DEBUG = False
   TESTING = False
   SECRET_KEY = environ.get("APP_SECRET_KEY", default=urandom(16))

class DevelopmentConfig(Config):
   DEBUG = True
   SECRET_KEY = 'INSECURE_FOR_LOCAL_DEVELOPMENT'

class ProductionConfig(Config):
   DEBUG = False
   TESTING = False

Before we can use this configuration, we first need to load one of these into our flask app, which we can do by adding this code to our app.py:

environment = environ.get("FLASK_ENV", default="development")
if environment == "development":
   cfg = config.DevelopmentConfig()
elif environment == "production":
   cfg = config.ProductionConfig()

app.config.from_object(cfg)

As you can see, we’re using an environmental variable called FLASK_ENV, which we haven’t set (at least, not yet, but we will set up environmental variables later in this article).
Therefore, the default value “development” is selected.
We will run our Flask app using uwsgi. To get it working, we will use two files (you can check what each file does in the uwsgi.ini documentation):

  • wsgi.py — to run our app
  • uwsgi.ini — to set up the configuration

wsgi.py

#!/usr/bin/python3

from app import app as application

if __name__ == "__main__":
   application.run()

uwsgi.ini

[uwsgi]
module = wsgi
chmod-socket = 660
vacuum = true
disable-logging = false
enable-threads = false
harakiri = 30
harakiri-verbose = true
hook-master-start = unix_signal:15 gracefully_kill_them_all
master = true
max-requests = 3000
processes = 4
socket = :3031
touch-reload = wsgi.py
uid = uwsgi_user

Important: Keep all installed Python packages in the file requirements.txt.

Step 2: Dockerizing The Application

Now that we have all the above files in the directory, it’s time to create a Docker image.

The Docker image will hold the source code of our application and install the requirements — for this; we will use a Debian-based image with Python pre-installed.

(Note: Feel free to use any image that suits your needs though keep in mind: the smaller, the better.)

Dockerfile

FROM python:3.7-slim

RUN apt-get update

RUN mkdir /app/
ADD requirements.txt /app/

WORKDIR /app/
RUN pip install -r requirements.txt
COPY ./ /app/

RUN useradd --home-dir=/app --uid=1000 uwsgi_user

USER uwsgi_user

CMD uwsgi --strict uwsgi.ini

Now that we have a Docker image alongside the source code, we can create the docker-compose files that will hold the configuration for the containers.
Let’s kick this process off by creating a base docker-compose file, which we will use to store the parts for both development and production.

docker-compose.yml

version: '3.7'
services:
 webapp:
   container_name: hello-webapp
   build: .
   restart: always
   networks:
     - back-tier

 nginx:
   container_name: hello-nginx
   image: typeai/zombie-nginx
   restart: always
   networks:
     - back-tier
   depends_on:
     - webapp

networks:
 back-tier:

By this point, we have configured two containers:

  1. A webapp that will build the Docker file from within the directory;
  2. And the second uses a typeai/zombie-nginx image (find documentation here).

However, they both need to be in a single network to allow NGINX to reach the Flask application.

Therefore, before we start creating the docker-compose files for development and production, we first need to create two configuration files for Zombie NGINX: one for development, the other for production.

Let’s start with the development configuration file.

How to spin up a simple Flask app using NGINX — in 15 minutes or less

Development Configuration File

webapp-zombie-dev.yml

servers:
 webapp:
   server_name: dlabs.ai  # for development purpose this is not much relevant
   check_host_header: no
   upstream: uwsgi://webapp:3031 
   static_files: /var/www/webapp-static
   tls: no

We want our Zombie NGINX to have an upstream link to our uwsgi running application. And as we have previously set up a socket to open on port 3031, let’s assume our application is more than a basic, ‘Hello World!’ application.

As a matter of fact, we know we want to host static files as well as media: to do so, all we need to add is an entry with a location where we can store these files (we will also mount a docker volume in this location, later on).

Note: We do not need HTTPS for development purposes, so let’s leave it off in this case.

Production Configuration File

The production configuration file will hold all the information on the domain where it will be hosted.

webapp-zombie-prod.yml

servers:
 webapp:
   server_name: dlabs.ai
   check_host_header: yes
   upstream: uwsgi://webapp:3031
   static_files: /var/www/webapp-static
   tls: auto

In this file, we need to provide the correct server name, check the host header, point to the proper uwsgi socket, host static files, and enable the Certbot (https://certbot.eff.org/) certificate issue for our domain.

Certbot will automatically issue a certificate for our domain (server_name).

Note: If you want to use your own certificate (i.e. not Certbot-issued), you will have to change tls from ‘auto’ to ‘mapping’ as in the example below.

tls:
 - certificate: certificate.txt
   key: private.txt
   root_chain: root_chain.txt

Keep in mind that certificate.txt should also store the value of the intermediate certificate.

Now, let’s create the docker-component-specific files for both development and production.

docker-compose.dev.yml

version: '3.7'

services:
 webapp:
   environment:
     - FLASK_ENV=development
 nginx:
   volumes:
     - ./webapp-zombie-dev.yml:/etc/appconf/nginx.yml:ro
     - ./static:/var/www/webapp-static:ro
   environment:
     - FLASK_ENV=development
   ports:
     - 0.0.0.0:80:80
  • For the webapp container: we set Flask mode to “development” using the environment variable;
  • For the NGINX container: we mount our Zombie configuration file to /etc/appconf/nginx.yml from which we can create a proper containerized NGINX configuration.

We will also mount a directory with static files in the same location in which we specified the Zombie configuration.
Plus, we will expose port 80 to our local machine.

docker-compose.prod.yml

version: '3.7'

services:
 webapp:
   environment:
     - FLASK_ENV=production

 nginx:
   volumes:
     - ./backend-nginx-prod.yml:/etc/appconf/nginx.yml:ro
     - ./static:/var/www/webapp-static:ro
   environment:
     - LETSENCRYPT_EMAIL=admin@dlabs.ai
   ports:
     - 0.0.0.0:80:80
     - 0.0.0.0:443:443

In the above example, we set our Flask app to “production” mode. We can set a custom secret key (simply set the APP_SECRET_KEY variable). Further, we can mount the configuration (production) in our container and static files directory.

Finally, as we want our application to be secure, we now expose port 443.

Step 3: Development

To start the application in development mode, run it in the project directory:

$ docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

Step 4: Deployment

To deploy our application, we clone our repository to a production environment; then, we go to the root of the application and run the command:

$ docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Doing the above will start both containers in production mode — as daemons.

Step 5: Bonus Feature

As a final step, you can create a simple Makefile to run the above commands.

Makefile

start_dev:
	docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d

start_prod:
	docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d

stop:
	docker-compose down

From this point, onwards — start the app in development mode by running:

$ make start_dev

(Note: You may need to install Makefile to your OS.)

how to implement ai

Marek Czaplicki

Marek Czaplicki is a Backend Developer eager to dive into Frontend and DevOps culture.

Read more on our blog