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.
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 ApplicationThe 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 gunicornStep 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 nginxStep 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 ErrorsInitially 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:
Updating it to:
server_name ericmuheto.com www.ericmuheto.com;
resolved the problem.
Cloudflare DNS Typo
One of my DNS records accidentally pointed to:
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.