Deploying a Django Application on an Ubuntu VPS with Nginx, Gunicorn, and MySQL
A practical guide to setting up an Ubuntu server, configuring non-root deploy access, connecting MySQL, and running Django securely behind Gunicorn and Nginx.
Deploying a Django web application to a bare virtual private server gives you complete architectural control, predictable performance, and freedom from platform lock-in. In this guide, we walk through the exact foundation required to launch a production-grade Django application on an Ubuntu server using Gunicorn as the WSGI gateway, Nginx as the reverse proxy, and MySQL for persistence.
1. Initial Server Setup and Deploy User
Never run a public web application as the root user. First, connect to your server and create a dedicated deploy user with administrative permissions:
adduser deploy
usermod -aG sudo deploy
su - deploySet up an SSH key pair so your server can pull repository files securely without prompting for passwords:
ssh-keygen -t ed25519 -C "deploy@server"
cat ~/.ssh/id_ed25519.pubAdd the printed public key to your Git hosting platform under deploy keys with read-only access.
2. Installing Core System Packages
Update system packages and install the Python build chain, Nginx, MySQL, and essential development headers:
sudo apt update && sudo apt upgrade -y
sudo apt install python3-pip python3-venv python3-dev build-essential libssl-dev libffi-dev pkg-config -y
sudo apt install mysql-server libmysqlclient-dev nginx git ufw -y3. Database Provisioning
Log into MySQL to initialize your project database, create an application user, and grant scoped privileges. Always substitute placeholder names and generate strong passwords:
sudo mysqlCREATE DATABASE app_production_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_db_user'@'localhost' IDENTIFIED BY 'StrongRandomSecret987!';
GRANT ALL PRIVILEGES ON app_production_db.* TO 'app_db_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;4. Cloning the Project and Setting Up the Environment
Create a dedicated folder for your projects, clone your repository, and set up an isolated Python virtual environment:
mkdir -p ~/Projects && cd ~/Projects
git clone git@github.com:your-organization/your-django-repo.git myapp.com
cd myapp.com
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
pip install gunicorn mysqlclientCreate a production .env file. When setting special characters such as the @ symbol in passwords within database URLs, remember to URL-encode them (for example, @ encodes to %40):
nano .envDEBUG=False
SECRET_KEY=your-long-generated-secret-key-here
ALLOWED_HOSTS=203.0.113.195,example.com,www.example.com
DATABASE_URL=mysql://app_db_user:StrongRandomSecret987!@localhost:3306/app_production_dbApply your database migrations and collect your static assets:
python manage.py migrate
python manage.py collectstatic --no-input
python manage.py createsuperuser5. Configuring Gunicorn with Systemd
Systemd ensures Gunicorn starts automatically on system boot and restarts if an error occurs. Create a service file:
sudo nano /etc/systemd/system/gunicorn.serviceInsert the following configuration, adjusting project folder paths to match your layout:
[Unit]
Description=Gunicorn daemon for Django Application
After=network.target
[Service]
User=deploy
Group=www-data
WorkingDirectory=/home/deploy/Projects/myapp.com
RuntimeDirectory=gunicorn
ExecStart=/home/deploy/Projects/myapp.com/venv/bin/gunicorn \
--access-logfile - \
--workers 3 \
--bind unix:/run/gunicorn/gunicorn.sock \
config.wsgi:application
[Install]
WantedBy=multi-user.targetEnable and start the service:
sudo systemctl daemon-reload
sudo systemctl start gunicorn
sudo systemctl enable gunicorn
sudo systemctl status gunicorn6. Reverse Proxy Setup with Nginx
Nginx acts as a high-speed buffer, handling client SSL handshakes, caching, and serving static files directly from disk while forwarding dynamic requests to Gunicorn via the Unix socket.
sudo nano /etc/nginx/sites-available/myappserver {
listen 80;
server_name 203.0.113.195 example.com www.example.com;
client_max_body_size 25M;
location = /favicon.ico {
access_log off;
log_not_found off;
}
location /static/ {
alias /home/deploy/Projects/myapp.com/staticfiles/;
}
location /media/ {
alias /home/deploy/Projects/myapp.com/media/;
}
location / {
include proxy_params;
proxy_pass http://unix:/run/gunicorn/gunicorn.sock;
}
}Fix traversal permissions so the www-data user can read static files stored within the home directory:
sudo chmod 755 /home/deploy
sudo chmod 755 /home/deploy/Projects
sudo chmod 755 /home/deploy/Projects/myapp.com
sudo chmod -R 755 /home/deploy/Projects/myapp.com/staticfilesActivate the site, test the Nginx configuration, and reload:
sudo ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx7. Firewall Hardening
Protect non-essential ports using Ubuntu's Uncomplicated Firewall (UFW):
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw --force enableYour Django web application is now active, isolated, and serving traffic through an optimized production web stack.