Skip to main content

Django – deploy an existing project on a LumaDock VPS

Upload or clone your Django project and run it with Gunicorn and Nginx on Ubuntu 22.04 with optional HTTPS and PostgreSQL.

A
Written by Alexandru Stan
Updated over a month ago

This guide explains how to deploy an existing Django project on a LumaDock VPS using Ubuntu 22.04 LTS. You will upload your code, install dependencies in a virtual environment, configure environment variables, and run Django with Gunicorn behind Nginx. Optional sections cover HTTPS and PostgreSQL.

Firewall note: if you enable the cloud firewall, allow inbound 22/tcp, 80/tcp, and 443/tcp.


Requirements

A complete deployment requires:

  • A LumaDock VPS running Ubuntu 22.04 LTS

  • SSH access as root or a sudo-enabled user

  • A Django project containing manage.py and requirements.txt

  • A domain pointed to your VPS (recommended for HTTPS)


1) Prepare the server

Connect:

ssh root@YOUR_SERVER_IP

Update the system:

apt update && apt -y upgrade

Install required packages:

apt install -y python3 python3-venv python3-pip python3-dev build-essential nginx git

Optional: create a non-root user:

adduser admin usermod -aG sudo admin ssh admin@YOUR_SERVER_IP


2) Upload or clone your project

Create a directory:

mkdir -p /opt/djangoapp

cd /opt/djangoapp

You can now load your project into this folder:

  • Clone from Git

  • Upload your local folder via SFTP or rsync

Your manage.py file should end up at:

/opt/djangoapp/manage.py


3) Create a virtualenv and install dependencies

Set up your environment:

cd /opt/djangoapp \
python3 -m venv venv \
source venv/bin/activate \
pip install --upgrade pip \
pip install -r requirements.txt

If your project uses Gunicorn or another WSGI/ASGI server, make sure it is listed in requirements.txt.


4) Configure environment variables and Django settings

For production you should set:

  • ALLOWED_HOSTS

  • DEBUG=False

  • A secure SECRET_KEY stored outside Git

  • Database configuration if not using SQLite

You can store values in an .env file or load them via django-environ.

Example /opt/djangoapp/.env:

DJANGO_DEBUG=False \
DJANGO_SECRET_KEY=change_me_to_a_strong_secret \
DJANGO_ALLOWED_HOSTS=yourdomain.com,YOUR_SERVER_IP

Update your settings.py to load the environment file or rely on environment variables that will be passed in the systemd unit.


5) Database and migrations

If using SQLite you can skip installation. For PostgreSQL or MySQL, install and configure those first.

Run migrations:

cd /opt/djangoapp \
source venv/bin/activate \
python manage.py migrate

Create a superuser if needed:

python manage.py createsuperuser


6) Static and media files

Example settings:

STATIC_URL = "static/"
STATIC_ROOT = "/opt/djangoapp/static"

MEDIA_URL = "media/"
MEDIA_ROOT = "/opt/djangoapp/media"

Collect files:

python manage.py collectstatic --noinput

These paths will be served directly by Nginx.


7) Run with Gunicorn (systemd service)

Create a systemd unit:

nano /etc/systemd/system/gunicorn-djangoapp.service

Insert:

[Unit]
Description=Gunicorn for Django (djangoapp)
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/djangoapp
Environment="DJANGO_SETTINGS_MODULE=yourproject.settings"
Environment="DJANGO_SECRET_KEY=change_me"
Environment="DJANGO_DEBUG=False"
Environment="DJANGO_ALLOWED_HOSTS=yourdomain.com,YOUR_SERVER_IP"
Environment="PATH=/opt/djangoapp/venv/bin"
ExecStart=/opt/djangoapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 yourproject.wsgi:application
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start:

systemctl daemon-reload \
systemctl enable --now gunicorn-djangoapp \
systemctl status gunicorn-djangoapp


8) Nginx reverse proxy

Create an Nginx server block:

nano /etc/nginx/sites-available/djangoapp

Paste:

server {
listen 80;
server_name YOUR_DOMAIN_OR_IP;

client_max_body_size 20M;

location /static/ {
alias /opt/djangoapp/static/;
}

location /media/ {
alias /opt/djangoapp/media/;
}

location / {
proxy_pass http://127.0.0.1:8000;

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

Enable it:

ln -s /etc/nginx/sites-available/djangoapp /etc/nginx/sites-enabled/ \
nginx -t && \
systemctl restart nginx

Visit:

http://YOUR_DOMAIN_OR_IP


9) Optional — HTTPS with Let’s Encrypt

Install Certbot:

apt install -y certbot python3-certbot-nginx

Issue a certificate:

certbot --nginx -d YOUR_DOMAIN -m you@example.com --agree-tos --redirect

Check auto-renew:

systemctl list-timers | grep certbot


10) Updating your app

To update your deployed application:

  • Pull new code or upload a new release

  • Reinstall updated dependencies

  • Apply migrations and collect static files

  • Reload Nginx and restart Gunicorn

Example:

cd /opt/djangoapp \
git pull \
source venv/bin/activate \
pip install -r requirements.txt \
python manage.py migrate \
python manage.py collectstatic --noinput \
systemctl reload nginx \
systemctl restart gunicorn-djangoapp​

Troubleshooting

Common issues include:

  • 400 Bad Request: add your domain/IP to ALLOWED_HOSTS

  • Static files missing: verify STATIC_ROOT and run collectstatic

  • 502 Bad Gateway: Gunicorn not running or wrong proxy_pass target

  • Permission errors: adjust ownership for static and media directories

  • Let’s Encrypt failure: domain not pointing to VPS or port 80 blocked


Appendix: useful commands and permissions

Logs and restarts:

journalctl -u gunicorn-djangoapp -e \
journalctl -u nginx -e \
systemctl restart gunicorn-djangoapp nginx

Permissions:

chown -R www-data:www-data /opt/djangoapp \
find /opt/djangoapp -type d -exec chmod 755 {} \; \
find /opt/djangoapp -type f -exec chmod 644 {} \; \
chmod +x /opt/djangoapp/manage.py

Your existing Django project is now deployed behind Gunicorn and Nginx on your LumaDock VPS. You can continue iterating on your code, automate deployments, or add monitoring as needed.

Did this answer your question?