A Production-Ready Python Stack
Django and Flask ship with development servers that are perfect for local work and dangerous in production. A real deployment separates concerns: Gunicorn runs the WSGI app, systemd keeps it alive, and Nginx terminates TLS and serves static files. This guide walks through that stack on Ubuntu 22.04 LTS and Ubuntu 24.04 LTS.
Prerequisites
- A hardened Ubuntu VPS (see our hardening checklist).
- A domain pointing at the VPS.
- Nginx installed and configured (see Nginx reverse proxy).
Step 1: Install Python and Build Tools
apt update
apt install -y python3 python3-venv python3-pip python3-dev \
build-essential libpq-dev git
Ubuntu 22.04 ships Python 3.10; 24.04 ships 3.12. Both work with modern Django 4.2+ and Flask 3.x.
Step 2: Create a Dedicated User and Project Directory
adduser --system --group --home /opt/myapp myapp
mkdir -p /opt/myapp/src
chown -R myapp:myapp /opt/myapp
Running as a dedicated system user with no login shell limits blast radius if the app is ever compromised.
Step 3: Clone the App and Create a Virtualenv
sudo -u myapp -H bash
cd /opt/myapp
git clone https://github.com/your-org/myapp.git src
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r src/requirements.txt gunicorn
exit
Step 4: Test Gunicorn Manually
For a Django app (myapp.wsgi):
cd /opt/myapp/src
../venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 myapp.wsgi:application
For Flask (wsgi.py exporting app):
../venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 wsgi:app
Verify with curl http://127.0.0.1:8000/. A good worker-count starting point is (2 x CPU cores) + 1.
Step 5: Create a systemd Unit
Save as /etc/systemd/system/myapp.service:
[Unit]
Description=MyApp Gunicorn Service
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp/src
Environment="PATH=/opt/myapp/venv/bin"
EnvironmentFile=/opt/myapp/env
ExecStart=/opt/myapp/venv/bin/gunicorn \
--workers 3 \
--bind unix:/run/myapp.sock \
--access-logfile - \
--error-logfile - \
myapp.wsgi:application
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Store secrets in /opt/myapp/env with mode 600, one KEY=value per line. Enable and start:
systemctl daemon-reload
systemctl enable --now myapp
systemctl status myapp
Step 6: Configure Nginx
Create /etc/nginx/sites-available/myapp:
upstream myapp {
server unix:/run/myapp.sock fail_timeout=0;
}
server {
listen 80;
server_name myapp.example.com;
client_max_body_size 25M;
location /static/ {
alias /opt/myapp/src/staticfiles/;
expires 30d;
access_log off;
}
location /media/ {
alias /opt/myapp/src/media/;
}
location / {
proxy_pass http://myapp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enable the site and reload:
ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Step 7: Collect Static Files and Migrate
For Django:
sudo -u myapp /opt/myapp/venv/bin/python /opt/myapp/src/manage.py migrate
sudo -u myapp /opt/myapp/venv/bin/python /opt/myapp/src/manage.py collectstatic --noinput
Step 8: Add TLS with Let's Encrypt
apt install -y certbot python3-certbot-nginx
certbot --nginx -d myapp.example.com --redirect
See our Let's Encrypt guide for renewal and troubleshooting.
Step 9: Gunicorn Tuning
| Flag | Recommendation |
|---|---|
--workers | (2 x cores) + 1 for sync workloads |
--worker-class | gevent or uvicorn.workers.UvicornWorker for async/ASGI |
--timeout | 30s default; raise for long reports |
--max-requests | 1000 with jitter, recycles workers to free RAM |
--preload | Loads app once, forks workers - saves memory |
Step 10: Zero-Downtime Deploys
Gunicorn reloads without dropping connections via SIGHUP:
systemctl reload myapp
# or
kill -HUP $(cat /run/myapp.pid)
For code updates, pull the new commit, install dependencies, run migrations, then reload. A simple deploy script:
#!/bin/bash
set -e
cd /opt/myapp/src
sudo -u myapp git pull
sudo -u myapp /opt/myapp/venv/bin/pip install -r requirements.txt
sudo -u myapp /opt/myapp/venv/bin/python manage.py migrate --noinput
sudo -u myapp /opt/myapp/venv/bin/python manage.py collectstatic --noinput
systemctl reload myapp
Logging and Observability
systemd captures stdout/stderr into the journal. View live logs with:
journalctl -u myapp -f
For structured logging, configure Python's logging module to write JSON and ship via vector or promtail to a central store.
Running production Ubuntu servers? MassiveGRID's Cloud VPS provides NVMe storage and burstable CPU ideal for Python/WSGI workloads. For scaling Django or Flask with managed databases and load balancers, explore our Managed Cloud Servers or contact our team.
Published by MassiveGRID - cloud hosting with managed Python deployments and 24/7 operations.