Deploying a website is a pain. Using a "simplified" service, e.g. Heroku, it's still a pain but you don't learn very much. I learned that firsthand when I deployed my first Django project using Heroku. When the process was complete, I felt I had learned vaguely how to use Heroku's tools, and next to nothing about deployment in general.

For my next project, I decided to use a service that could provide me with only a Unix box and force me to do everything else. I went with DigitalOcean, as it offers an inexpensive basic server and met my needs.

DigitalOcean provides some helpful guides for getting started, but I quickly ran into caveats and outdated information that could be very frustrating. After a lot of googling and trial-and-error, I successfully launched that second project; however, I made the mistake of not documenting my process. When it came time to deploy the site containing this blog, I resolved to take detailed notes about the entire process, to ease future deployments and hopefully help others.

My intent with this article is to create "the comprehensive guide I wish I had" when trying to deploy my first site. I've tried to both synthesize the information from other guides, as well as rectify portions found to be incorrect or outdated. So without further ado, here it is.1

1 Resources

Here are some resources that may be helpful over the course of this guide, conveniently gathered to prevent incessant googling. Many of the steps on this page are based largely on the links in the "Tutorials" section. I do not recommend reading the tutorials right away, as there are significant problems with some of them (discussed infra); however, they may be useful to consult as a secondary source.

1.1 Docs

1.2 Tutorials

2 Assumptions

A guide like this is difficult due to the sheer number of options for each component of a website. There's a choice to be made between generalizing for a wider audience but lacking complete detail for any one stack, and writing a tightly focused guide but losing applicable readers. Given the stack used for this site is a common one, this guide as a whole is catered to that particular stack, with all the specifics that entails.

2.1 General

This guide assumes basic familiarity with Linux and working with the terminal. Things like pressing Enter to execute a command, how to navigate to a directory, and how to use an in-terminal text editor go unstated. The commands for anything more complicated than these will be explicitly given.

2.2 Django Project

Naturally, prior to the deployment process, one needs a project. This guide assumes a project that was developed locally, tested, and is ready to go. I find the guides that deploy an empty starter project, while likely more concise, often lack some important concepts.

The project discussed in this article is generally referred to as yourproject, or yourprojectrepo at the repository root level.4 This name will frequently appear, e.g. in the nginx configuration file, the virtualenv, ssh key files, etc. When following this guide for your own project, rename any such name in line with your own project name.

The source for this site is available here. Perusing the source may aid in clarifying something about my project that is not properly explained in this guide.2

2.3 Stack

It is assumed that the machine used to develop the site is running a recent Ubuntu release, but naturally any Unix flavor would be applicable with possibly a little tweaking.

The version of Python assumed is 3.4. The version of Django assumed is 1.7.6 (check yours with import Django; django.VERSION in a Python shell), but anything 1.7/1.8 should be fine.3 The version control system assumed is git.

The desired web stack assumed for this guide is a PostgreSQL database, Nginx to serve static files, and Gunicorn as the WSGI HTTP server. The Django source, database, and static files are assumed to be hosted on a single machine.

2.4 Layout

The general project layout assumed follows that described in Chapter 3 of Two Scoops of Django. An overview of the relevant portions of the layout is seen below:

yourprojectrepo/              # REPOSITORY ROOT
    staticfiles/              # `collectstatic` will place static files here. 
    yourproject/              # PROJECT ROOT
        static/               # All static files here
        yourproject/          # CONFIGURATION ROOT
            settings/         # Note settings subpackage, multiple modules

I'm not advocating changing an existing project to this layout, merely putting it here for reference.

Note the repository, project, and configuration root directories, which will be referred to throughout this guide.

The other important piece of the layout to note for this guide is the multiple settings files. These are set up as discussed in section 5.2 of the aforementioned Two Scoops.

2.5 Virtualenv and Environment Variables

This guide assumes virtualenv and virtualenvwrapper are being used in local development (and will be used in production). If not using virtualenvwrapper, the primary (only?) differences will be how the virtual environent is created and activated, so it is not particularly essential.

Sensitive data such as the Django secret key, database credentials, etc. are assumed to be kept in environment variables exported and unset, respectively, by the postactivate and predeactivate scripts of the project's virtual environment. Assuming virtualenvs are kept in /home/yournamehere/.virtualenvs, these files are located as such:


postactivate might look like this:

# This hook is sourced after this virtualenv is activated.

# I mashed the keyboard; this is not a real secret key.
export SECRET_KEY='ukh3qfu%3jh@jf(' 
export DB_USERNAME=yournamehere
export DB_PASSWORD=dbpassword

In that case, predeactivate should contain:

# This hook is sourced before this virtualenv is deactivated.

If this is new to you, note that the Django settings would access these variables like so:

from os import environ


3 Steps

With the assumptions out of the way, here are the steps to deploy.

3.1 Prepare Django Project

Before starting the deployment process, it's a good time to look over your project again with the development server, and do any preparation necessary for deployment. The Deployment Checklist is a helpful reference for this.

This process might entail the following:

  • Minify CSS/JavaScript and ensure all templates are using the minified files.

  • Ensure requirements.txt is up to date (pip freeze > requirements.txt at the repository root is the easiest way). pip will be used on the remote machine to populate the virtual environment with necessary packages, so this is important.

  • Look over the production settings module. Insert the domain you own/intend to purchase in ALLOWED_HOSTS if necessary. Make sure DEBUG is turned off!

  • Ensure the default settings module in yourproject/yourproject/ is the production settings:

    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "yourproject.settings.production").
  • Probably a git commit

For reference, here are this site's production settings:5

from .base import *

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR.parent.child('staticfiles')
DEBUG = False

3.2 Create a DigitalOcean Droplet

If you have not used DigitalOcean before, you will need to create an account.

Once you are logged in, click the "Create Droplet" button. This DigitalOcean guide provides a tutorial on creating a droplet.

For reference, my settings were:

  • Hostname: portfolio
  • Size: Smallest. 512 MB RAM, 20 GB SSD, 1000 GB transfer.
  • Region: New York 3
  • No extras (IPV6, private networking, etc.)
  • Image: Ubuntu 14.10 x32
  • Applications: None
  • SSH Keys: None (these will be handled later in the guide)

The default image is x64. Note it is recommended to use a 32-bit OS with <1GB ram, as per the guide linked above.

3.3 Purchase Domain (Optional)

This step is technically optional (you could just enter the IP of your droplet in your browser) but my assumption is that most will want a domain. This step also does not need to be done at exactly this time (or perhaps has already been done).

I used to purchase my domain. I used that service on a previous site without incident. Any service that easily permits you to allow a different host to hanldle DNS is fine.

3.3.1 Set Up DNS

If you wish to use a domain name, the next step is to set up a DNS record at DigitalOcean to point your domain to your droplet's IP.

From the dashboard at the DigitalOcean website (log in again if necessary), click the "DNS" button, then "Add Domain" on the upper right. The "name" box should contain your domain, e.g. "" For "IP Address" either enter your droplet's IP or select it from the "Select a droplet" dropdown menu, then click "Create Domain."

In the domain's record, there should be three "NS" entries. With the URLs in those entries in mind, go on to the next step.

3.3.2 Transfer DNS

A request to your domain needs to be pointed to DigitalOcean's DNS servers. Whichever service you bought your domain with should allow this somehow.

If you used, follow the instructions below.6 If you used a different service, you may find instructions for it at How to Point to DigitalOcean Nameservers From Common Domain Registrars.

  1. Log in to namecheap.

  2. From your account's Home Page, click "Your Domains/Products"

  3. Click your domain name.

  4. On the left, click "Transfer DNS to Webhost."

  5. In the dropdowns under "Specify Custom DNS Servers," enter the three URLS from DigitalOcean's DNS record. For me, they were,, and

  6. Click "Save Changes."

3.4 Initial Droplet Connection and Setup

By this point, DigitalOcean should have sent an email with the droplet's IP and root password.

Follow steps one through three in the DigitalOcean guide found here to do some initial setup steps on the droplet.

It is convenient, but not necessary, to make your user name on the droplet machine the same as your local user name.

It's a decent idea at this point to upgrade any packages on the droplet with:

sudo apt-get update
sudo apt-get upgrade

The DigitalOcean guide linked in the previous step covers setting up SSH authentication, but I found it to be inadequate. Specifically, because this was my second site with an SSH key pair, there were some extra steps. Even if you are deploying your first/only site, I recommend following these steps because these steps will automate connection to the droplet with a friendly name.

For the sake of brevity, I won't go into the details of SSH key pairs, merely provide the steps.7

3.5.1 Generate a Key Pair

On the local machine, enter ssh-keygen on the terminal. You will be asked to enter a path for the key file, or use the default. Enter your own path, such as ~/.ssh/yourproject_rsa, substituting your project's name.

Next you will be prompted to enter a passphrase. Do it if you wish, but this guide will assume the passphrase was left blank.

In the ~/.ssh directory, there should now be two files: yourproject_rsa and The .pub file contains the public key, and the other file contains the private key.

Copy the entire contents of the public key file to the clipboard.

3.5.2 Copy the Public Key to the Droplet

Back in the terminal connected to the droplet, switch to the user you created in the Initial Droplet Connection step with su - yournamehere if you haven't already done so. In your user's home directory, create the .ssh directory and restrict its permissions with mkdir .ssh and chmod 700 .ssh.

Next, open a file in the .ssh folder named authorized_keys, paste the public key from the local machine into it, and save it.

Restrict the permissions of .ssh/authorized_keys with chmod 600 .ssh/authorized_keys.

3.5.3 Configure SSH on the Local Machine

Back in a terminal for the local machine, navigate to the .ssh directory from the previous step (probably cd ~/.ssh).

Open a file named config in the .ssh directory, and use the following format for its contents:

Host [friendly-name]
Hostname [ip.of.droplet]
Port [####]
IdentityFile [~/.ssh/private_key_file]

Replace the placeholders above (anything surrounded by brackets, but omit the brackets in the file) with your own information.

If omitted, the default port is 22. For security purposes, it is recommended to change it to some other port between 1025 and 65536.

For reference, my .ssh/config file looks like this (the port was changed on account of security):

Host portfolio
Port 1025
IdentityFile ~/.ssh/portfolio_rsa

Notice multiple Host names can be provided. These are the names by which you can refer to the remote server to ssh, i.e. either ssh yourproject or ssh would use the settings in the config file to connect to the droplet.

If your username on the local machine and droplet differ, an additional User field can be added to config to specify the remote user name, e.g.:

User yourremoteusername

Additional servers may be added to config using the same format, as seen in this StackOverflow answer.

3.5.4 Configure SSH on the Remote Machine

In the terminal connected to the droplet, open the file /etc/ssh/sshd_config.

In that file, find the line that contains Port 22 and change the number to match what you put in the Port line of .ssh/config in the previous step.

In the same file, find the line that says PermitRootLogin yes and change it to PermitRootLogin no. This disallows using root as the username to login as via ssh, and is a preventative security measure. Save and exit the file.

Restart ssh with:

service ssh restart

3.5.5 Test SSH Connection

SSH should now be properly configured, but this must be verified before moving on.

Open a new terminal on the local machine, and run...

ssh yourproject

...substituting your Host name from the .ssh/config file.

At this point, I ran into a problem. I received the error "ssh agent admitted failure to sign using the key." If this happens, run...

ssh-add ~/.ssh/yourproject_rsa

...substituting your private key file's name. Now, try connecting to the droplet again.

If successful, you should be logged into the remote machine without having to type a password (unless you provided an SSH passphrase).

Now that you can login to your droplet via SSH without your password, you may wish to disable password authentication as an extra layer of security.

Be sure to backup the private SSH key to a safe place if disabling password authentication, as it will always be necessary to access your droplet.

To disable password authentication, follow the steps in this tutorial (only the linked section "Disabling Password Authentication on your Server").

Do not close the terminal connected to the droplet or logout until you have verified, from a separate terminal window, that you can ssh into your droplet successfully.

Follow the steps in this DigitalOcean guide.

I ran into no issues following the steps in that guide exactly as given.

3.7 Install Things

Now its time to start installing the things necessary for the web stack.

Python3 is assumed throughout. If using Python2, remove "3" from any package name, i.e. python3-pip becomes python-pip.

All of the following should be done in a terminal connected to the droplet.

3.7.1 pip

sudo apt-get install python3-pip

3.7.2 PostgreSQL

sudo apt-get install libpq-dev python3-dev
sudo apt-get install postgresql postgresql-contrib

3.7.3 Nginx

sudo apt-get install nginx

3.7.4 git

If you're using a version control system that is not git, skip this step.

sudo apt-get install git

3.7.5 virtualenv

sudo apt-get install python3-virtualenv

3.7.6 virtualenvwrapper

First, run:

sudo pip3 install virtualenvwrapper

Then, open ~/.bashrc and add the following lines at the end of the file:

export WORKON_HOME=$HOME/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
source /usr/local/bin/

You may need to alter the first two lines to point to where you want your virtualenvs and projects located, respectively.

Finally, run:

source ~/.bashrc

virtualenvwrapper should now be available at all times.

3.8 Configure PostgreSQL

It is now time to set up the database. First, in the droplet's terminal, change the current user to the postgres superuser:

sudo su - postgres

Next create a database. Remember to use the database name that is in your DATABASES Django setting.

createdb yourproject

Now create a new user. Again, this user name should match what is in your DATABASES setting.

createuser -P yournamehere

Then enter the postgres shell and grant privileges on the new database to the new user:

postgres=# GRANT ALL PRIVILEGES ON DATABASE yourproject TO yournamehere;
postgres=# \q

Finally, change back to your normal username:

su - yournamehere

3.9 Make virtualenv

Next, still on the droplet's terminal, create the virtualenv for your Django project:

mkvirtualenv --python=/usr/bin/python3 yourproject

The default python3 on the droplet, 3.4.2, was what I used to develop, so that's what I used in the virtualenv. Installing a different version of Python is a topic too far out of scope for this guide, but if you install one, point to its exectuable as in the example above to make it the default python of the virtualenv.

3.10 Set Up Droplet Environment Variables

First, read over Virtualenv and Environment Variables above if you have not already done so.

The virtualenv on the droplet should be set up the same way as the local one. Either copy and paste the contents of the postactivate and predeactivate files into the respective files on the remote machine, or use scp to copy the files over (be aware this will overwrite the files on the remote machine).

To do the latter, use (on the local machine):

 scp ~/.virtualenvs/yourproject/bin/postactivate \

 scp ~/.virtualenvs/yourproject/bin/predeactivate \

3.11 Clone Project

The actual code for the Django project can now be placed on the remote machine. Hopefully, your project is under version control and you can clone it over to the remote. If you're using git and have pushed your repo to Github, that would look something like this:

# On the terminal connected to the droplet
cd ~
mkdir dev
cd dev/
git clone

Replace the URL with your own, of course.

In this case, the dev directory is used to coincide with the path given for $PROJECT_HOME earlier when setting up virtualenvwrapper. Use whatever name/path you'd like, but make sure the path matches $PROJECT_HOME.

3.12 Transfer Static Files Not in Version Control (Optional)

I didn't keep some static files (images, fonts, etc.) under version control. If you have done something similar, now is the time to transfer any extra static files from your local environment over to the remote one.

To do this, use scp with the -r switch to recursively copy everything in a given directory.


# On the local machine
scp -r ~/dev/yourprojectrepo/yourproject/static/images/ \ 

Note scp is also using SSH, and the yourproject: in the second argument is the Host name from the ~/.ssh/config file.

3.13 Install Required Project Packages

Next, install the packages required to use your Django project. If you have a requirements.txt (you should), navigate to the repository root on the remote machine and run:

# Activate the virtualenv
workon yourproject

# Install required packages
pip install -r requirements.txt

Make sure to activate the virtualenv with workon first.

Things that definitely need to be in requirements.txt are Django and psycopg2 (for PostgreSQL).

3.13.1 Install Gunicorn

If Gunicorn wasn't in your requirements.txt, install it now with:

# With the virtualenv still activated
pip install gunicorn

3.14 Migrate

Now that the database is configured, the project code is on the remote machine, and Django and psycopg2 are installed for the virtualenv, the database can be set up for your project. If using Django 1.7+, doing this should be a simple matter of using migrations.

If you have not yet run makemigrations, or the migrations aren't under version control for some reason, run this now (with the virtualenv still activated):

# From the project root directory
python makemigrations

Apply the migrations with:

# From the project root directory
python migrate

3.15 Create Django Superuser

If you need a superuser in Django (e.g. for the admin site), create one now with:

# From the project root directory
python createsuperuser

Follow the prompts to create the user.

3.16 collectstatic

Django's collectstatic will copy all static files into the STATIC_ROOT directory, as defined in the project's settings.

If your STATIC_ROOT directory is inside your repository, make sure your version control system is set up to ignore it.9

For example, if your STATIC_ROOT is yourprojectrepo/staticfiles/, this line should be added to your .gitignore file:


My sets local settings by default (to be used with runserver in the development environment), and I only define STATIC_ROOT in my production settings, so when calling collectstatic I set the settings to be used explicitly:

python collectstatic --settings=yourproject.settings.production

Answer "yes" to the prompt if the paths look correct.

3.17 Test Site with Development Server (Optional)

With the project source and the database set up on the droplet, it's possible to use the Django development server to test that the site is working properly, even before configuring Nginx and Gunicorn.

This step can be helpful to ensure that your site is in the working state that it was in during development before moving on. However, use the development server only momentarily, as it is inefficient and unsafe to use in actual production.

Prior to this, however, you must temporarily open up a port to allow outside access to the development server:

sudo ufw allow 8000/tcp

Start the development server with runserver, specifying the IP of your droplet and the port you opened in the previous step.

# At the project root directory
python runserver ip.of.droplet:8000 --settings=yourproject.settings.production

While the development server is running, you should be able to access your site from Specifying the port is necessary because Nginx is not yet proxying requests from port 80.

When finished, kill the development server with Ctrl-C. Then, delete the firewall record that opened port 8000:

sudo ufw delete allow 8000/tcp

3.18 Set Up Nginx

This section assumes the following Django settings for production:

STATIC_URL = '/static/'
STATIC_ROOT = `/home/yournamehere/dev/yourprojectrepo/staticfiles/`

To set up Nginx, a server block must be created, and then enabled. The goal of the Nginx configuration in this guide is to serve all static files with Nginx, and proxy any other request to Gunicorn (and Django).

First, start the Nginx service with:

sudo service nginx start

Next, open a new Nginx configuration file with:

sudo nano /etc/nginx/sites-available/yourproject

The name of this file does not matter, but for the sake of consistency it makes sense to name it after your project.

Into this file, place the following:

server {
  listen 80;
  server_name yourproject;

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  location /static/ {
    alias /home/yournamehere/dev/yourprojectrepo/staticfiles/;

Make the following substitutions, if necessary:

  • After server_name, use your project name or domain name.

  • The path in the alias line should match your STATIC_ROOT, and should be an absolute path as shown.

  • The port on the proxy_pass line (8000 in the example) should be the port on which you intend Gunicorn to listen. Do not alter the localhost IP (

  • The path after location should match your STATIC_URL.

To activate the site for Nginx, place a symbolic link to the file you just created in the sites-enabled directory:

cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/yourproject

Remove the file default in this directory:

# In /etc/nginx/sites-enabled
sudo rm default

Finally, restart Nginx to activate the new site:

sudo service nginx restart

3.18.1 Test Nginx

Notice Nginx is proxying requests on port 80 to localhost. That makes it simple to test that Nginx is working properly using Django's development server, which by default listens on localhost.

# Navigate to project root
cd ~/dev/yourprojectrepo/yourproject

# Activate virtualenv, if not activated
workon yourproject

# Start the development server on port 8000
python runserver 8000 --settings=yourproject.settings.production

Make sure the port on the final line matches what's in the Nginx configuration file (proxy_pass) in the previous section.

Now, when accessing your domain (or the IP of your droplet) from a web browser, the site should appear. Do not specify a port this time; just navigate to e.g.

Kill the development server with Ctrl-C.

**Use the development server only momentarily to ensure that Nginx is working. It is inefficient and unsafe to use in actual production.**

3.18.2 If You Have Problems

Configuring Nginx is probably the most complicated step of this process. The configuration file above is about the simplest this file can be, but it's still easy to do something wrong the first time.

If, on trying to reach your site, you receive a 502 (bad gateway) error, that generally means nothing is listening on the port Nginx is trying to proxy to (8000 in the example above). Double check that the ports in the proxy_pass line and runserver call are the same.

For other problems, the Nginx error log may be helpful. On my system it is located at /var/log/nginx/error.log (and requires sudo permission to read).

Finally, you could try calling runserver with local settings (i.e. with DEBUG turned on) and see if that gives any more information about the problem.

3.19 Start Gunicorn

Once Nginx is working properly, it is time to start Gunicorn, the production HTTP server.

Go back to the terminal connected to your droplet. Make sure the virtualenv is still activated. To test that Gunicorn works, first start it as a normal process, like so:

# Run this at the project root directory
gunicorn yourproject.wsgi --bind

Note: Using gunicorn [APP_MODULE] is preferred over gunicorn_django.8

Again, make sure the port (8000 in the example above) matches what was in your Nginx configuration file.

Go to your domain (or droplet IP) in a browser. Hopefully, your site appears.

If your site is working, congratulations! You're basically done. Kill Gunicorn with Ctrl-C so you can prepare to run it as a daemon (i.e. in the background).

First make a file for Gunicorn logs to be placed in. I made a logs directory outside of the repository with a file name that specifies the project, like so:

cd ~/dev
mkdir logs
touch logs/yourproject_gunicorn.log
cd yourprojectrepo/yourproject

Now, start gunicorn as a daemon:

# From the project root directory
gunicorn yourproject.wsgi --bind --daemon \
    --log-file ~/dev/logs/yourproject_gunicorn.log --workers=3

The Gunicorn docs recommend workers be "in the 2-4 x $(NUM_CORES) range."

I found it useful to stick the above command in a script for reuse.

4 Maintenance

There are a few useful things to know and remember now that your site is deployed.

4.1 Stopping Gunicorn

Restarting Gunicorn may be necessary after pulling changes to your site's Python code. When you want to stop Gunicorn when it's running as a daemon, the simplest way is:

pkill gunicorn

That kills any running process called gunicorn.

4.2 collectstatic

Remember that whenever you pull in new static files or changes to existing ones, collectstatic must be run again to move the files to STATIC_ROOT where Nginx can find them. This assumes that the STATIC_ROOT directory is not under version control, which it should not be.9

5 Conclusion

At this point, your site should be fully deployed. Hopefully, this guide took some of the confusion and headache out of deployment.

6 Notes

  1. For the inaugural article of this blog, the deployment of the blog itself seems a particularly appropriate topic.  

  2. The vast majority of this guide will also work with Django 1.6. The only non-applicable portion would be the Migrate section, where syncdb would be used instead. 

  3. Just to be clear, I'm not advocating ending the names of repositories with "repo." It's just a convention used here so the generic name is unambiguous. 

  4. The setting of TEMPLATE_DEBUG to False is actually redundant. Turning off DEBUG will automatically also turn off TEMPLATE_DEBUG

  5. As of March 2015, there is a message on implying the UI described here will change in the near future, so the instructions given may not remain completely accurate. 

  6. For an overview of how SSH works, and why it is recommended, see the first few sections of this guide

  7. Starting with Django 1.4, is added automatically by startproject, making gunicorn_django unnecessary. As of Gunicorn 18.0, gunicorn_django is deprecated. The DigitalOcean deployment guide mistakenly advises to use gunicorn_django

  8. See the warning on this django-staticfiles docs page