Deployment Instructions

Backend

Ubuntu Server using Nginx as a Reverse Proxy and Gunicorn as WSGI server

  • First, download required packages (if you have some alreadt installed, you can omit them):
    $ sudo apt update
    $ sudo apt install python3-venv python3-dev libpq-dev postgresql postgresql-contrib nginx curl git
  • Next, we will create the PostgreSQL database and user.
  • Log into an interactive Postgres session, which we will use to set up our requirements, by typing:
    $ sudo -u postgres psql
  • Create the database for the app (I will call it ilmhub_db):
    postgres=# CREATE DATABASE ilmhub_db;
  • Next, create a database user. I will call mine ilmhub_admin with password 'password'. Make sure you use a secure password.
    postgres=# CREATE USER ilmhub_admin WITH PASSWORD 'password';
  • Next, set the default connection parameters for this user to values Django uses. This will speed up database operations.
    postgres=# ALTER ROLE myprojectuser SET client_encoding TO 'utf8';
    postgres=# ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed';
    postgres=# ALTER ROLE myprojectuser SET timezone TO 'UTC';
  • Now, we give the new user database administrator privileges:
    postgres=# GRANT ALL PRIVILEGES ON DATABASE ilmhub_db TO ilmhub_admin;
  • Exit out of the PostgreSQL prompt by typing:
    postgres=# \q
  • Now, we clone the repository.
    $ git clone git@github.com:UAlberta-CMPUT401/pronunciation-practice
  • Create a virtual environment.
    $ python3 -m venv venv
  • Activate the virtual environment.
    $ source venv/bin/activate
  • Change directory to the repository.
    $ cd pronunciation-practice
  • Install the requirements.
    $ pip3 install -r requirements.txt && pip3 install gunicorn
  • Change directory to the django project's folder.
    $ cd backend/IlmHub/
  • Create a .env file in this directory.
    $ nano .env
  • Paste the following, replacing the {} with the info of the database and user created earlier:
SECRET_KEY={YOUR_SECRET_KEY}
DB_NAME={DATABASE_NAME}
DB_USER={DATABASE_USER}
DB_PASSWORD={DATABASE_USER_PASSWORD}
DB_HOST=localhost
DB_PORT=5432
DEBUG=FALSE
  • To save the file, press Ctrl+S, and to exit press Ctrl+X.
  • To generate a value for secret key, run the following command:
    $ python3 -c "from django.core.management.utils import get_random_secret_key as grsk; print(grsk())"

Here is my output (yours will probably be different):

    je8@jwb(^vc1=6j6%clyl^4x^cn5q(6ku&0cbd4+46&fp3@bt=
  • If you use the same values I've used, your .env file should look something like this:
SECRET_KEY=je8@jwb(^vc1=6j6%clyl^4x^cn5q(6ku&0cbd4+46&fp3@bt=
DB_NAME=ilmhub_db
DB_USER=ilmhub_admin
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=5432
DEBUG=FALSE
  • Now, change directory back to the backend folder.
    $ cd ..
  • We need to migrate our Django models to the PostgreSQL database. We can do so by running the following commands:
    $ python3 manage.py makemigrations
    $ python3 manage.py migrate
  • We also need to create a superuser so we can manage the database from the admin page. To do so, run the following command and enter the required information
    $ python manage.py createsuperuser
  • Run the following command to collect static files:
    $ python3 manage.py collectstatic
  • Now, we set up the systemd Socket and Service Files for Gunicorn. Start by creating and opening a systemd socket file for Gunicorn with sudo privileges:
    $ sudo nano /etc/systemd/system/gunicorn.socket
  • Paste the following into the gunicorn.socket file.
[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target
  • Now, we create and open a systemd service file for Gunicorn.
    sudo nano /etc/systemd/system/gunicorn.service
  • Paste the following into the gunicorn.service file. Make sure you change the {} below to reflect your system.
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User={LINUX_USER}
Group=www-data
WorkingDirectory={ABSOLUTE_PATH_TO_REPOSITORY}/backend
ExecStart={ABSOLUTE_PATH_TO_VIRTUAL_ENVIRONMENT}/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          IlmHub.wsgi:application

[Install]
WantedBy=multi-user.target
  • In my case, this is how my file looks like:
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/pronunciation-practice/backend/
ExecStart=/home/ubuntu/venv/bin/gunicorn \
          --access-logfile - \
          --workers 3 \
          --bind unix:/run/gunicorn.sock \
          IlmHub.wsgi:application

[Install]
WantedBy=multi-user.target
  • Now, we start and enable the Gunicorn socket.
    $ sudo systemctl start gunicorn.socket
    $ sudo systemctl enable gunicorn.socket
  • Now, we check the status of our Gunicorn socket. We first run the following command:
    $ sudo systemctl status gunicorn.socket

You should get an output similar to:

Output
● gunicorn.socket - gunicorn socket
     Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor preset: enabled)
     Active: active (listening) since Mon 2023-04-03 17:53:25 UTC; 5s ago
   Triggers: ● gunicorn.service
     Listen: /run/gunicorn.sock (Stream)
     CGroup: /system.slice/gunicorn.socket

Apr 03 17:53:25 django systemd[1]: Listening on gunicorn socket.
  • Now, check for the existence of the gunicorn.sock file within the /run directory:
   $ file /run/gunicorn.sock

Output:

/run/gunicorn.sock: socket
  • If you encountered an error, run the following command to check the logs:
    $ sudo journalctl -u gunicorn.socket
  • Let's check out the Gunicorn service.
    $ sudo systemctl status gunicorn

Output:

○ gunicorn.service - gunicorn daemon
     Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled)
     Active: inactive (dead)
TriggeredBy: ● gunicorn.socket
  • Test the socket by typing in the following command:
    $ curl --unix-socket /run/gunicorn.sock localhost
  • If you receive some HTML output, then that means Gunicorn was started and is able to serve the application.
    $ sudo systemctl status gunicorn

Output:

● gunicorn.service - gunicorn daemon
     Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled)
     Active: active (running) since Mon 2022-04-18 17:54:49 UTC; 5s ago
TriggeredBy: ● gunicorn.socket
   Main PID: 102674 (gunicorn)
      Tasks: 4 (limit: 4665)
     Memory: 94.2M
        CPU: 885ms
     CGroup: /system.slice/gunicorn.service
             ├─102674 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
             ├─102675 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
             ├─102676 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application
             └─102677 /home/sammy/myprojectdir/myprojectenv/bin/python3 /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunicorn.sock myproject.wsgi:application

Apr 18 17:54:49 django systemd[1]: Started gunicorn daemon.
Apr 18 17:54:49 django gunicorn[102674]: [2022-04-18 17:54:49 +0000] [102674] [INFO] Starting gunicorn 20.1.0
Apr 18 17:54:49 django gunicorn[102674]: [2022-04-18 17:54:49 +0000] [102674] [INFO] Listening at: unix:/run/gunicorn.sock (102674)
Apr 18 17:54:49 django gunicorn[102674]: [2022-04-18 17:54:49 +0000] [102674] [INFO] Using worker: sync
Apr 18 17:54:49 django gunicorn[102675]: [2022-04-18 17:54:49 +0000] [102675] [INFO] Booting worker with pid: 102675
Apr 18 17:54:49 django gunicorn[102676]: [2022-04-18 17:54:49 +0000] [102676] [INFO] Booting worker with pid: 102676
Apr 18 17:54:50 django gunicorn[102677]: [2022-04-18 17:54:50 +0000] [102677] [INFO] Booting worker with pid: 102677
Apr 18 17:54:50 django gunicorn[102675]:  - - [18/Apr/2022:17:54:50 +0000] "GET / HTTP/1.1" 200 10697 "-" "curl/7.81.0"
  • If the output from curl or systemctl status indicate errors, check the logs by running:
    $ sudo journalctl -u gunicorn
  • If there are any problems and you edit your /etc/systemd/system/gunicorn.service, reload the daemon and restart the process to apply changes.
    $ sudo systemctl daemon-reload
    $ sudo systemctl restart gunicorn
  • Now we configure Nginx. We first start by creating a new server block in Nginx's sites-available directory:
    $ sudo nano /etc/nginx/sites-available/IlmHub
  • Enter the following, changing the info as required:
server {
    listen 80;
    listen [::]:80;

    server_name {SERVER_DOMAIN_OR_IP};

    client_max_body_size 10G;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root {PATH_TO_REPO}/backend;
    }

    location /media/ {
        root {PATH_TO_REPO}/backend;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }

}
  • This is how it looks for me (I am using an IPv6 address, hence the [] in the server IP):
    $ server {
    listen [::]:80;

    server_name [2605:fd00:4:1001:f816:3eff:fec8:27a8];

    client_max_body_size 10G;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/ubuntu/pronunciation-practice/backend;
    }

    location /media/ {
        root /home/ubuntu/pronunciation-practice/backend;
    }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }

}
  • Save and close the file. Now, you can enable the file by linking it to the sites-enabled directory:
    $ sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled
  • Test your Nginx configuration for syntax errors:
    $ sudo nginx -t
  • If no errors are reported, restart Nginx by typing:
    $ sudo systemctl restart nginx
  • Finally, you need to open up your firewall to traffic on port 80:
    $ sudo ufw allow 'Nginx Full'

Front-end

Heroku (Paid)

  • To deploy on Heroku, first create a verified Heroku account from here.
  • Install the Heroku CLI from Heroku CLI and login to your account and verify installation.
  • Create a new app on the Heroku Dashboard from here
  • Once you are done with the above steps, register the newly created app from heroku dashboard in the existing project github repo by using the command heroku git:remote -a app-name and then set buildpacks using heroku buildpacks:set heroku/nodejs
  • Commit and push your changes using git commit -am "my commit" and git subtree push --prefix path/to/subdirectory heroku main
  • Bonus: To setup auto deploy using github actions and heroku, checkout this.

Github Pages (Free)

  • Install Github pages in frontend directory by npm install gh-pages — save-dev.
  • Add homepage property to package.json file, for github user site, "homepage": "https://{organization}.github.io". Custom domains can also be added by "homepage": "https://testwebsite.com".
  • Add deploy scripts to package.json file. "predeploy": "npm run build", "deploy": "gh-pages -d build"
  • npm run deploy
  • Check Pages on github settings to see the published url. Remember to change source branch to gh-pages.
  • To follow subdirectory troubleshooting, checkout this Stackoverflow answer

Bonus

  • An alternate choice can be Netlify which is also free for public organization repos.