Applies to: LumaDock VPS • Ubuntu 22.04 LTS (Jammy)
This step-by-step guide shows how to deploy a Django application on a fresh LumaDock VPS. You’ll set up Python, a virtual environment, Gunicorn (WSGI), and Nginx as a reverse proxy—with optional PostgreSQL and free HTTPS using Let’s Encrypt.
Requirements
- Fresh LumaDock VPS running Ubuntu 22.04 LTS (IPv4/IPv6 as preferred)
- Root SSH access (or a sudo-enabled user)
- A domain pointed to your VPS public IP (recommended for HTTPS)
1) Connect & update
ssh root@YOUR_SERVER_IP
apt update && apt -y upgrade
Tip: Create a sudo user instead of using root directly: adduser admin && usermod -aG sudo admin
, then ssh admin@YOUR_SERVER_IP
.
2) Install Python & build tools
apt install -y python3 python3-venv python3-pip python3-dev build-essential nginx git
3) Create a Django project & virtual environment
- Create a project directory (example:
/opt/djangoapp
):mkdir -p /opt/djangoapp && cd /opt/djangoapp
- Create & activate a virtualenv:
python3 -m venv venv source venv/bin/activate
- Install Django & Gunicorn:
pip install --upgrade pip pip install django gunicorn
- Create a new Django project (named
myproject
):django-admin startproject myproject .
- Allow your server host/domain in Django:
# edit settings.py nano myproject/settings.py # set: # ALLOWED_HOSTS = ["YOUR_DOMAIN_OR_IP"]
4) Run with Gunicorn (test locally)
# from /opt/djangoapp with venv active
python manage.py migrate
python manage.py collectstatic --noinput
gunicorn --bind 127.0.0.1:8000 myproject.wsgi
Keep this running in one terminal (for a quick test). In another terminal/shell: curl -I http://127.0.0.1:8000
should return 200 OK
.
5) Set up Nginx reverse proxy
- Create an Nginx server block (replace domain):
nano /etc/nginx/sites-available/djangoapp
server { listen 80; server_name YOUR_DOMAIN_OR_IP; # Max upload size (adjust as needed) client_max_body_size 20M; location /static/ { alias /opt/djangoapp/static/; } 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 and test config:
ln -s /etc/nginx/sites-available/djangoapp /etc/nginx/sites-enabled/ nginx -t && systemctl restart nginx
- Open your site:
http://YOUR_DOMAIN_OR_IP
. You should see the Django welcome page.
STATIC_URL
and STATIC_ROOT
are set (see step 9), then run collectstatic
.6) Create a systemd service for Gunicorn
This keeps your app running in the background and restarts it on boot.
nano /etc/systemd/system/gunicorn-djangoapp.service
[Unit]
Description=Gunicorn for Django (djangoapp)
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/djangoapp
Environment="PATH=/opt/djangoapp/venv/bin"
ExecStart=/opt/djangoapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myproject.wsgi:application
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now gunicorn-djangoapp
systemctl status gunicorn-djangoapp
7) (Optional) Enable HTTPS with Let’s Encrypt
- Install Certbot:
apt install -y certbot python3-certbot-nginx
- Issue a certificate (replace domain and email):
certbot --nginx -d YOUR_DOMAIN -m you@example.com --agree-tos --redirect
- Verify auto-renew (a systemd timer is installed by default):
systemctl list-timers | grep certbot
8) (Optional) Use PostgreSQL instead of SQLite
- Install PostgreSQL & dev headers:
apt install -y postgresql postgresql-contrib libpq-dev
- Create DB & user:
sudo -u postgres psql CREATE DATABASE mydb; CREATE USER myuser WITH PASSWORD 'StrongPassword123!'; GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser; \q
- Install driver & configure Django:
source /opt/djangoapp/venv/bin/activate pip install psycopg2-binary
# myproject/settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "mydb", "USER": "myuser", "PASSWORD": "StrongPassword123!", "HOST": "127.0.0.1", "PORT": "5432", } }
- Migrate & reload:
python manage.py migrate systemctl restart gunicorn-djangoapp
If PostgreSQL is remote, restrict firewall to your app IP and use SSL as needed.
9) Configure & collect static files
# myproject/settings.py
STATIC_URL = "static/"
STATIC_ROOT = "/opt/djangoapp/static"
python manage.py collectstatic --noinput
systemctl restart gunicorn-djangoapp
systemctl restart nginx
The Nginx block maps /static/
to /opt/djangoapp/static/
.
Troubleshooting
Symptom | Likely cause | Fix |
---|---|---|
Bad Request (400) from Django | ALLOWED_HOSTS missing your domain/IP |
Add your domain/IP to ALLOWED_HOSTS in settings.py , restart Gunicorn. |
403/404 on static files | Static not collected or Nginx path mismatch | Set STATIC_ROOT , run collectstatic , verify Nginx location /static/ alias. |
502 Bad Gateway | Gunicorn not running / wrong socket/port | Check systemctl status gunicorn-djangoapp , logs, ensure proxy_pass matches Gunicorn bind. |
Connection refused on port 80/443 | Nginx down or firewall blocking | systemctl status nginx , open ports in cloud/OS firewall, ensure DNS points to VPS IP. |
Appendix: Useful commands
Service & logs
journalctl -u gunicorn-djangoapp -e
journalctl -u nginx -e
systemctl restart gunicorn-djangoapp nginx
Open ports check
ss -tulpen | egrep ':80|:443|:8000'
curl -I http://127.0.0.1:8000
curl -I http://YOUR_DOMAIN_OR_IP