Tilbake til blogg
August 19, 2026Guider

Slik kjører du Python-apper med Gunicorn og Nginx på en VPS

Deploy en Flask- eller FastAPI-applikasjon på Ubuntu med en virtualenv, Gunicorn systemd-tjeneste, Nginx reversproxy, TLS, miljøfiler, logging og en sjekkliste for 502-feil og worker-dimensjonering.

Slik kjører du Python-apper med Gunicorn og Nginx på en VPS

Den innebygde Flask-serveren og uvicorn --reload er for utvikling. På en offentlig VPS vil du ha en prosessbehandler som starter workers på nytt, binder til localhost og sitter bak en reversproxy som håndterer TLS og trege klienter. Gunicorn er det vanlige WSGI-valget for Flask og Django. FastAPI kan kjøre under Gunicorn med en Uvicorn-worker-klasse. Nginx (eller Caddy) avslutter HTTPS og videresender til 127.0.0.1:8000.

Denne veilederen går gjennom et oppsett som overlever omstarter: prosjekt i /srv/app, virtualenv, .env med hemmeligheter, en gunicorn.service-enhet, Nginx-serverblokk, Let's Encrypt og loggfiler du faktisk kan grep:e. Vi snakker også om worker-antall, tidsavbrudd og hvorfor 502 Bad Gateway nesten aldri er 'Nginx er ødelagt' — det er nesten alltid Gunicorn som ikke kjører, er bundet til feil socket, eller krasjer ved import. Eksempelet bruker Flask, med merknader for FastAPI der kommandoen skiller seg.

Hvorfor denne stakken

Gunicorn preforker worker-prosesser. Hver worker håndterer én forespørsel om gangen med mindre du bruker en annen worker-klasse. Nginx buffer trege klienter slik at workers ikke sitter fast med å sende byte til et mobilnett. systemd starter appen på nytt hvis den dør. Sammen er det kjedelig, som er det du vil ha klokken tre om natten.

  • Gunicorn: stabil WSGI/ASGI-prosessmodell
  • systemd: start ved oppstart, omstart ved krasj, journald-logger
  • Nginx: TLS, statiske filer, grenser for forespørselsstørrelse, gzip
  • venv: system-Python forblir rent
  • localhost-binding: appen nås bare gjennom Nginx

Krav

Python 3.10+ på Ubuntu 22.04/24.04 er greit. Ikke kjør pip som root inn i systemets site-packages. Du trenger et domene for TLS. Hvis du foretrekker Caddy som proxy, forblir Gunicorn-enheten i denne artikkelen den samme — bare frontend-configen endres (se Caddy-artikkelen).

  • Ubuntu 22.04- eller 24.04-VPS
  • Applikasjonen din med en requirements.txt eller tilsvarende
  • Et WSGI-inngangspunkt (for Flask: app:app) eller ASGI (for FastAPI: app:app med uvicorn-workers)
  • Domene-A-post for HTTPS

Steg 1: Systempakker, bruker og prosjektkatalog

Opprett en systembruker som ikke kan logge inn interaktivt, eie koden og kjøre Gunicorn. Å installere python3-venv og byggeverktøy unngår pip-feil på pakker som fortsatt kompilerer C-utvidelser.

bash
ssh root@YOUR_VPS_IP
apt update && apt -y upgrade
apt -y install python3 python3-venv python3-pip python3-dev build-essential nginx curl

adduser --system --group --home /srv/app appuser
mkdir -p /srv/app
chown appuser:appuser /srv/app

# Copy your code (example):
# rsync -a --delete ./myproject/ appuser@YOUR_VPS_IP:/srv/app/

Steg 2: Virtualenv og avhengigheter

Opprett venv som appuser slik at fileierskapet blir riktig. Lås versjoner i produksjon. Etter installasjon, bekreft at du kan importere appen i en engangs gunicorn --check-config eller en Python-import. Importfeil her er de samme feilene som senere blir 502.

bash
sudo -u appuser -H bash -lc '
cd /srv/app
python3 -m venv /srv/app/venv
/srv/app/venv/bin/pip install --upgrade pip
/srv/app/venv/bin/pip install -r /srv/app/requirements.txt gunicorn
'

# Flask example check:
sudo -u appuser -H /srv/app/venv/bin/python -c "from app import app; print('import ok')"

Steg 3: Miljøfil

Ikke hardkod SECRET_KEY eller database-URL-er i systemd-enheten på en måte som ender i verdenslesbar git. Bruk EnvironmentFile. chmod 640, eier root, gruppe appuser (eller eier appuser hvis du foretrekker det).

bash
cat >/srv/app/.env <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-openssl-rand-hex-32
DATABASE_URL=postgresql://app:password@127.0.0.1:5432/app
EOF
chown appuser:appuser /srv/app/.env
chmod 600 /srv/app/.env

Steg 4: Gunicorn systemd-tjeneste

Bind til 127.0.0.1:8000, ikke 0.0.0.0, med mindre du har en grunn til å hoppe over Nginx. Worker-antall er ofte (2 x CPU) + 1 for synk-workers; på en 2 vCPU-VPS er det 5, som kan være for mange hvis hver worker laster en tung ML-modell — bruk da 2–3. For FastAPI, sett --worker-class uvicorn.workers.UvicornWorker og installer uvicorn. Tidsavbrudd skal overstige den tregeste ærlige forespørselen din, ikke 30 sekunder hvis du har 2-minutters eksporter.

bash
cat >/etc/systemd/system/gunicorn.service <<'EOF'
[Unit]
Description=Gunicorn for the web app
After=network.target

[Service]
User=appuser
Group=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --timeout 60 --access-logfile - --error-logfile - app:app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now gunicorn
systemctl status gunicorn --no-pager
ss -tulpn | grep 8000

Steg 5: Nginx reversproxy og TLS

Nginx lytter på 80/443 og proxer til Gunicorn. client_max_body_size betyr noe for opplastinger. proxy_read_timeout skal matche eller overstige Gunicorns timeout. Når serverblokken virker på HTTP, utsted et sertifikat med Certbot (eller bytt fronten til Caddy). Snippetet under er bare HTTP slik at du kan teste; kjør deretter Certbot som kan redigere filen.

bash
cat >/etc/nginx/sites-available/app <<'EOF'
server {
    listen 80;
    server_name app.example.com;
    client_max_body_size 20m;

    location /static/ {
        alias /srv/app/static/;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
        proxy_read_timeout 90s;
    }
}
EOF

ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
nginx -t && systemctl reload nginx

ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable

apt -y install certbot python3-certbot-nginx
certbot --nginx -d app.example.com

Steg 6: Statiske filer, rettigheter og Flask/FastAPI-spesifikke ting

Nginx bør servere statiske filer hvis du kan; det er raskere enn Gunicorn. Django collectstatic, Flask send_from_directory for små apper, eller en CDN senere. appuser må kunne lese treet. Hvis du bruker FastAPI, installer uvicorn[standard] i venv og endre ExecStart til å bruke UvicornWorker. Hvis du bruker Unix-sockets i stedet for TCP, pek proxy_pass mot socketen og match rettigheter slik at www-data kan skrive til den.

bash
# FastAPI ExecStart example:
# ExecStart=/srv/app/venv/bin/gunicorn -k uvicorn.workers.UvicornWorker --workers 2 --bind 127.0.0.1:8000 app:app

# Unix socket variant:
# --bind unix:/run/gunicorn/app.sock
# Nginx: proxy_pass http://unix:/run/gunicorn/app.sock:

chown -R appuser:appuser /srv/app

Workers, minne og null-nedetid-omlastinger

Synk-Gunicorn-workers er enkle og nok for CPU-lette request/response-API-er. Hvis du trenger mange samtidige trege I/O-ventinger, vurder gevent eller en ASGI-worker — mål, ikke gjett. Hver worker laster appen din; RAM ~= workers ganger app-RSS. En 2 GB-VPS med 8 workers av en 300 MB-app vil swappe og føles 'tilfeldig treg'. systemctl reload gunicorn (HUP) kan starte workers på nytt med den nye koden hvis du deployet filer på plass; en full omstart er tydeligere når avhengigheter endres.

bash
# After git pull / rsync of new code:
sudo -u appuser -H /srv/app/venv/bin/pip install -r /srv/app/requirements.txt
systemctl restart gunicorn
curl -I https://app.example.com/health

Feilsøking av 502 og stille krasj

502 betyr at Nginx ikke fikk et gyldig svar fra upstream. journalctl -u gunicorn -e er den første kommandoen. Vanlige årsaker: feil module:app-navn, manglende .env-nøkkel, Postgres kjører ikke, bundet til 127.0.0.1 men Nginx på en annen vert, SELinux (sjeldent på Ubuntu), eller appen lytter bare på IPv6. 504 er et tidsavbrudd. 301-løkker skjer når appen omdirigerer til HTTP mens X-Forwarded-Proto ignoreres.

  • systemctl status gunicorn — er den aktiv?
  • journalctl -u gunicorn -n 100 — ImportError, manglende env, database
  • curl -v http://127.0.0.1:8000/ fra VPS-en — hvis dette feiler, er Nginx uskyldig
  • nginx -t og error.log — upstream prematurely closed connection
  • ss -tulpn | grep 8000 — ingenting lytter
  • Disken full — workers krasjer på mystiske måter

Sikkerhet

Appen binder aldri offentlig. Hemmeligheter blir i .env. Hold venv og OS patchat. Ikke kjør Gunicorn som root. Hvis du håndterer innlogginger, sett øktinformasjonskapsler til Secure og SameSite, og konfigurer rammeverket til å stole på X-Forwarded-Proto bare fra Nginx. Hastighetsbegrens innloggingsruter i Nginx eller i appen.

  • bind 127.0.0.1 eller en unix-socket
  • chmod 600 .env
  • Ikke-root User= i systemd
  • TLS via Certbot eller Caddy
  • Deaktiver debug-modus og auto-reload i produksjon

Tips

  • Legg til en /health-rute som sjekker DB-tilkobling for Compose og lastbalanserere
  • Send statiske ressurser med en cache-control-header i Nginx
  • Bruk en separat worker eller kø (Redis + systemd) for e-post og tunge jobber
  • Lås gunicorn- og uvicorn-versjoner i requirements.txt
  • Ta et øyeblikksbilde før den første produksjonsovergangen

En Python-app på en VPS er produksjonsklar når den kjører under Gunicorn som en systemd-tjeneste, binder bare til localhost, og nås gjennom Nginx eller Caddy med TLS. Legg hemmeligheter i en EnvironmentFile, dimensjoner workers etter RAM i stedet for en bloggformel, og feilsøk 502 fra Gunicorn-journalen først. Når denne stien er dokumentert for repoet ditt, er hver senere deploy rsync eller git pull, pip install og systemctl restart gunicorn.