Flask, uwsgi, Nginx with Docker
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.