Create Flask app with uWSGI, Nginx, Certbot for SSL and all this with docker

Rafał Łagowski
5 min readApr 26, 2019

Creating Basic Flask with Jinja2, Blueprint, some templates and getting content from API

I had to write some program that shows different contents for different domains. So, for example, if you call example.de the content is in German and if you call example.fr the content is in French. First, we need to create a structure for our project.

where we have myapp folder with the application, data folder with all configs and certificates for SSL (we’ll create it after), docker folder with Dockerfiles

Let’s create app. First, we create a basic structure of folders and files. This structure will create web page getting content from remote API.

+-- myapp
| +-- frontend
| +-- api
| +-- content.py
| +-- templates
| +-- index.html
| +-- routes.py
| +-- static
| +-- ...
| +-- app.py
| +-- requirement.txt

Out app.py where we import Blueprint — is like this:

from flask import Blueprint, Flask
from frontend import frontend_blueprint
app = Flask(__name__)
app.register_blueprint(frontend_blueprint)

if __name__ == '__main__':
app.run()

Our routes.py. We define the route for GET method and render with this data an index.html template

from flask import render_template, session, redirect, url_for, flash, request, jsonify
import requests
from . import frontend_blueprint
from .api.Content import Content

@frontend_blueprint.route('/', methods=['GET'])
def home():
try:
content = Content.get_content(session)
except requests.exceptions.ConnectionError:
print("Not connected")
content={"title": "No title", "text": "No text"}
return render_template('index.html', content=content)

Now we have to define our templates. Let’s define base of out html — so we call it base.html — Here we can define all hardcoded elements like styles or javascripts

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ content.title }}</title>
</head>

<body>
{% block content %}
{% endblock %}
</body>
</html>

Now we will extend base html with file layout.html — here we can define elements upper level, related with page — for example if is public or private

{% extends "base.html" %}

{% block content %}
{% block pageContent %}
{% endblock %}
{% endblock %}

and now we can extend layout.html with index.html — where we have the proper definition of contents

{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}

{% block pageContent %}
{% with content=content %}
{% include 'navigation.html' %}
{% endwith %}
{% include 'body.html' %}
{% endblock %}

here we need as we see define two files more — navigation.html

<div>NAVIGATION</div>

and body.html

<div>There is Content: {{ content.text }}</div>

Now we need to get content for our web. We make this in api/Content.py — here we call API or if fail we return a default content

import requests
import os


class Content:

@staticmethod
def get_content(session_info):
try:
response = requests.request(method="GET", url=os.environ['API_ADDRESS'])
content = response.json()
except:
content = { "title": "Default Title", "text": "Default Text" }

return content

To start this app we need install dependencies and simply run

python flask_app/app.py
* Serving Flask app “app” (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

You can see all this code in this state on Github

Adding uWSGI

When we start on production we need to serve our web with uWSGI and not with proper Flask. This is a short step. We have to ensure that in docker we will have a uWSGI==2.0.17.1 library- so look at it in requirements.txt

For that, we need uwsgi.ini file inside of flask_app folder

;/flask_app/uwsgi.ini[uwsgi]protocol = uwsgi
plugins = python
; This is the name of our Python file
; minus the file extension
module = app
; This is the name of the variable
; in our script that will be called
callable = app
uid = www-data
gid = www-data
master = true
; Set uWSGI to start up 5 workers
processes = 5
; We use the port 5000 which we will
; then expose on our Dockerfile
socket = 0.0.0.0:5000
vacuum = true
die-on-term = true

Now uwsgi will communicate with Flask, but we need to access to uwsgi by nginx.

The best what we can to do is create a docker in this moment

Putting all in Docker

Next step could be dockerize this solution. For that we create Dockerfile in root directory

FROM python:3.7-slim

COPY ./flask_app/requirements.txt /app/requirements.txt

WORKDIR /app

RUN apt-get clean \
&& apt-get -y update \
&& pip install --upgrade pip \
&& apt-get -y install python3-dev \
&& apt-get -y install build-essential \
&& pip install -r requirements.txt \
&& rm -rf /var/cache/apk/*

COPY
./flask_app /app

CMD ["uwsgi", "--ini", "uwsgi.ini"]

We can build docker image

docker build -f Dockerfile -t flask_app:1.0.2 .

and we can run it

docker run -d -rm --name flask_app -p 5000:5000/tcp flask_app:1.0.0

but, the best solution is to create a docker-compose.yml file in root directory

version: '3'

services:
nginx:
image: nginx:1.15-alpine
depends_on:
- flask_app
ports:
- "80:80"
volumes:
- ./data/nginx:/etc/nginx/conf.d

flask_app:
image: flask_app
build:
context: .
dockerfile: ./Dockerfile

as we see we need to create data directory and inside nginx directory (the names are up to you — only must be the same as in docker-compose.yml file) and inside an app.conf where we have to configure howto nginx can comunicate with uwsgi. Here is the file.

server {
listen 80;
server_name localhost;


location / {
try_files $uri @app;
}
location @app {
include uwsgi_params;
uwsgi_pass flask_app:5000;
}
}

Let’s see if this works

docker-compose up -d

or

docker-compose up

if you want to see the output. Now you can access your web launched in docker.

This state you can see in here in Github

SSL and Certbot

Really I used a solution of Philipp — this is his post. And there is my little modification.

So as said Philipp we modify docker-compose.yml

version: '3'

services:
nginx:
image: nginx:1.15-alpine
depends_on:
- flask_app
ports:
- "80:80"
- "443:443"
volumes:
- ./data/nginx:/etc/nginx/conf.d
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot

certbot:
image: certbot/certbot
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot

flask_app:
image: flask_app
build:
context: .
dockerfile: ./Dockerfile
environment:
API_ADDRESS: ${API_ADDRESS}

And we create .env file — there we can store an address op API point used for get text and title. This file normally could not be stored on Github but I will do that for show you all structure. So .env file is like this

API_ADDRESS=http://123.123.123.123/v1/mypoint

Now about details, you can read in Philipp post but basically, you have to download this file

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh

Then run chmod +x init-letsencrypt.sh and sudo ./init-letsencrypt.sh.

I had some problems with this script and I had to modify it and there is on my Github also in the final solution.

And below is the approximate structure of the project.

+-- myapp
| +-- frontend
| +-- api
| +-- content.py
| +-- templates
| +-- index.html
| +-- routes.py
| +-- static
| +-- ...
| +-- app.py
| +-- requirement.txt
| +-- uwsgi.ini
+-- data
| +-- certbot
| +-- conf
| +-- www
| +-- nginx
| +-- app.conf
+-- Dockerfile
+-- .env
+-- docker-compose.yml
+-- init-letsencrypt.sh

--

--