Deploying Flask Application on Ubuntu VPS using Nginx
In my free time between my work, my python course, dev job search, etc, I was building a flask application for quotes that I have gathered during the years. I wanted this project to be finished, to be live. For that I need to host it.
Will share with you how I did it.
1 Apache vs nginx
Will just leave it here.
Also, one thing I would like you to know is that Apache is a threaded server, which means it is prone to DDOS and DOS attacks. But Nginx on the other hand is an asynchronous web server and reverse proxy, so it is not as vulnerable as Apache, and it is also very easy to set up.
2 Get yourself a virtual private server(VPS)
This time I was using Linode, since I got some free credits with it, but every provider is basically the same.
Create new linode:
Wait for it to be provisioned(2-3min).
Connect to it over ssh.
ssh root@139.xx.xx.xx
replace xx with your server ip address. Enter the credentials that you have set up for it. And you should now be inside your server!
sudo apt update
3 Install nginx
sudo apt install nginx
Create a configuration for nginx web server
cd /etc/nginx/sites-enabled/flask_app
This should be the content of this file:
server { listen 80; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
If you got to your server's ip now - you will see nginx default config, we need to unlink it so it uses our config.
sudo unlink /etc/nginx/sites-enabled/default # if previous step succeeds, do this: sudo nginx -t sudo nginx -s reload
Refresh the webpage(will get "bad gateway", because nginx is trying to connect to localhost port 8000, because we don't have a webserver running there. That is why we will user Gunicorn.
4 Let's run project
sudo apt install python3-pip # go to the source folder of your project pip3 install -r requirements.txt
At this point you can do python3 run.py
and go to the port 5000 or w/e
you set up.
If you still can not see your webpage - make sure that host="0.0.0.0",
is written next to debug=True
.
0.0.0.0 adapts to any ip address where this app is being hosted.
Now it works! Your application is live! Magic!
5 Gunicorn
"This is currently a development server and not good for production since if multiple users try to connect to the server, they will not be able to do it." (actually works, I just checked on my phone.)
sudo apt install gunicorn3
Set workers and tell which files contain flask app
# (run - name of the file, app - app object?) gunicorn3 --workers=3 run:app
Now go to the server's ip, port 8000, as the gunicorn is listening - NOTHING
Go to IP itself only - TADAA, serving through nginx(setting reverse proxy to localhost 8000)!
It's better to run your server as a daemon, so it runs in the background and you can user your terminal for other things:
# to run the daemon process(must run this from the run.py dir) gunicorn3 --workers=3 run:app --daemon # to kill the daemon process sudo pkill -f gunicorn3
6 Dealing with secret files
6.1 config.json method
We will try to hide the:
- db name
- secret key
- static folder name (silly, yes, but anyway.. for practice)
let's create a config.json file next to our run.py
{ "SECRET_KEY": "1x1x1x11x1x1x2", "SQLALCHEMY_DATABASE_URI": "dbname.db", "UPLOAD_FOLDER": "static/images" }
update your .gitignore to ignore this file. We don't want to push it to public repository. It would defeat the whole purpose of keeping these secrets secret.
# if you don't have it already pip install urlib3
In your __init__.py
import these:
import json import urllib3
Then let's make our config.json file accessible:
with open("./config.json") as config_file: config = json.load(config_file)
Now we can replace all the information that was previously in plain text, for example:
# replace this app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join( basedir, "db.db" ) # with this app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join( basedir, config.get("SQLALCHEMY_DATABASE_URI") )
# this app.config["UPLOAD_FOLDER"] = "static/images" # with this app.config["UPLOAD_FOLDER"] = config.get("UPLOAD_FOLDER")
# this SECRET_KEY = os.urandom(32) app.config["SECRET_KEY"] = SECRET_KEY # with this app.config["SECRET_KEY"] = config.get("SECRET_KEY")
Push this change to your github repo, and pull it on your server.
When trying to launch the server - it says there is no config.json file. That's correct, because it is not in our github repo.
Let's add config.json to our server using SCP(what is SCP):
# copy files FROM the server scp remote_username@remote_host:/path/to/remote/file /path/to/local/file
# copy files TO the server scp /path/to/local/file remote_username@remote_host:/path/to/remote/file # copy FOLDER's and their content TO the server scp -rp static remote_username@139.xxx.xx.xxx:/root/citatos_flask/web_site
We use the latter option to copy files TO the server, same folder where run.py sits on our server.
In my case it was:
scp ./config.json bla@139.xxx.xx.xxx:/bla/citatos_flask
The config.json file is now on the server, you can run your server, it should find config.json file.