Deploying a Django Application on DigitalOcean with Nginx, Gunicorn, and Cloudflare

Published on August 7, 2026

After building my personal portfolio with Django, I wanted to deploy it on a low-cost DigitalOcean Droplet and make it accessible through my own domain name using Cloudflare. Although the overall architecture is straightforward, I encountered several issues involving Gunicorn, Django configuration, Nginx, DNS records, and SSL certificates.
This article documents the complete deployment process and the lessons learned along the way.

Architecture


The final architecture looks like this:
Internet
    │
    ▼
Cloudflare DNS
    │
    ▼
DigitalOcean Reserved IP
    │
    ▼
Nginx
    │
    ▼
Gunicorn (Unix Socket)
    │
    ▼
Django Application

The server was hosted on:

  • Ubuntu 24.04 LTS
  • DigitalOcean Basic Droplet (1 vCPU, 1 GB RAM)
  • Django
  • Gunicorn
  • Nginx
  • Cloudflare
  • Let's Encrypt (Certbot)

Step 1: Provision the DigitalOcean Droplet


Create a basic Ubuntu Droplet and connect via SSH.

ssh root@your_server_ip

Create a regular user:

adduser youruser
usermod -aG sudo youruser

Login as the new user.

Step 2: Install Required Packages

sudo apt update
sudo apt install \
python3 \
python3-pip \
python3-venv \
nginx \
git \
certbot \
python3-certbot-nginx -y

Step 3: Clone the Django Project

git clone https://github.com/yourusername/yourproject.git
cd yourproject
Create a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
Install dependencies:
pip install -r requirements.txt

Step 4: Configure Environment Variables


Never hardcode secrets.
#Example .env:

DEBUG=False

SECRET_KEY=your-secret-key

ALLOWED_HOSTS=ericmuheto.com,www.ericmuheto.com,localhost,127.0.0.1

Load the variables:

from dotenv import load_dotenv

load_dotenv()
SECRET_KEY = os.environ["SECRET_KEY"]

DEBUG = os.getenv("DEBUG", "False").lower() == "true"

ALLOWED_HOSTS = [
    host.strip()
    for host in os.getenv("ALLOWED_HOSTS", "").split(",")
]

Step 5: Collect Static Files

python manage.py collectstatic

Step 6: Test Gunicorn

gunicorn \
--bind 0.0.0.0:8000 \
personal_portfolio.wsgi:application
If everything works, stop Gunicorn.

Step 7: Create a Systemd Service


Create:
/etc/systemd/system/gunicorn.service
Example:
[Unit]
Description=Gunicorn daemon
After=network.target

[Service]
User=youruser
Group=www-data

WorkingDirectory=/home/youruser/project

Environment="PATH=/home/youruser/project/.venv/bin"

RuntimeDirectory=gunicorn

ExecStart=/home/youruser/project/.venv/bin/gunicorn \
    --workers 2 \
    --bind unix:/run/gunicorn/gunicorn.sock \
    personal_portfolio.wsgi:application

Restart=always

[Install]
WantedBy=multi-user.target

#Enable:

sudo systemctl daemon-reload

sudo systemctl enable gunicorn

sudo systemctl start gunicorn

Step 8: Configure Nginx


Create a site configuration:
server {

    listen 80;

    server_name ericmuheto.com www.ericmuheto.com;

    location / {

        include proxy_params;

        proxy_pass http://unix:/run/gunicorn/gunicorn.sock;

    }

    location /static/ {

        alias /home/youruser/project/staticfiles/;

    }

}

#Enable the site:

sudo ln -s /etc/nginx/sites-available/mysite \
/etc/nginx/sites-enabled/

#Test:

sudo nginx -t

#Reload:

sudo systemctl reload nginx

Step 9: Configure Cloudflare DNS


Create:

A
@
Reserved IP

Create:

CNAME
www
ericmuheto.com
Initially, keep both records as DNS only (grey cloud).

Step 10: Obtain an SSL Certificate

sudo certbot --nginx \
-d ericmuheto.com \
-d www.ericmuheto.com

After the certificate is installed, enable automatic HTTP → HTTPS redirection.

Step 11: Enable Cloudflare Proxy


After HTTPS is working:

Enable the orange cloud for both DNS records.
Set Cloudflare SSL mode to Full (strict).
This provides:

HTTPS
DDoS protection
CDN
WAF
IP masking
Problems I Encountered
Missing SECRET_KEY
Django refused to start because the SECRET_KEY environment variable was not defined.

Solution:


Store it in a .env file and load it using python-dotenv.

Incorrect ALLOWED_HOSTS

Django returned:

400 Bad Request

Solution:

Add the domain and required hosts:

ALLOWED_HOSTS = [
    "ericmuheto.com",
    "www.ericmuheto.com",
    "localhost",
    "127.0.0.1"
]
Gunicorn Socket Errors

Initially Gunicorn failed to create its Unix socket because of an incorrect runtime directory.

Using:

/run/gunicorn/gunicorn.sock

with a matching RuntimeDirectory=gunicorn resolved the issue.

Nginx Still Used the Old IP

My Nginx configuration still contained:

server_name 134.xxx.xxx.xxx;
This prevented Certbot from finding the correct virtual host.

Updating it to:

server_name ericmuheto.com www.ericmuheto.com;
resolved the problem.

Cloudflare DNS Typo

One of my DNS records accidentally pointed to:

www.ericmuheto.con

instead of:

ericmuheto.com

This caused Let's Encrypt validation to fail with an NXDOMAIN error.

Always verify DNS records carefully before requesting certificates.

Lessons Learned


Deploying Django is not just about writing Python code. A production deployment requires knowledge of Linux system administration, networking, web servers, process management, DNS, reverse proxies, SSL certificates, and cloud infrastructure.
The most valuable lesson from this deployment was learning to troubleshoot systematically. Rather than changing multiple settings at once, verify each layer independently:

Django application
Gunicorn
Unix socket
Nginx
Local HTTP requests
DNS resolution
SSL certificate issuance
Cloudflare proxy

This layered approach makes diagnosing production issues significantly easier.

Final Thoughts


A 1 GB DigitalOcean Droplet is more than sufficient for a personal Django portfolio. Combined with Cloudflare, it provides a professional, secure, and cost-effective hosting solution suitable for showcasing projects, technical articles, and professional experience.

This deployment strengthened my understanding of production-grade Linux infrastructure and reinforced the importance of automation, observability, and disciplined troubleshooting—skills that are essential for modern Platform Engineering and DevOps roles.

Comments

No comments yet. Be the first to comment.

Leave a comment