Get in touch!
Back

Deploy a Django app in under 15 minutes!

By Ram Parameswaran Project deployment Dev-ops Bare-metal deployment

deovps.jpg

I've been working on client Django project for a few weeks now. However, so far the app just lives in a git repository and can only be run locally. It's time for deployment!


Step 1 - Folder structure

A common mistake: the code is well-documented, but the server config is not!

A typical Django deployment requires a number of server process configurations (e.g. Nginx, Celery, Supervisory, Gunicorn). These typically live in the /etc/ folder, which can be messy and unclear. What if you want to spin up a new server instance - how will you know which config files to copy over??

To mitigate this problem I use the folder structure outlined in this blog by Nick Sweeting. It recommends the following folder structure:

/home/
 - MyProject.com/
 -- .git
 -- .gitignore
 -- README.md
 -- bin/
 -- -- setup
 -- -- start
 -- -- stop
 -- -- backup
 -- -- update
 -- etc/
 -- -- nginx/ ...
 -- -- mysql/ ...
 -- -- cron.d/ ...
 -- -- ...
 -- data/
 -- -- MyProject/ ... [<- all Django files reside here]
 -- -- database/ ...
 -- -- logs/ ...

When deploying, all config files should be sim-linked back to the server's /etc/ directory. Et voila, now everything required for the project resides in one directory!


Step 2 - Server setup

The next step is setting up the server and virtual environment.

  1. Install correct python version
    sudo apt update
    sudo apt install software-properties-common
    sudo add-apt-repository ppa:deadsnakes/ppa
    sudo apt install python3.7
    sudo apt-get install python3.7-dev libpq-dev

  2. Clone the git repo
  3. Create a virtual environment with the correct python version. We'll again place the virtualenv inside the project directory.
    sudo apt-get install python-virtualenv
    virtualenv --python=/usr/bin/python3.7 .

  4. Create a system user with limited permissions. Why? Because your server security can be compromised, in which case you don't want the intruder to have root privileges. Here we create a group (I like to use the groupname 'webapps' in all my apps for consistency) and a user (named 'project_user' in this example) assigned to that group, with a home directory in the project directory.
    sudo groupadd --system webapps
    sudo useradd --system --gid webapps -shell /bin/bash --home /home/MyProject.com/ project_user


    And we make the project_user the ower of the project directory:
    sudo chown project_user:webapps /home/MyProject.com
    sudo chmod -R 777 /home/MyProject.com

  5. Install Django requirements from the requirements.txt file. I use Django-Cookiecutter for my projects so the requirements are usually spread across three files:
    pip install -r requirements/base.txt
    pip install -r requirements/local.txt
    pip install -r requirements/production.txt

  6. [Only if your project uses Postgres] Install PostgreSQL and create a database with the name and authentication specified in your Django settings.
    sudo apt-get install postgresql postgresql-contrib
    pip install psycopg2

    Create the database, database-user, and set permissions:
    sudo su - postgres
    psql
    CREATE DATABASE <db_name>;
    CREATE USER <db_username> WITH PASSWORD '<db_password>';
    ALTER ROLE <db_username> SET client_encoding TO 'utf8';
    ALTER ROLE <db_username> SET default_transaction_isolation TO 'read committed';
    ALTER ROLE <db_username> SET timezone TO 'Canada/Pacific';
    GRANT ALL PRIVILEGES ON DATABASE <db_name> TO <db_username>;
    \q
    exit


    Finally, run python manage.py migrate to build the database, and run python manage.py loaddata database/db.json to populate the database from an existing data dump.

    Your Django app should run locally now without a problem!


Step 3 - Gunicorn, Supervisor and Nginx setup

[**Firstly, if you don't know what Gunicorn and Nginx are and why you should use them, read this and this.]

  1. Install gunicorn
    pip install gunicorn
  2. Create a shell script to automatically initialise gunicorn to server the Django project when the server boots up. To do this we create a file named gunicorn_start in the ./bin/ directory in our project. The contents of the file will be:
#!/bin/bash
NAME="MyProject" # Name of the application
DJANGODIR=/home/MyProject.com/data # Django project directory
SOCKFILE=/home/MyProject.com/bin/run/gunicorn.sock # we will communicte using this unix socket
USER=project_user # the user to run as
GROUP=webapps # the group to run as
NUM_WORKERS=3 # how many worker processes should Gunicorn spawn
DJANGO_SETTINGS_MODULE=config.settings.production # which settings file should Django use
DJANGO_WSGI_MODULE=config.wsgi # WSGI module name

echo "Starting $NAME as `whoami`"

# Activate the virtual environment
cd $DJANGODIR
source ../bin/activate
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH

# Create the run directory if it doesn't exist
RUNDIR=$(dirname $SOCKFILE)
test -d $RUNDIR || mkdir -p $RUNDIR

# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec ../bin/gunicorn ${DJANGO_WSGI_MODULE}:application \
 --name $NAME \
 --workers $NUM_WORKERS \
 --user=$USER --group=$GROUP \
 --bind=unix:$SOCKFILE \
 --log-level=debug \
 --log-file=-
  1. And change the permissions on this file: sudo chmod u+x bin/gunicorn_start
  2. Install Supervisor. Supervisor is a process manager whose primary role will be to start the gunicorn service when the server boots.
    sudo apt-get install supervisor

    Create the supervisor config file myproject.conf inside the etc/supervisor/conf.d folder in your project directory. The file should contain:
    [program:MyProject]
    command = /home/MyProject.com/bin/gunicorn_start ; Command to start app
    user = project_user ; User to run as
    stdout_logfile = /home/MyProject.com/data/logs/gunicorn_supervisor.log ; Where to write log messages
    redirect_stderr = true ; Save stderr in the same log
    environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8 ; Set UTF-8 as default encoding


    Then create the logs folder and file:
    mkdir /home/MyProject.com/data/logs/
    touch /home/MyProject.com/data/logs/gunicorn_supervisor.log


    Simlink the conf.d file to your system's /etc/supervisor/conf.d folder.
  3. Install Nginx.
    sudo apt-get install nginx

    Create an Nginx config file (MyProject) in the etc/nginx/ folder in your project directory. It should contain:
upstream project_server {
 # fail_timeout=0 means we always retry an upstream even if it failed
 # to return a good HTTP response (in case the Unicorn master nukes a
 # single worker for timing out).

 server unix:/home/MyProject.com/bin/run/gunicorn.sock fail_timeout=0;
}

server {
 listen 80;
 server_name myproject.com;
 client_max_body_size 4G;
 access_log /home/MyProject.com/data/logs/nginx-access.log;
 error_log /home/MyProject.com/data/logs/nginx-error.log;

 location /static/ {
 alias /home/MyProject.com/data/staticfiles/;
 }

 location /media/ {
 alias /home/MyProject.com/data/media/;
 }

 location / {
 # an HTTP header important enough to have its own Wikipedia entry:
 # http://en.wikipedia.org/wiki/X-Forwarded-For
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

 # enable this if and only if you use HTTPS, this helps Rack
 # set the proper protocol for doing redirects:
 # proxy_set_header X-Forwarded-Proto https;

 # pass the Host: header from the client right along so redirects
 # can be set properly within the Rack application
 proxy_set_header Host $http_host;

 # we don't want nginx trying to do something clever with
 # redirects, we set the Host: header above already.
 proxy_redirect off;

 # set "proxy_buffering off" *only* for Rainbows! when doing
 # Comet/long-poll stuff. It's also safe to set if you're
 # using only serving fast clients with Unicorn + nginx.
 # Otherwise you _want_ nginx to buffer responses to slow
 # clients, really.
 # proxy_buffering off;

 # Try to serve static files from nginx, no point in making an
 # *application* server like Unicorn/Rainbows! serve static files.
 if (!-f $request_filename) {
 proxy_pass http://project_server;
 break;
 }
 }

 # Error pages
 error_page 500 502 503 504 /500.html;
 location = /500.html {
 root /home/MyProject.com/data/staticfiles/;
 }
}
  1. Again, simlink the conf.d file to your system's /etc/supervisor/nginx/sites-enabled folder.
    sudo ln -s /home/MyProject.com/etc/nginx/MyProject /etc/nginx/sites-enabled/MyProject


And we are done!

Now the site should be accesible at the server's IP address.

If there are any further problems, check the Gunicorn and Nginx logs. A common problem is with file/directory permissions. Often new files/directories are created with root permission. Simply change their owner to 'project_user'.

Ram Parameswaran

about Ram Parameswaran

Django & React Developer, Data Geek and Maker-Enthusiast.
Ram builds web applications that help businesses operate more efficiently.