Deploying Django Easily With A VPS, Docker And Nginx

Often when asking how to deploy Django, you'll hear things like render.com or heroku. While these are great services that make the process quite easy, i think there's much more value in learning how to deploy from the ground up and have complete control of it.

Table of Contents:

  1. Docker, Gunicorn & Nginx
  2. Prerequisites
  3. Setting up our docker files
  4. Preparing Django files
  5. Building our Docker image and container
  6. Configuring Nginx
  7. Deploying with PostgreSQL (Optional)
  8. Analytics with PostHog (Optional)



Docker, Gunicorn & Nginx

So before we go any further lets just do a quick run down on what these three are, for those who may not be familiar.

  • Gunicorn:
    A Python WSGI HTTP server used to serve Django apps and handle multiple requests concurrently. It acts as the bridge between our app and our web server (in this case Nginx).

  • Docker:
    A containerization platform that will allow us to easily configure how we want to build our app and package all the needed dependencies.

  • Nginx:
    The web server that will be our reverse proxy and will send requests to gunicorn. We will configure this for our domain and and also to serve our static files. Along with SSL certificates and access management.


![]

Setting up our docker files

Firstly we are going to need to create two Docker files in the root of our Django project (this just means where your manage.py file is). We will use this files to configure and build our docker container:

  • Dockerfile (create the file like this with no extension)
  • docker-compose.yml
  • requirements.txt

Now that we have these files we can start adding to them. We'll start with the Dockerfile, which is used to configure our image:

FROM python:3.12

COPY . /blogsite #change this to your project folder name
WORKDIR /blogsite #set your docker container directory name

RUN pip install -r requirements.txt
RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "blogsite.wsgi:application"]
  • FROM tells docker to use the python image for our container
  • COPY and WORKDIR copies your Django project into the containers working directory
  • RUN runs the needed commands like installing our packages and collecting static file
  • EXPOSE allows port 8000 to be accessed by the host
  • CMD this tells the Docker to run a command when it is built. Here we are running gunicorn to start our Django app running on 0.0.0.0:8000


Our docker-compose.yml file is used to tell docker how to build our image and its configuration:

version: '3'

services:
  web:
    image: itsterminal-v1.0
    container_name: itsterminal
    build: .
    command: gunicorn --bind 0.0.0.0:8000 blogsite.wsgi:application
  volumes:
    - .:/blogsite
    - ./staticfiles:/staticfiles
    - ./media:/media
  ports:
    - "8000:8000"
  environment:
    - DEBUG=0
  • services is where we assign all the services/containers we want to build. Currently we are only building our web container.
  • inside web we can name the image we will build from our Dockerfile, name the container that is created from the image and assign a command to run once its built. The build tells Docker where to find the Dockerfile we created earlier (in this case in the same directory).
  • volumes is how we map volumes from our host to our container. This means any changes made to the hosts folders will be reflected in the container. This makes it easy to update our files without having to ever enter the docker container.
  • ports simply maps are exposed container port 8000 to ours hosts.
  • environment allows us to change variables, in this case we want to change our project from debug to production in our settings.py file.


Although not necessarily being a docker file we will also want to prepare a requirements.txt of our dependencies for docker to use. It should look something like this:

Django==5.0.7
pillow==10.3.0
psycopg==3.2.2
markdown2==2.5.1
gunicorn==23.0.0

If you have been using a virtual environment for your Django project you can simply run the below command while inside your virtual environment to generate the file:

pip freeze > requirements.txt

If you haven't been using a virtual environment this will be a bit more difficult. You can still run pip freeze to get a list of all your python packages, you will just have to check all the packages you are using in your project and grab those packages from the pip freeze output and put them into a requirements.txt file.


Preparing Django files

On the side of our Django project files we will need to make changes to our settings.py file to get it ready for deployment. We will need to allow our domain name for our website along with hiding our SECRET_KEY into an environment variable. First you'll need to install the python package dotenv:

pip install python-dotenv

Once it is installed we will load and use it in our settings.py, the top of your file should look something like this (before replacing the SECRET_KEY line make sure to copy and paste your key someone so you can access it later and don't lose it):

from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env'))

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.getenv("SECRET_KEY")

DEBUG = True

ALLOWED_HOSTS = ["itsterminal.io", "127.0.0.1", "localhost"] #replace with your domain

We have setup our Django app to use a environment variable file for our SECRET_KEY. This means we now need to create that file and out our key in there. Create a .env file in the root of your project:

SECRET_KEY = "insert the key here"


Building our Docker image and container

From here we are ready to start interacting with our VPS. You will need to copy your entire Django project folder over to your VPS, however you like. Once you have all the files on the VPS. We can then install docker and docker-compose to start building our image and container.

The process to install Docker and docker-compose can vary depending on the Linux distro you are using. I wont cover the install process but you can find the steps on the official Docker docs. For example here is the process for debian based systems.

Once you have those installed move into your projects root folder, where you have your Docker files. To build your docker image and container simply run the following command:

docker-compose up -d
  • This will use our Dockerfile to build both the image and container
  • It will then instantly run this container in the background with -d (detached)


Configuring Nginx

If all went well with your build and your Django app is running correctly in the container. We can now move to configuring Nginx to route traffic to our domain that will be hosting the app. Note you will also need to go through the process of assigning your domain to the VPS IP address though your domain provider.

First simply install Nginx onto your system. For debian based system it is:

sudo apt update
sudo apt install nginx

Once installed we will need to current a configuration file for our site, which is called a server block. Create the following file (named after your domain) in the below directory and open it with your preferred text editor:

sudo nvim /etc/nginx/sites-enabled/itsterminal.io
  • This directory is where nginx looks for configurations of your sites


Once you have the file open we can start configuring Nginx to handle our site:

server {

  server_name yourdomain.com www.yourdomain.com;

  location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  location /static/ {
      alias /path/to/staticfiles/;
  }

  location /media/ {
      alias /path/to/media/;
  }
}
  • Make sure to set the correct path to your static and media files that are stored on the server
  • We also want to use port 443 for SSL so we will also need to setup a certificate


To setup an SSL certificate we can do this with lets encrypt which is a free, automated, and open certificate authority.

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
  • This uses certbot with the Nginx plugin
  • Follow the prompts to setup your certificate


If all goes well certbot will automatically add some lines to your Nginx server block. Double check the file again to make sure it has.

Now everything should be ready we just need to test and reload Nginx to update the configurations:

sudo nginx -t #test our config to make sure there is no error
sudo systemctl reload nginx #reloads nginx to update configs

You can now test to see if your site is available on at your domain.


Deploying with PostgreSQL (Optional)

If we need a database to handle large amounts of data and requests we can setup another service in docker to run an instance of a PostgreSQL database.

Update Docker-compose file:

version: '3'

services:
  web:
    build: .
    command: gunicorn --bind 0.0.0.0:8000 blogsite.wsgi:application
    volumes:
      - .:/blogsite
      - ./staticfiles:/app/staticfiles
    ports:
      - "8000:8000"
    environment:
      - DEBUG=0

    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_DB: django_db
      POSTGRES_USER: django_user
      POSTGRES_PASSWORD: django_password
    volumes:
      - postgres_data:/var/lib/postgresql/data/

volumes:
    postgres_data:

Update settings.py to be configured with PostgreSQL:

DATABASES = {

'default': {
    'ENGINE': 'django.db.backends.postgresql',
    'NAME': 'django_db',
    'USER': 'django_user',
    'PASSWORD': 'django_password',
    'HOST': 'db',
    'PORT': '5432',
    }
}


Analytics with PostHog (Optional)

It's always nice to have some analytics to see how many people our site and how they are interacting with it. PostHog offs a simple and free way to get some basic web analytics.

Go to the PostHog website and signup for a free account. From there choose the HTML snippet option and simply post the snippet into the head of your Django layout.html or base.html template that is being rendered on all your pages. If you aren't handling your templates that way you will have to post the snippet into the head of all your pages.

Wrapping Up

By now you should hopefully have an up and running Django app inside docker and being served by Nginx. Of course things don't always go to plan and you may encounter some trouble along the way. Although hopefully this post gave you the tools and steps to get there.

If you have any questions or problems along the way, drop a comment below or reach out to me through my contact page found at the bottom of the home page of this site.