Introduction
Hi, its been a while since i wrote something on my blog. Today, i am going to write about the process of deployment of a django application on a Ubuntu 22.02 live server. I’ll try to cover as much details as i can and mention about the common issues i normally faces while following the process. This article is also a kind of reminder for myself for the tutorials and blog that i followed and the issues i ran into. So, we are going to cover this deployment process through Gunicorn, NGINX and optionally using Postgresql database.
Tutorial
So, without wasting any further time. Lets jump right into the details.
Pre-Reqs
The foremost thing are the pre-reqs that we need to follow this tutorial. First is a server with a public IP. Many of you who are reading this might already have one before moving one. Well, if you don’t you can simply get one from any of the hosting services or Cloud platforms. Personally, i use DigitalOcean or AWS most of the time. And also, for the sake of this tutorial i’ll be using a t2.micro
instance of EC2 service from AWS.
But in general what you need is an online server with Ubuntu 22.04 installed and with publicly accessibly ports. Thi should also work just fine with Ubuntu 20.04. Let me know in comments if someone tests it.
Installing Packages
First, lets update our ubuntu machine and install the required packages:
bash
sudo apt updatesudo apt install -y nginx python3 python3-dev python3-pip python3-virtualenv libpq-dev postgresql postgresql-contrib
Before going forward, lets escalate to the root
shell as we might run into the permission issues again and again. And don’t worry, we will see the project and permissions of the other files at the last:
bash
sudo -s
Setting up Project Directory
Now, lets move to the /var/www/
directory where we are going to place our project folder. I am going to setup a project from scratch for demo purposes using django-admin
command. But if you already have one, make sure its uploaded correctly. I normally used rsync
for this:
bash
rsync -arvz -e 'ssh -p <port-number>' --progress --delete /path/to/local/folder user@remote-server:/var/www/
For now, i will setup a new project from scratch and name it myweb
. Lets install django and initialize an empty project:
bash
cd /var/wwwpip3 install djangodjango-admin startproject myweb
Move to myweb
and initialize new virtual environment:
bash
cd myweb/virtualenv venv
Moving forward, activate the virtual environment and install all the necessary packages that you might have placed in requirements.txt
.
bash
source ./venv/bin/activatepip3 install -r requirements.txt # In case you have onepip3 install django gunicorn psycopg2-binary
Setting up Postgres (Database)
You could move forward without this step if you are planning on using some other database or the default sqlite. But please note that you will have to install the relevant packages for that. While in this case, i am only focusing on postgres
.
Launch postgres
:
sudo -u postgres psql
The next steps are supposed to be inside the postgres
terminal. Create a new database. I am naming it hashbase
. Setup a new user and assign it to the database that we just created:
CREATE DATABASE hashbase;CREATE USER hashbaseuser WITH PASSWORD 'password1122';GRANT ALL PRIVILEGES ON DATABASE hashbase TO hashbaseuser;
Next, we will modify a couple of our database settings for better processing and django functions:
bash
ALTER ROLE hashbaseuser SET client_encoding TO 'utf8';ALTER ROLE hashbaseuser SET default_transaction_isolation TO 'read committed';ALTER ROLE hashbaseuser SET timezone TO 'UTC';
Quit:
bash
q
Project Settings
We will look at the project settings now that needs to be replaced or modified. Considering we are in the project directory located at /var/www/myweb
. Lets edit the settings.py
file:
bash
nano myweb/settings.py
Edit the ALLOWED_HOSTS
list variable to include all the IPs or Hostnames that you plan on using for the website. In here, i am simply going to put the PUBLIC IP of the server i am on. You can put domain names if you are going to configure one with the website.
python
...ALLOWED_HOSTS = ['13.229.230.156', # Public IP of the Server'mydomain.com', # In case you have a domain, you can skip the IP Address'someotherhostetc',]...
Now, move a bit below to this and find the DATABASES
variable section. By default it is configured to use sqlite3
as its database. We will change the settings to use postgresql
:
python
...DATABASES = {'default': {'ENGINE': 'django.db.backends.postgresql_psycopg2','NAME': 'hashbase','USER': 'hashbaseuser','PASSWORD': 'password1122','HOST': 'localhost','PORT': '',}}...
After this, we need to setup a static
and media
directory where our project static files are going to be stored. The media
folder will contain files that are supposed to be uploaded by the user of the application. So, again in the settings.py
file, move at the end of the file and find the line STATIC_URL = 'static/'
and add some new content below it:
python
...STATIC_URL = 'static/'STATIC_ROOT = BASE_DIR / 'public'MEDIA_URL = 'media/'MEDIA_ROOT = BASE_DIR / 'media'...
The STATIC_ROOT
defines the folder where the public content like css,js or imgs are going to be stored upon running the collectstatic
command on manage.py
file. Please note that its totally different from the STATICFILES_DIRS
variable which is used to define the directories to locate static files. Think of STATIC_ROOT
as more of a production specific variable.
Spin up the Project
Lets move towards spinning up the project for the public. For that, we will do the normal django commands like migrations and super user.
Run migrations and create a superuser:
python3 manage.py makemigrationspython3 manage.py migratepython3 manage.py createsuperuser # Enter your credentials for the superuser when prompted
After this, we will run the collectstatic
command to locate all the static files we have in our project and put them to the folder that we defined at STATIC_ROOT
variable. These static files will be served by the nginx
server and not from django like we see during the development.
python
python3 manage.py collectstatic
You can now execute the runserver
command on public interface to see your website. Before also make sure nginx is not using the port 80
by default:
python
service nginx stoppython3 manage.py runserver 0.0.0.0:80
Now, if you access your website at port 80
you would be able to see your project:
python
http://PUBLCI_IP_ADDRESS
Finally, back in the settings.py
put the DEBUG
value to False
. You can keep it enabled if you are going to debug the project or facing some other issue. But make sure to put the value to False
for the production cases:
nano myweb/settings.py...DEBUG = False...
At this point, we have our project all ok. But this is not how we are going to host the website actually.
Setting up Gunicorn
Gunicorn is the server service that we are going to configure with nginx
for the public. We will configure a new systemd
service for this. But before, we will test if gunicorn
is really working or not. So, simply bind gunicorn to the 0.0.0.0
and test if we can access the website at port 80
like we did before. Note that we are still inside /var/www/myweb
directory
bash
gunicorn --bind 0.0.0.0:80 myweb.wsgi
Fire up nano
again for the gunicorn service and do the configuration:
bash
nano /etc/systemd/system/gunicorn.service
Inside the file. I’ll not get into much details of what is happening here. But you can refer to the systemd
documentation for more in depth detail. However, we will review the Service
section here:
python
[Unit]Description=gunicorn daemonAfter=network.target[Service]User=www-dataGroup=www-dataWorkingDirectory=/var/www/mywebExecStart=/var/www/myweb/venv/bin/gunicorn --access-logfile - --workers 3 --timeout 300 --bind unix:/var/www/myweb/myweb.sock myweb.wsgi:application[Install]WantedBy=multi-user.target
The User
and Group
defines the privileges the command is going to be executed with. The complete command is specified with ExecStart
variable. Make sure to specify WorkingDirectory
as well. Otherwise, gunicorn wouldn’t be able to locate the myweb.wsgi
on path. Save the file.
Make sure to change the project’s owner before starting gunicorn:
bash
chown www-data:www-data -R /var/www/myweb
Start the service after saving and make that its running and doesn’t have any errors. If there are errors, you will need to fix them before moving forward:
bash
systemctl daemon-reloadsystemctl restart gunicorn.servicesystemctl status gunicorn.service
You can see more detailed logs of gunicorn if there are errors using:
bash
sudo journalctl -u gunicorn
Setting UP NGINX
This is the last step. We will configure nginx
which in short is the public server and will take requests from the public and forward it to gunicorn as a reverse proxy. Remove the default enabled nginx website and create a new nginx
project file:
bash
rm /etc/nginx/sites-enabled/defaultnano /etc/nginx/sites-available/myweb
Inside the file, configure your web server:
nginx
server {listen 80;server_name DOMAIN_OR_PUBLIC_IP;location = /favicon.ico { access_log off; log_not_found off; }location /static/ {alias /var/www/myweb/public/;add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;}location /media/ {alias /var/www/myweb/media/;}location / {include proxy_params;add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;proxy_pass http://unix:/var/www/myweb/myweb.sock;}}
In the above configuration, make sure you have changed it according to your requirements. One thing to look out for is the server_name
directive. If you are working with a domain name, make sure to replace it correctly. Another thing would be the paths that are mentioned in the configuration. Make sure all of the mentioned paths in your correct according to your settings.
Save the file and enable this nginx project by creating its soft link at sites-enabled
directory:
python
ln -s /etc/nginx/sites-available/myweb /etc/nginx/sites-enabled/myweb
Check nginx configuration and restart nginx:
nginx -tsystemctl restart nginxsystemctl status nginx
If everything goes correctly, you should be able to see the nginx service running and if you visit the PUBLIC IP or Domain Name now, you will see your website. For the final test, visit the /admin
page to make sure the static files are also served correctly from our nginx service:
python
http://PUBLIC_IP_ADDRESS_OR_DOMAIN/admin
In case your static are not served properly, you can check the nginx
error logs at: /var/log/nginx/error.log
and debug the issues.
Conclusion
Thanks for making it through. All in all, we saw how to host a django website using gunicorn, nginx and postgresql. Its not very tricky but yes sometimes debugging the configuration files can take time. So, while doing your own, make sure to double check your configuration files and make sure that they are making sense for what they are supposed to do.