Python for Network Engineers

Flask, uwsgi, Nginx with Docker

by: George El., November 2020, Reading time: 8 minutes

In this post I will describe briefly what is docker and how to run a simple python program in a docker container. Then I will do the same for a flask app using uswgi and nginx in different containers and then I will combine the two using docker-compose.

What is a Container?

A Container is a running environment for an image. It provides isolation from other containers and processes. The image is a package of software that includes everything the application needs to run.

What is Docker?

Docker is a container platform that is used for configuring, building, running and distributing containers. It is by far the most popular platform and the de facto standard of containers. It is used by devops to package, run and distribute applications.

What is the difference with VMs?

VMs include the whole operating system as well as the application. So a system with 3 VMs would run a hypervisor and 3 separate OS on top of it. In contrast, a server running three containers, will run a single OS and the docker engine. So it is more lightweight and it takes less time to start and stop containers.

What is Kubernetes?

Kubernetes is an orchestration platform for containers, that is, it provides tools for automated arrangement, coordination and management of software containers. With kubernetes you can deploy thousands of workers in an array of machines called workers, providing load balancing, scalability, efficient resource management and disaster recovery.

What problem do containers solve?

If you develop an app on your local machine and then you move it to a server, it is very likely that things won’t run as expected. That is because the server can run different versions of software. The solution to this is to do your development using containers and then move the whole container to production.

Major benefits

  • Containers are lightweight and a single machine can host hundreds of containers compared to a few VMs.
  • Containers take less time to start and can be instantiated as needed
  • Containers facilitate the development of micro-services, that is an application is split into many smaller apps, each running on a separate container

DockerImage

In order to create a container you need first to download or create a new image. There are many images publicly available in hub.docker.com. We will download a python3 image writing:

docker pull python:3

This will start downloading the image if the image is not available locally.

docker pull python:3
3: Pulling from library/python
Digest: sha256:429b2fd1f6657e4176d81815dc9e66477d74f8cbf986883c024c9b97f7d4d5a6
Status: Image is up to date for python:3
docker.io/library/python:3

In my case I have already downloaded. Now we can write docker images to see the images that we have locally

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
python              3                   5336a27a9b1f        2 weeks ago         886MB

To run this image we write docker run python:3

~/myapp$ docker run python:3
~/myapp$ 

As you see nothing happens because this container has only python3 and has no instructions to run something. Lets create a simple file hello-world.py

print("hello-world")

I will create a new image that will have this program and run it. To create a new image I need to create a file called Dockerfile. The format is as following:

FROM python:3

WORKDIR /usr/src/app

COPY . .

CMD [ "python", "./hello-world.py" ]

to build the image into a container and give it a name you run

docker build -t hello-python-app .

where . is the directory where you have the dockerfile. in this case it is the current directory. if you run now docker images it will show the image

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello-python-app    latest              1c55973d15ae        20 hours ago        896MB

to run the image in an interactive terminal and give it a container name you type

docker run my-python-app

this will print hello world and stop the container

hello-world

usually what you start will be a daemon, like web-server, database, etc. In this case you will start it with the -d option as we shall see below.

to get a bash shell in the container you type

$docker run -it  hello-python /bin/bash

and you get bash shell inside the container. or if the container is already running you can write

$docker exec -it  hello-python /bin/bash

so if we write docker run -it hello-python /bin/bash we get into the bash shell of the container, provided it has one, because some very lightweight containers have only sh.

root@72ab217a15be:/usr/src/app# ls
dockerfile  hello-world.py  requirements.txt
root@72ab217a15be:/usr/src/app# cd /
root@72ab217a15be:/# ls
bin   dev  home  lib64	mnt  proc  run	 srv  tmp  var
boot  etc  lib	 media	opt  root  sbin  sys  usr
root@72ab217a15be:/# cd /usr/src/app
root@72ab217a15be:/usr/src/app# ls
dockerfile  hello-world.py  
root@72ab217a15be:/usr/src/app# python
Python 3.9.0 (default, Oct 13 2020, 20:14:06) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
root@72ab217a15be:/usr/src/app# python hello-world.py 
hello-world
root@72ab217a15be:/usr/src/app# 

you see that it includes a basic linux os and has installed python 3.9 and our app. Now lets move to a more complicated example, how to run a flask app.

Creating a flask app using flask_restful

I have the following flask app that for brevity purposes I have ommited the put, update and delete methods.

from flask import Flask, abort
from flask_restful import Api, Resource

app = Flask(__name__)
api = Api(app, prefix="/api/v1")

users = [
    {"email": "john@gmail.com", "name": "John", "id": 1},
    {"email": "george@gmail.com", "name": "george", "id": 2},
    {"email": "nick@gmail.com", "name": "nick", "id": 3},
]

def get_user_by_id(user_id):
    for x in users:
        if x.get("id") == int(user_id):
            return x

class Users(Resource):
    def get(self):
        return { 'users': users}

class User(Resource):
    def get(self, id):
        user = get_user_by_id(id)
        if not user:
            return {"error": "User not found"}
        return user


api.add_resource(User,'/user/<int:id>')
api.add_resource(Users,'/users')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0')

to run int you have to install flask and flask_restful via pip and create the requirements.txt by writing

pip install flask flask-restful
pip freeze > requirements.txt

the requirements file is

aniso8601==8.0.0
click==7.1.2
Flask==1.1.2
Flask-RESTful==0.3.8
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
pytz==2020.4
six==1.15.0
Werkzeug==1.0.1

creating a dockerfile for the flask app

FROM python:3

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY flask_restful_app.py .

CMD [ "python", "./flask_restful_app.py" ]

docker build -t flask_restful_app .
Sending build context to Docker daemon  13.05MB
Step 1/6 : FROM python:3
 ---> 5336a27a9b1f
Step 2/6 : WORKDIR /usr/src/app
 ---> Using cache
 ---> b8ae69e69308
Step 3/6 : COPY requirements.txt ./
 ---> 571672bf4ca0
Step 4/6 : RUN pip install --no-cache-dir -r requirements.txt
 ---> Running in be721845aa8f
Collecting aniso8601==8.0.0
  Downloading aniso8601-8.0.0-py2.py3-none-any.whl (43 kB)
Collecting click==7.1.2
  Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
Collecting Flask==1.1.2
  Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
Collecting Flask-RESTful==0.3.8
  Downloading Flask_RESTful-0.3.8-py2.py3-none-any.whl (25 kB)
Collecting itsdangerous==1.1.0
  Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Jinja2==2.11.2
  Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB)
Collecting MarkupSafe==1.1.1
  Downloading MarkupSafe-1.1.1.tar.gz (19 kB)
Collecting pytz==2020.4
  Downloading pytz-2020.4-py2.py3-none-any.whl (509 kB)
Collecting six==1.15.0
  Downloading six-1.15.0-py2.py3-none-any.whl (10 kB)
Collecting Werkzeug==1.0.1
  Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
Building wheels for collected packages: MarkupSafe
  Building wheel for MarkupSafe (setup.py): started
  Building wheel for MarkupSafe (setup.py): finished with status 'done'
  Created wheel for MarkupSafe: filename=MarkupSafe-1.1.1-cp39-cp39-linux_x86_64.whl size=32204 sha256=07aa7eed12fad6acc1b29d93db1344aca3efcaf4c2cab1168336a245d9b13aeb
  Stored in directory: /tmp/pip-ephem-wheel-cache-bx6d7ad8/wheels/e0/19/6f/6ba857621f50dc08e084312746ed3ebc14211ba30037d5e44e
Successfully built MarkupSafe
Installing collected packages: aniso8601, click, itsdangerous, Werkzeug, MarkupSafe, Jinja2, Flask, pytz, six, Flask-RESTful
Successfully installed Flask-1.1.2 Flask-RESTful-0.3.8 Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 aniso8601-8.0.0 click-7.1.2 itsdangerous-1.1.0 pytz-2020.4 six-1.15.0
Removing intermediate container be721845aa8f
 ---> 7b19620e2d13
Step 5/6 : COPY flask_restful_app.py .
 ---> 402130fa52df
Step 6/6 : CMD [ "python", "./flask_restful_app.py" ]
 ---> Running in c4aec2144524
Removing intermediate container c4aec2144524
 ---> fd25724d51ba
Successfully built fd25724d51ba
Successfully tagged flask_restful_app:latest

To run int you type

$ docker run -p 5000:5000 flask_restful_app
 * Serving Flask app "flask_restful_app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 185-826-284
172.17.0.1 - - [04/Nov/2020 08:44:24] "GET /api/v1/users HTTP/1.1" 200 -

if i call http://localhost:5000/api/v1/users I get

{
    "users": [
        {
            "email": "john@gmail.com",
            "name": "John",
            "id": 1
        },
        {
            "email": "george@gmail.com",
            "name": "george",
            "id": 2
        },
        {
            "email": "nick@gmail.com",
            "name": "nick",
            "id": 3
        }
    ]
}

or if i call http://localhost:5000/api/v1/user/1

{
    "email": "john@gmail.com",
    "name": "John",
    "id": 1
}

In reality you would use a wsgi server to run the flask app like uwsgi or gunicorn and you would also install nginx for all other requests. so lets first install uwsgi

uwsgi

Lets see now how to run our flask app using uwsgi which is a professional application server

pip install uwsgi

we need to update also our requirements.txt

pip freeze > requirements.txt

Next we need to create an wsgi.ini, which will hold the configuration for wsgi

[uwsgi]
wsgi-file = flask_restful_app.py
callable = app
socket = 0.0.0.0:5000
processes = 1
threads = 1
master = true
chmod-socket = 664
vacuum = true
die-on-term = true

and modify our dockerfile accordingly

FROM python:3

WORKDIR /usr/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY flask_restful_app.py .
COPY wsgi.ini .

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

Install nginx

Lets now install and configure nginx. The dockerfile will be

FROM nginx

# Remove the default nginx.conf
RUN rm /etc/nginx/conf.d/default.conf

# Replace with our own nginx.conf
COPY nginx.conf /etc/nginx/conf.d/

and the nginx.conf file is

server {

    listen 80;
    server_name localhost;

    location /api/v1 {
        include uwsgi_params;
        uwsgi_pass flask:5000;
    }

}

this config says that whenever you call /api/v1 forward the request to flask:5000 using uswgi. Flask is the name of the service that we will create in our docker-compose file

Docker-compose

We will create now a docker compose file to initialize and run the two containers

version: "3.7"

services:

  flask:
    build: ./flask_restful_app
    container_name: flask_restful_app
    restart: always
    expose:
      - 5000

  nginx:
    build: ./nginx
    container_name: nginx-flask
    restart: always
    ports:
      - "80:80"

we have two folders one is the flask_restful_app and one is the nginx, each with their own dockerfile. That’s why we say build ./flask_resful_app and build ./nginx. We expose the port 5000 on the flask app and we map port 80 of nginx to port 80 on the outside. So we will call the app like this

http://127.0.0.1/api/v1/user/1

the result is

{"email": "john@gmail.com", "name": "John", "id": 1}

So now we have a full functioning app with docker, flask, uwsgi and nginx. Of course nginx and uwsgi confs need more tuning for production environments.

comments powered by Disqus