Python Flask app on Docker in ISPConfig3 with Nginx - Part 1: Minimal app
ISPConfig is a great hosting control panel but it does not support Python applications out of the box. This post shows how you can do it using Docker.
This is a post showing how to run a Flask app on ISPConfig3. Why? I have a VPS on the internet running Debian and ISPConfig3. It is running static sites and PHP sites. But now I also want to run my Flask python apps here. This way I can use the domain management I am used to and do not need an extra server for Python apps.
This solution uses Docker to run the Flask app, printing 'Hello world', and is a first proof of concept showing it is possible to deploy a Flasp app on ISPConfig3.
My local machine:
- Ubuntu desktop 18.04.1
- Python 3.6.7
- nginx 1.14.0
- Docker 18.09.0
- Docker-compose 1.23.2
My VPS:
- Debian 9 (Stretch)
- ISPConfig3 3.1.13
- Nginx 1.10.3
- docker 18.09.0
- Docker-compose 1.23.2
Try to keep the Docker and Docker-compose versions on both machines identical. Docker developers sometimes fail to keep backward compatibility.
Contents:
- Step 1: Create and run a Flask app helloworld on the local machine, using the development server
- Step 2: Run helloworld on the local machine, using the production server Gunicorn
- Step 3: Run helloworld on the local machine, using Docker
- Step 4: On the local machine serve the dockarized helloworld with a reverse proxy nginx server
- Step 5: On the ISPConfig3 machine, deploy the dockarized helloworld
- Summary
- Notes
Step 1: Create and run a Flask app helloworld on the local machine, using the development server
1.1. Create virtual environment and activate
See instructions on the internet. Do something like:
> mkdir flask_docker
> cd flask_docker
> mkdir python3
> cd python3
> mkdir venvs
> python3 -m venv venvs/helloworld
> source venvs/helloworld/bin/activate
> cd venvs/helloworld
> pwd
.../flask_docker/python3/venvs/helloworld
1.2. Check the directory helloworld
You should see something like:
.
|-- bin
|-- include
|-- lib
|-- lib64 -> lib
|-- __pycache__
|-- share
`-- pyvenv.cfg
All commands below are from this directory.
1.3. Install Flask
Type:
> pip install flask
1.4. Create simple Flask application hello.py
With you editor create a Flask app helloworld.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "<h1>Hello world!</h1>"
if __name__ == '__main__':
app.run()
1.5. Run it using the development server
> python3 helloworld.py
In your browser type: localhost:5000
You should see: Hello World
Stop the development server using Ctrl-C.
Step 2: Run helloworld on the local machine, using the production server Gunicorn
2.1. Install production server gunicorn
pip install gunicorn
2.2. Create application object wsgi.py file for gunicorn:
Create the wsgi.py as follows:
from helloworld import app
if __name__ == "__main__":
app.run()
We are simply importing the app from helloworld.py.
2.3. Run it using the gunicorn server
> gunicorn --bind 0.0.0.0:8000 wsgi:app
In your browser type: localhost:80000
You should see: Hello World
Stop the gunicorn server using Ctrl-C.
Step 3: Run helloworld on the local machine, using Docker
3.1. Install Docker and Docker-compose
See instructions on docker.com, digitalocean.com, etc.
If you cannot issue Docker commands without sudo, then add yourself to the Docker group:
Post-installation steps for Linux
https://docs.docker.com/install/linux/linux-postinstall/
3.2. Store the required software in requirements.txt
> pip freeze > requirements.txt
If there is a line 'pkg-resources==0.0.0' present in requirements.txt, remove it.
This is a bug on some systems.
My requirements.txt file look like this:
Click==7.0
Flask==1.0.2
gunicorn==19.9.0
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
Werkzeug==0.14.1
3.3. Create a Dockerfile
The Dockerfile looks like this:
FROM python:3.6-alpine
RUN adduser -D helloworlduser
RUN mkdir -p /home/flask_app/helloworld
WORKDIR /home/flask_app/helloworld
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY helloworld.py wsgi.py boot.sh ./
RUN chmod +x boot.sh
ENV FLASK_APP helloworld.py
RUN chown -R helloworlduser:helloworlduser ./
USER helloworlduser
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
The start up script boot.sh:
#!/bin/sh
# called by Dockerfile
# go to directory where wsgi.py is
cd /home/flask_app/helloworld
# start gunicorn
exec gunicorn -b :5000 --access-logfile - --error-logfile - wsgi:app
The directory now looks like this:
.
|-- bin
|-- include
|-- lib
|-- lib64 -> lib
|-- __pycache__
|-- share
|-- boot.sh
|-- docker-compose.yml
|-- Dockerfile
|-- Dockerfile_web
|-- helloworld.py
|-- image_helloworld.tar
|-- pyvenv.cfg
|-- requirements.txt
`-- wsgi.py
3.4. Build the container image
> docker build -t helloworld:latest .
The output ends with something like:
...
Successfully built d3e8bc220161
Successfully tagged helloworld:latest
You can check if the container has been created:
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest d3e8bc220161 2 minutes ago 84.8MB
python 3.6-alpine 1837080c5e87 5 weeks ago 74.4MB
3.5. Run the container image
First run the container as a foreground application, i.e. without the -d option:
> docker run --name helloworld -p 8001:5000 --rm helloworld:latest
This will show debug information. If something is wrong e.g. in boot.sh you can correct it and rebuild the container again:
> docker build -t helloworld:latest .
You can also enter the container to see what is happening, e.g. if the files are really there:
> docker run -it --entrypoint /bin/sh --rm helloworld:latest
If everything is fine you can run the container in the backgrounf using the -d option:
> docker run --name helloworld -d -p 8001:5000 --rm helloworld:latest
In your browser type: localhost:80001
You should see: Hello World
Check that the container is running:
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
37f06d44cd30 helloworld:latest "./boot.sh" 4 seconds ago Up 4 seconds 0.0.0.0:8001->5000/tcp helloworld
Check the logs:
> docker logs helloworld
[2019-02-02 14:52:57 +0000] [1] [INFO] Starting gunicorn 19.9.0
[2019-02-02 14:52:57 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2019-02-02 14:52:57 +0000] [1] [INFO] Using worker: sync
[2019-02-02 14:52:57 +0000] [9] [INFO] Booting worker with pid: 9
To stop the container:
> docker kill helloworld
Check that the container stopped running:
> docker ps
Step 4: On the local machine serve the dockarized helloworld with a reverse proxy Nginx server
4.1. Install Nginx on the local machine
If it is not yet installed:
> sudo apt install nginx
4.2. Configure Nginx
We are going to serve the helloworld.net website at port 8080. Configure Nginx as a reverse proxy, i.e. it directs client requests to our Gunicorn server running on Docker.
Create a file /etc/nginx/sites-available/helloworld.conf:
server {
listen 8080;
server_name helloworld.net;
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 1M;
}
}
Create a symbolic link in /etc/nginx/sites-enabled:
> sudo ln -s /etc/nginx/sites-available/helloworld.conf /etc/nginx/sites-enabled/helloworld.conf
Restart Nginx:
> sudo systemctl restart nginx
If anything goes wrong, look at the status:
> sudo systemctl status nginx.service
4.3. Add helloworld.net to the hosts file
> sudo nano /etc/hosts
Add a line:
127.0.0.1 helloworld.net
Check it is working, it should respond 127.0.0.1 on ping:
> ping helloworld.net
PING helloworld.net (127.0.0.1) 56(84) bytes of data.
64 bytes from helloworld.net (127.0.0.1): icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from helloworld.net (127.0.0.1): icmp_seq=2 ttl=64 time=0.042 ms
In chromium (chrome) you can check and clear the dns by typing in the address bar:
chrome://net-internals/#dns
4.4. Start the container and access your website
In your browser type: helloworld.net:8080
If you did not start the helloworld docker container image yet, the browser will display:
502 Bad Gateway
Start your container:
> docker run --name helloworld -d -p 8001:5000 --rm helloworld:latest
In your browser you should see: Hello World
Step 5: On the ISPConfig3 VPS, deploy the dockarized helloworld
We now have a Docker container image that is working on our local machine. To run this on our ISPConfig3 machine we have several options. Here we use: copy the Docker image to another host.
5.1. Install Docker and Docker-compose on the VPS
Follow installation procedures.
5.2. Copy Dockerfile and docker image to ISPConfig3 machine
Locate our image:
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest bee00f1c8607 21 hours ago 84.8MB
Save the docker image as a tar file:
> docker save -o ./image_helloworld.tar helloworld
Use a file transfer program to copy Dockerfile and the image to the ISPConfig3 machine.
On the ISPConfig3 machine go to the directory where the files are and add our image to Docker:
> docker load -i image_helloworld.tar
This may take some time. Check the image is available:
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
helloworld latest bee00f1c8607 21 hours ago 84.8MB
5.3. Configure your site in ISPConfig3
Assumption is you have a domain name already pointing to your ISPConfig3 machine, you added the domain, and added the site. In this case when typing in your browser:
<domain>
you shoud see something like:
Welcome to your website!
Login to ISPConfig3.
Go to: Sites->Your site
In the Domain tab:
- CGI uncheck
- PHP: disabled
Click Save.
In the Options tab
Add the following to nginx Directives:
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 1M;
}
Click Save.
If your site was working before it should now show:
ERROR 502 - Bad Gateway!
5.4. Start the container
> docker run --name helloworld -p 8001:5000 --rm helloworld:latest
Now after typing in your browser:
<domain>
you shoud see: Hello World
Great!
Stop the Docker Flask app:
> docker kill helloworld
Check it is gone:
> docker ps
Summary
We created a simple Flask app and deployed it on our ISPConfig3 machine.
In a next post we will use a more advanced example:
- Serve multiple pages
- Serve static files
- Connect to the ISPConfig3 database
Notes
1. You may want to use Gunicorn always for development
Enabling the Flask Interactive Debugger in Development with Gunicorn
https://nickjanetakis.com/blog/enabling-the-flask-interactive-debugger-in-development-with-gunicorn
Links / credits
Deploy flask app with nginx using gunicorn and supervisor
https://medium.com/ymedialabs-innovation/deploy-flask-app-with-nginx-using-gunicorn-and-supervisor-d7a93aa07c18
Embedding Python In Apache2 With mod_python (Debian/Ubuntu, Fedora/CentOS, Mandriva, OpenSUSE)
https://www.howtoforge.com/embedding-python-in-apache2-with-mod_python-debian-ubuntu-fedora-centos-mandriva-opensuse
How to Configure NGINX for a Flask Web Application
https://www.patricksoftwareblog.com/how-to-configure-nginx-for-a-flask-web-application/" target="_blank">How to Configure NGINX for a Flask
How to copy Docker images from one host to another without using a repository
https://stackoverflow.com/questions/23935141/how-to-copy-docker-images-from-one-host-to-another-without-using-a-repository
How to deploy a django application on a linux (Debian/Ubuntu) production server running ISPConfig 3
http://blog.yawd.eu/2011/how-to-deploy-django-app-linux-server-ispconfig/
How To Serve Flask Applications with Gunicorn and Nginx on Ubuntu 18.04
https://www.digitalocean.com/community/tutorials/how-to-serve-flask-applications-with-gunicorn-and-nginx-on-ubuntu-18-04
The Flask Mega-Tutorial Part XIX: Deployment on Docker Containers
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xix-deployment-on-docker-containers
Recent
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
- Should I migrate my Docker Swarm to Kubernetes?
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Connect to a service on a Docker host from a Docker container
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Using PyInstaller and Cython to create a Python executable
- Flask RESTful API request parameter validation with Marshmallow schemas