PROJECTS NOTES HOME

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:

linode.png

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.