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

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

FlagRecommendation
--workers(2 x cores) + 1 for sync workloads
--worker-classgevent or uvicorn.workers.UvicornWorker for async/ASGI
--timeout30s default; raise for long reports
--max-requests1000 with jitter, recycles workers to free RAM
--preloadLoads 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.