Deploying Django Project to DigitalOcean
Last updated on July 27, 2020
This chapter provides step by step guide to deploy Django projects to the DigitalOcean server.
DigitalOcean is one of the leaders VPS provider and their plans are very inexpensive - starting at just $5 per month.
If you don't have a DigitalOcean account, use this link to sign up and you will get $10 free credit.
The git repository of the Django project used in this chapter is available here. But you can also follow along with your own project if you want to.
Creating Droplet #
Login to your DigitalOcean account and you will be taken to the dashboard page.
Click the "Create" button at the top of the page and select Droplets.
In the Create Droplets page, under the Choose an image section select Ubuntu 16.04.
Select size of droplet:
Choose the region where the server will be located:
Additional options and SSH keys are optional.
Give a name to your droplet and hit the Create button.
Once the droplet is created you will receive an email containing the credentials required to login to the server.
Connecting to the Server #
To connect to the server start the terminal and type the following command:
$ ssh root@167.99.235.81
Note: Windows user can use PuTTY SSH client to connect to the server.
If you are logging in for the first time, you will be prompted for a password change.
Creating a User with limited access #
You should never run applications as root user because if an attacker broke into your application then he instantly gains access to the whole system as a root user.
Further, a root user is very powerful full and thus can perform any action, even if it leads to a broken system. Do you want to format disks? or delete the /usr
directory, just execute the command and it's done. When you are root the system assumes you know what are doing.
For this reason, most applications in Linux is run as system users with restricted access.
To add an additional security layer some distributions ship with root access disabled. To perform the administrative action you will have to elevate privileges using the sudo
command.
To create a new user enter the following command:
$ adduser django
You will be asked for a password and some optional details.
Next, add the user to the sudo
group by executing the following command:
gpasswd -a django sudo
Now, this user has the ability to execute administrative commands.
To login with your newly created user type su
followed by the username:
su django
Change your current working directory to the user's home directory using the cd
command:
$ cd
In the next step, we will update our system and install some necessary packages.
Installing PIP, PostgreSQL and Nginx #
To begin with, update the system using the following command:
1 2 | $ sudo apt-get update
$ sudo apt-get upgrade
|
Ubuntu 16.04 comes preinstalled with Python 3.5 so we don't need to install Python. However, you do need to install pip.
PIP #
To install PIP type the following:
$ sudo apt-get install python3-pip
Virtualenv #
Just like we did in the development, we will use virtualenv to create a virtual environment. Install virtualenv by typing:
$ pip3 install virtualenv
PostgreSQL #
PostgreSQL is the preferred database in the Django community. To install it type:
$ sudo apt-get install postgresql postgresql-contrib
After the installation database server will start automatically. To test the status of the server type:
$ sudo service postgresql status
The output will be like this:
1 2 3 4 5 6 7 8 9 | ● postgresql.service - PostgreSQL RDBMS
Loaded: loaded (/lib/systemd/system/postgresql.service; enabled; vendor preset: enabled)
Active: active (exited) since Fri 2018-05-18 13:33:21 UTC; 1h 54min ago
Main PID: 20416 (code=exited, status=0/SUCCESS)
CGroup: /system.slice/postgresql.service
May 18 13:33:21 djangobin-ubuntu systemd[1]: Starting PostgreSQL RDBMS...
May 18 13:33:21 djangobin-ubuntu systemd[1]: Started PostgreSQL RDBMS.
May 18 13:33:26 djangobin-ubuntu systemd[1]: Started PostgreSQL RDBMS.
|
Nginx #
Nginx is a high-performance web server with very low footprint. We will use Nginx as a proxy server and to serve static files. To install it by type:
$ sudo apt-get install nginx
Once installed, Nginx will start automatically. We can check the status of the Nginx server by typing:
$ sudo service nginx status
The output will be something like this:
1 2 3 4 5 6 7 8 9 10 | ● nginx.service - A high-performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2018-05-18 15:17:51 UTC; 9min ago
Main PID: 22691 (nginx)
CGroup: /system.slice/nginx.service
├─22691 nginx: master process /usr/sbin/nginx -g daemon on; master_process on
└─22692 nginx: worker process
May 18 15:17:51 djangobin-ubuntu systemd[1]: Starting A high performance web server and a reverse proxy server...
May 18 15:17:51 djangobin-ubuntu systemd[1]: Started A high performance web server and a reverse proxy server.
|
We can also test whether Nginx is running or not by directly requesting a page from it. Open your browser and visit http://167.99.235.81/
(replace 167.99.235.81 with your IP). You should get a page like this:
RabbitMQ #
Install RabbitMQ:
$ sudo apt-get install rabbitmq-server
Creating Database and User #
When you install PostgreSQL, it automatically creates a user named postgres
to perform administrative tasks.
Before we do anything, let's login with this account via psql
and create a new database.
$ sudo -u postgres psql
The output will be like this:
1 2 3 4 5 | psql (9.5.12)
Type "help" for help.
postgres=#
postgres=#
|
Create a new database by typing:
1 2 3 | postgres=# CREATE DATABASE djangobin;
CREATE DATABASE
postgres=#
|
Next, create a new user by typing:
1 2 3 4 | postgres=#
postgres=# CREATE ROLE db_user WITH LOGIN PASSWORD 'password' CREATEDB;
CREATE ROLE
postgres=#
|
Finally, grant all privileges on database djangobin
to db_user
:
Creating Virtual Environment and Setting up Project #
To clone the repository type the following command:
$ git clone https://github.com/overiq/djangobin.git
This will create a directory named djangobin
inside your current working directory. Change the current working directory to djangobin
using the cd
command and create a new virtual environment:
1 2 | $ cd djangobin
$ virtualenv env
|
Once done, activate the virtual environment and cd
into the django_project
directory (the same place where manage.py
is located).
1 2 | $ source env/bin/activate
$ cd django_project/
|
Next, install the dependencies from the requirements file.
$ pip install -r requirements.txt
Since we are using PostgreSQL database in production, we need to install PostgreSQL database adapter for Python, called psycopg2.
$ pip install psycopg2
Create a JSON file to store sensitive configuration.
$ nano djangobin-secrets.json
And add the following code to it:
djangobin/django_project/djangobin-secrets.json
1 2 3 4 5 6 7 8 9 10 11 12 | {
"SECRET_KEY": "rj3vhyKiDRNmth75sxJKgS9JP8Gp7SpsS9xAlvBMTXW3Z6VTODvvFcV3TmtrZUbGkHBcs$",
"DATABASE_NAME": "djangobin",
"DATABASE_USER": "db_user",
"DATABASE_PASSWORD": "password",
"DATABASE_HOST": "127.0.0.1",
"DATABASE_PORT": "5432",
"EMAIL_HOST_USER": "apikey",
"EMAIL_HOST": "smtp.sendgrid.net",
"EMAIL_HOST_PASSWORD": "TW.qQecgRphQDa3TkLLlj18pqA.5Xrjod3G8XXojH45W4loxAsktdY3Nc",
"EMAIL_PORT": 587
}
|
Make sure to replace database credentials and API keys appropriately.
At this point, if you try to execute ./manage.py
file you will get an error because Django doesn't know where your setting file is located:
Specify the settings file location temporarily using the export
command:
export DJANGO_SETTINGS_MODULE=django_project.settings.prod
With this command, we have put our application in production mode.
To create all the necessary tables in the djangobin
database run migrate
command:
$ ./manage.py migrate
Create a superuser for the project by typing:
1 2 3 4 5 6 | $ ./manage.py createsuperuser
Username (leave blank to use 'django'): admin
Email address: admin@mail.com
Password:
Password (again):
Superuser created successfully.
|
Next, create a guest user and set its is_active
attribute to False
, so that the account can't be used to login.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $ ./manage.py createsuperuser
Username (leave blank to use 'django'): guest
Email address: guest@mail.com
Password:
Password (again):
Superuser created successfully.
$
$
$ ./manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
>>> from django.contrib.auth.models import User
>>>
>>> u = User.objects.get(username="guest")
>>>
>>> u.is_active
True
>>>
>>> u.is_active = False
>>>
>>> u.save()
>>>
>>> u.is_active
False
>>>
|
As said before, we will serve static files via Nginx server. To collect all the static files of the project in the static
directory type the following command:
$ ./manage.py collectstatic
Gunicorn #
Nginx will face the outside world and will serve static files. However, it can't communicate to Django application; it needs something that will run the application, feed it requests from the web, and return responses. This is where Gunicorn comes into the play.
Install Gunicorn by typing:
$ pip install gunicorn
To server our application via Gunicorn type the following command:
1 2 3 4 5 6 7 | $ gunicorn -w 3 -b 0.0.0.0:8000 django_project.wsgi
[2018-05-19 07:07:32 +0000] [25653] [INFO] Starting gunicorn 19.8.1
[2018-05-19 07:07:32 +0000] [25653] [INFO] Listening at: http://0.0.0.0:8000 (25653)
[2018-05-19 07:07:32 +0000] [25653] [INFO] Using worker: sync
[2018-05-19 07:07:32 +0000] [25656] [INFO] Booting worker with pid: 25656
[2018-05-19 07:07:32 +0000] [25658] [INFO] Booting worker with pid: 25658
[2018-05-19 07:07:32 +0000] [25659] [INFO] Booting worker with pid: 25659
|
This command starts Gunicorn with three worker processes and binds the socket to 0.0.0.0
address. By default, Gunicorn only listens on the local interface (i.e 127.0.0.1
), which means you can't access your work from other computers on the network. To tell Gunicorn to listen on all interfaces bind the socket to 0.0.0.0
.
Open your browser and navigate to http://167.99.235.81:8000/
. You should see a page like this:
Our application appears to be broken. That's expected because we are not serving static files yet.
Setting up Nginx #
Gunicorn is up and running, now we need to configure Nginx to pass the requests to it.
Start by creating a server configuration file in the /etc/nginx/sites-available/
directory:
$ sudo nano /etc/nginx/sites-available/djangobin
Next, add the following configuration to the file:
/etc/nginx/sites-available/djangobin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | server {
server_name 167.99.235.81;
access_log off;
location /static/ {
alias /home/django/djangobin2/django_project/static/;
}
location / {
proxy_set_header Host $http_host;
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';
}
}
|
Replace 167.99.235.81
with your IP and path to the static
directory to match your own file system.
To enable this configuration create a symbolic link in the sites-enabled
folder.
$ sudo ln -s /etc/nginx/sites-available/djangobin /etc/nginx/sites-enabled/djangobin
Test the configuration file syntax:
1 2 3 | $ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
|
Finally, restart the server for the changes to take effect.
$ sudo service nginx restart
Now, we are ready to test whether everything is working or not.
First off, start the celery worker along with celery beat by typing:
$ celery -A django_project worker -l info -B
Hit Ctrl+Z, followed by bg
to put the process into the background. Then, start the Gunicorn by typing:
$ gunicorn -w 3 -b 127.0.0.1:8000 django_project.wsgi
Note that this time we are binding the socket to listen on the local interface (i.e 127.0.0.1) because this time Nginx will face the outside world instead of Gunicorn.
Open your browser and visit http://167.99.235.81/
. You should see the index page of DjangoBin like this:
If you try to visit About or EULA flatpage you will get 404 error because these pages do not yet exist in the database. To create these pages login to Django admin site by visiting http://167.99.235.81/admin/
.
Enter user and password we created earlier in this chapter.
Click on the "Add" link in front of "Flat pages" and add About and EULA page as follows:
While we are at it, let's update the domain name in sites framework (django.contrib.sites
), so that the sitemap framework can generate correct links.
Visit site list page at http://<your_ip_address>/admin/sites/site/
. Click the domain name to edit and enter your server IP address in the Domain name and Display name fields, as follows:
Hit the save button to update the changes.
At last, visit the Contact page and submit a message. All the admins listed in ADMINS
setting will receive an email as follows:
Things are working as expected but what would happen if the gunicorn or celery gets killed for some reason or DigitalOcean restarts your droplet after performing some maintenance?
In that case, users will see 502 Bad Gateway error:
We can prevent such errors by using a process monitoring tool called Supervisor.
Monitoring Process with Supervisor #
Supervisor is a tool which allows us to monitor processes. Its job is to make sure certain processes keep running. If the process dies or gets killed for some reason, the Supervisor will start it automatically.
Install Supervisor by typing the following command:
$ sudo apt-get install supervisor
Supervisor will start automatically after the installation. We can check it's status by typing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | $ sudo service supervisor status
● supervisor.service - Supervisor process control system for UNIX
Loaded: loaded (/lib/systemd/system/supervisor.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2018-05-19 15:27:28 UTC; 1min 16s ago
Docs: http://supervisord.org
Main PID: 592 (supervisord)
CGroup: /system.slice/supervisor.service
└─592 /usr/bin/python /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf
May 19 15:27:28 djangobin-ubuntu systemd[1]: Started Supervisor process control system for UNIX.
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,830 CRIT Supervisor running a
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,831 WARN No file matches via
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,847 INFO RPC interface 'super
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,847 CRIT Server 'unix_http_se
May 19 15:27:28 djangobin-ubuntu supervisord[592]: 2018-05-19 15:27:28,848 INFO supervisord started
|
With Supervisor installed, we now have access to echo_supervisord_conf
command to create configuration files.
The configuration file is a Windows-INI-style file, which defines the program to run, how to handle output, environment variables to pass to programs and so on.
When Supervisor starts, it automatically reads configurations from /etc/supervisor/conf.d/
directory.
Create a new configuration file by typing:
$ echo_supervisord_conf > ./djangobin.conf
And then move it to /etc/supervisor/conf.d/
directory using the mv
command:
$ sudo mv djangobin.conf /etc/supervisor/conf.d/
If you open djangobin.conf
file, you will find that it contains lots of sections and comments (lines starting with ;
). Delete all the sections except the supervisor
section at the top of the file. At this point, the file should look like this:
/etc/supervisor/conf.d/djangobin.conf
1 2 3 4 5 6 7 8 9 | [supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
|
The next step is to add one or more [program:x]
section in order for the Supervisor to know which programs to start and monitor. The x
in the program section refers to the arbitrary unique label given to every section. This label will be used to manage the program.
The following table lists some common options we can define inside the [program]
section.
Option | Description | Required |
---|---|---|
command |
This option specifies the path of the program to run. | Yes |
directory |
It specifies the directory where Supervisor will cd into before running the program |
No |
autostart |
If set to true tells Supervisor to start the program when the system boots. |
No |
autorestart |
If set to true tells Supervisor to start the program if it dies or gets killed. |
No |
stdout_logfile |
The file to store the standard output of the process. | No |
stderr_logfile |
The file to store the standard error of the process. | No |
Open djangobin.conf
file and add the following three [program]
section towards the end of the file:
/etc/supervisor/conf.d/djangobin.conf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | [program:gunicorn]
command=/home/django/djangobin2/env/bin/gunicorn --access-logfile - --workers 3 --bind 127.0.0.1:8000 django_project.wsgi:application
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/gunicorn.err.log
stdout_logfile=/var/log/gunicorn.out.log
[program:celery_worker]
command=/home/django/djangobin2/env/bin/celery -A django_project worker -l info
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/celery.err.log
stdout_logfile=/var/log/celery.out.log
[program:celery_beat]
command=/home/django/djangobin2/env/bin/celery -A django_project beat -l info
directory=/home/django/djangobin2/django_project
autostart=true
autorestart=true
stderr_logfile=/var/log/celery_beat.err.log
stdout_logfile=/var/log/celery_beat.out.log
|
We also want Supervisor to pass DJANGO_SETTINGS_MODULE
environment variable to all the three process. To do so, add environment
option at the end of [supervisord]
section as follows:
/etc/supervisor/conf.d/djangobin.conf
1 2 3 4 5 6 7 8 9 10 | [supervisord]
logfile=/tmp/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10 ; (num of main logfile rotation backups;default 10)
loglevel=info ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false ; (start in foreground if true;default false)
minfds=1024 ; (min. avail startup file descriptors;default 1024)
minprocs=200 ; (min. avail process descriptors;default 200)
environment=DJANGO_SETTINGS_MODULE="django_project.settings.prod"
|
Tell Supervisor to load this new configuration type the following two commands:
1 2 3 4 5 6 7 8 | $ sudo supervisorctl reread
celery_beat: available
celery_worker: available
gunicorn: available
$ sudo supervisorctl update
celery_beat: added process group
celery_worker: added process group
gunicorn: added process group
|
You will have to execute these two commands every time you modify the configuration file.
Now, all our programs are up and running. At any time, you can check the status of programs by typing:
1 2 3 4 5 | $ sudo supervisorctl status
celery_beat RUNNING pid 6027, uptime 1:44:03
celery_worker RUNNING pid 6028, uptime 1:44:03
gunicorn RUNNING pid 6029, uptime 1:44:03
supervisor>
|
If we start supervisorctl
program without passing any argument, it will start an interactive shell which allows us to control processes currently managed by Supervisor.
1 2 3 4 5 | $ sudo supervisorctl
celery_beat RUNNING pid 6027, uptime 1:48:42
celery_worker RUNNING pid 6028, uptime 1:48:42
gunicorn RUNNING pid 6029, uptime 1:48:42
supervisor>
|
As you can see, in interactive mode supervisorctl
starts by printing the status of currently managed programs.
Once you are inside the interactive shell, to see the available commands, type help
:
1 2 3 4 5 6 7 8 9 10 | supervisor>
supervisor> help
default commands (type help <topic>):
=====================================
add exit open reload restart start tail
avail fg pid remove shutdown status update
clear maintail quit reread signal stop version
supervisor>
|
Now, we can stop, start and restart process using the corresponding command followed by the program label.
1 2 3 4 5 6 7 8 9 10 11 | supervisor>
supervisor> stop gunicorn
gunicorn: stopped
supervisor>
supervisor> start gunicorn
gunicorn: started
supervisor>
supervisor> restart gunicorn
gunicorn: stopped
gunicorn: started
supervisor>
|
To get the status of all the running processes type status
:
1 2 3 4 | celery_beat RUNNING pid 6027, uptime 5:51:00
celery_worker RUNNING pid 6028, uptime 5:51:00
gunicorn RUNNING pid 12502, uptime 0:02:06
supervisor>
|
We get also read the contents of the log file using the tail
command:
1 2 3 4 5 6 7 8 | supervisor>
supervisor> tail gunicorn
27.0.0.1 - - [20/May/2018:13:56:42 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
127.0.0.1 - - [20/May/2018:13:56:42 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
127.0.0.1 - - [20/May/2018:13:56:43 +0000] "POST /GponForm/diag_Form?images/ HTTP/1.0" 404 92 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
127.0.0.1 - - [20/May/2018:13:57:31 +0000] "GET / HTTP/1.0" 200 10327 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
supervisor>
|
By default, the tail
command reads from stdout
. Here is how we can read from stderr
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | supervisor>
supervisor> tail gunicorn stderr
[2018-05-20 17:29:28 +0000] [12492] [INFO] Worker exiting (pid: 12492)
[2018-05-20 17:29:28 +0000] [12493] [INFO] Worker exiting (pid: 12493)
[2018-05-20 17:29:28 +0000] [12490] [INFO] Worker exiting (pid: 12490)
[2018-05-20 17:29:29 +0000] [12487] [INFO] Shutting down: Master
[2018-05-20 17:29:29 +0000] [12502] [INFO] Starting gunicorn 19.8.1
[2018-05-20 17:29:29 +0000] [12502] [INFO] Listening at: http://127.0.0.1:8000 (12502)
[2018-05-20 17:29:29 +0000] [12502] [INFO] Using worker: sync
[2018-05-20 17:29:29 +0000] [12505] [INFO] Booting worker with pid: 12505
[2018-05-20 17:29:29 +0000] [12507] [INFO] Booting worker with pid: 12507
[2018-05-20 17:29:29 +0000] [12508] [INFO] Booting worker with pid: 12508
supervisor>
|
Finally, we can stop, start and restart all the processes at once as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | supervisor>
supervisor> stop all
celery_beat: stopped
gunicorn: stopped
celery_worker: stopped
supervisor>
supervisor>
supervisor> start all
celery_beat: started
celery_worker: started
gunicorn: started
supervisor>
supervisor>
supervisor> restart all
celery_beat: stopped
gunicorn: stopped
celery_worker: stopped
celery_beat: started
celery_worker: started
gunicorn: started
supervisor>
|
Once you are done hit Ctrl+C or type quit
to exit the supervisorctl shell.
Supervisor is now monitoring all our processes. If any process dies or gets killed for some reason supervisor will start the process automatically.
As a test, try rebooting the droplet using the sudo reboot
command. You will find that all the processes will start automatically on boot.
Congratulations, you have successfully deployed the DjangoBin project.
Load Comments