Setting up your Django app for production might seem complex, but it doesn’t have to be. This step-by-step guide walks you through deploying a Django web application on Ubuntu, using Nginx as a reverse proxy, Gunicorn as the application server, and either MySQL or PostgreSQL as the database backend.
Whether you're a developer, DevOps engineer, or startup founder, this tutorial will walk you through each step with clarity and confidence.
Prerequisites
Before diving in, make sure you have the following:
- A cloud server (e.g., AWS EC2, DigitalOcean, Linode) running Ubuntu 22.04+
- A non-root user with
sudoprivileges - A Django project ready for deployment
- Domain name pointed to your server IP (optional but recommended)
- Basic understanding of Linux commands
Update the Server & Install Required Packages
Update the server:
sudo apt update && sudo apt upgrade -yInstall Python and essential tools:
sudo apt install python3-pip python3-dev build-essential libssl-dev libffi-dev python3-venv curl git -y| Package | Description |
|---|---|
| python3-pip | Installs pip, the package manager for Python. You’ll use this to install Django, Gunicorn, and other Python libraries. |
| python3-dev | Provides the header files needed to build Python extensions (e.g., mysqlclient). |
| build-essential | Includes GCC, make, and compilers/tools required for packages with native extensions. |
| libssl-dev | SSL development libraries for secure connections (HTTPS, cryptography, etc.). |
| libffi-dev | Foreign Function Interface library, used with cryptography and security modules. |
| python3-venv | Lets you create isolated Python environments using python3 -m venv. |
| curl | Command-line tool for making HTTP requests or downloading files. |
| git | Version control system used to clone your Django project and manage code changes. |
Create a Non-Root User (if you don't have one)
sudo adduser --disabled-password --gecos "" djangoSet Up the Project Directory
Navigate to the /opt/ folder (a good location for app deployments), create your project directory, and assign ownership to the django user:
cd /opt/
sudo mkdir myproject
sudo chown -R django:www-data /opt/myprojectAdd Your Project Files
Now you have two options depending on your setup:
Option 1 — Clone from GitHub (Recommended if you have a repo)
If your project is stored in GitHub or any Git repository:
sudo su - django
cd /opt/
git clone https://github.com/yourusername/yourrepo.git myprojectThen give appropriate permissions:
sudo chown -R django:www-data /opt/myprojectOption 2 — Upload Files via FTP or SCP
If you don’t have a Git repo, you can upload your project files manually (for example, using FileZilla, WinSCP, or scp).
- Connect to your server with your FTP/SFTP client.
- Upload your Django project files into:
/opt/myproject/ After uploading, fix the permissions:
sudo chown -R django:www-data /opt/myproject
Why Use /opt/ Instead of /var/www/ for a Django Project?
| Reason | Explanation |
|---|---|
| Cleaner separation | /opt/ is designed for third-party or optional software — ideal for app-specific projects. |
| Avoids permission issues | /var/www/ is usually owned by www-data, which can cause permission problems. |
| Less clutter | Keeps system files (/var) separate from your application files. |
| Easier to manage | You can safely use sudo chown $USER:$USER, and tools like Git, pip, or virtualenv work seamlessly. |
Now that your project files are in place and have proper permissions, switch to the django user to continue setup:
sudo su - django
cd /opt/myprojectCreate a Virtual Environment and Install Requirements
cd /opt/myproject
python3 -m venv venv
source venv/bin/activate
pip install django gunicornIf you have a requirements.txt file, use:
pip install -r requirements.txtSet Up the Database (MySQL or PostgreSQL)
Let’s set up the database according to your project requirements. You can choose between MySQL or PostgreSQL depending on your preference or existing infrastructure. Once the database is ready and connected, we’ll config Django to initialize it properly.
Option A — MySQL
Install and secure MySQL:
sudo apt install mysql-server -y
sudo mysql_secure_installationInstall OS dev libraries required for Python MySQL client:
sudo apt install pkg-config libmysqlclient-dev -yCreate database and user (MySQL shell):
sudo mysql
CREATE DATABASE myprojectdb CHARACTER SET UTF8;
CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON myprojectdb.* TO 'myuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;Django database settings for MySQL (add this in settings.py):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'myprojectdb',
'USER': 'myuser',
'PASSWORD': 'strongpassword',
'HOST': 'localhost',
'PORT': '3306',
}
}Install Python MySQL client in your virtualenv:
pip install mysqlclientOption B — PostgreSQL
Install PostgreSQL:
sudo apt install postgresql postgresql-contrib -yCreate database and user (run as postgres user):
sudo -u postgres psql
CREATE DATABASE myprojectdb;
CREATE USER myuser WITH PASSWORD 'strongpassword';
ALTER ROLE myuser SET client_encoding TO 'utf8';
ALTER ROLE myuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE myuser SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE myprojectdb TO myuser;
\qInstall OS dev libraries required for Python Postgres client and the Python package:
sudo apt install libpq-dev -y
pip install psycopg2-binaryDjango database settings for PostgreSQL (add this in settings.py):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myprojectdb',
'USER': 'myuser',
'PASSWORD': 'strongpassword',
'HOST': 'localhost',
'PORT': '5432',
}
}Notes:
- Use strong, unique passwords in production and consider configuring the DB host, SSL, and restricted access for better security.
- If your DB is on a separate host, set
HOSTaccordingly and ensure the server firewall allows DB connections (and use private networking where possible).
Configure Django Settings for Production
- Set
DEBUG = False - Add your domain to
ALLOWED_HOSTS:
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']Configure Static Folder:
# Static files
STATIC_URL = 'static/'
# Where collectstatic will PUT the compiled/copied files
STATIC_ROOT = BASE_DIR / 'static_collected' # pick any folder name you prefer and use same name in nginxCollect static files:
python3 manage.py collectstaticRun migrations:
python3 manage.py migrateCreate superuser (optional):
python3 manage.py createsuperuserUse Environment Variables (.env) for Sensitive Settings
In production, never hardcode sensitive information such as database credentials, secret keys, or API tokens directly inside settings.py. These values should be stored securely in an environment file so they aren’t exposed or accidentally pushed to Git.
We’ll use python-decouple for this — it’s lightweight, simple, and integrates perfectly with Django.
1. Install python-decouple
pip install python-decouple2. Create a .env File
In your project’s root directory, create a file named .env and add your sensitive configuration values:
DB_NAME=myprojectdb
DB_USER=myuser
DB_PASSWORD=strongpassword
DB_HOST=localhost
DB_PORT=5432
SECRET_KEY=mysecretkey
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
⚠️ Important: The .env file contains confidential data and must never be committed to your Git repository.
3. Add .env to .gitignore
To ensure it’s never pushed to version control, add. env to your .gitignore file:
4. Update settings.py
Import Config from decouple and replace hardcoded values with references from the .env file:
from decouple import Config, Csv
config = Config()
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT', default='5432'),
}
}
SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())
With this setup, your Django app securely loads sensitive settings from the .env file instead of storing them in the codebase — keeping your secrets safe and your Git repo clean.
Run Gunicorn as the Application Server
Test Gunicorn manually:
gunicorn --bind 0.0.0.0:8000 myproject.wsgi:applicationCreate a systemd service for Gunicorn:
sudo nano /etc/systemd/system/gunicorn.servicePaste:
[Unit]
Description=gunicorn daemon
After=network.target
[Service]
User=django
Group=www-data
WorkingDirectory=/opt/myproject
ExecStart=/opt/myproject/venv/bin/gunicorn --workers 3 --bind unix:/opt/myproject/myproject.sock myproject.wsgi:application
[Install]
WantedBy=multi-user.targetEnable and start Gunicorn:
sudo systemctl daemon-reexec
sudo systemctl enable gunicorn
sudo systemctl start gunicornConfigure Nginx as a Reverse Proxy
sudo apt install nginx -yCreate new config:
sudo nano /etc/nginx/sites-available/myprojectPaste:
server {
listen 80;
server_name example.com www.example.com;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
alias /opt/myproject/static_collected/;
}
location / {
include proxy_params;
proxy_pass http://unix:/opt/myproject/myproject.sock;
}
}Enable the site and restart Nginx:
sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled
sudo nginx -t
sudo systemctl restart nginxAllow traffic on port 80:
sudo ufw allow 'Nginx Full'Secure with HTTPS (Optional but Recommended)
Use Let’s Encrypt:
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.comSet up auto-renewal:
sudo certbot renew --dry-runUpdate Nginx Config to use HTTPS:
# Redirect all HTTP -> HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# Strong SSL settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location = /favicon.ico { access_log off; log_not_found off; }
location /static/ {
alias /opt/myproject/static_collected/;
}
location / {
include proxy_params;
proxy_pass http://unix:/opt/myproject/myproject.sock;
}
}
Final Checklist
- Gunicorn is running as a service
- Nginx is correctly serving the Django app
- Static files are collected
- MySQL is connected and working
- HTTPS is enabled (optional)
- If Gunicorn shows a 500 error, inspect the logs:
sudo journalctl -u gunicorn -e
Leave a comment
Your email address will not be published. Required fields are marked *
