Deploya Django: checklista för säker produktionssättning

Uppdaterad och kontrollerad: · Av Redaktionen, Django Hosting Sverige

Den här checklistan beskriver django deployment på en egen Linuxserver eller managed VPS: Gunicorn eller Uvicorn bakom Nginx, styrt av systemd, med hemligheter utanför Git och en plan för att backa.

Kort svar: Stäng av DEBUG, läs hemligheter från miljövariabler, kör Gunicorn (WSGI) eller Uvicorn-workers (ASGI) via systemd bakom Nginx med TLS, ta backup och kör migrate och collectstatic före trafikbytet, verifiera med en hälsokontroll och behåll föregående release för snabb rollback.

Exemplen använder platshållare som example.com och minapp, inga riktiga hemligheter och inga påhittade prestandasiffror. Byt ut dem mot dina egna värden.

Förbered projektet

Innan servern rörs ska projektet vara deploybart: låsta beroenden, en produktionskonfiguration och en hälsokontroll. Här är hela flödet i tolv steg; resten av sidan fördjupar dem.

  1. Lås beroendena. Fäst versioner i requirements.txt eller en lockfil och använd en Python-version som din Django-version stöder. Samma filer ska användas lokalt, i CI och på servern.
  2. Kör testerna och deploychecken. Kör testsviten och python manage.py check --deploy mot produktionsinställningarna. Åtgärda varningar om DEBUG, HSTS, säkra cookies och ALLOWED_HOSTS innan du går vidare.
  3. Skapa en dedikerad användare. Appen ska köras som en egen systemanvändare utan sudo-rättigheter, aldrig som root. Ge användaren läsrätt till miljöfilen och skrivrätt bara till media och loggar.
  4. Lägg hemligheter i miljöfilen. SECRET_KEY, databaslösenord och API-nycklar hör hemma i en skyddad miljöfil eller plattformens hemlighetshantering. Lägg in .env i .gitignore från första commit.
  5. Förbered PostgreSQL. Skapa databas och användare med minsta nödvändiga rättigheter. Kontrollera att anslutning från appservern fungerar och att automatisk backup är aktiverad innan första migreringen.
  6. Checka ut releasen i egen katalog. Varje release får en katalog med tidsstämpel under /srv/minapp/releases och en egen virtuell miljö. Då kan föregående version startas igen på sekunder.
  7. Ta backup och kör migreringar. Gör en databasdump och kör sedan migrate innan ny kod tar trafik. Migreringarna ska vara bakåtkompatibla så att gammal kod fungerar under bytet.
  8. Samla statiska filer. Kör collectstatic till en delad katalog som Nginx serverar. Versionerade filnamn, till exempel via ManifestStaticFilesStorage, gör att webbläsare inte visar gammal CSS efter releasen.
  9. Konfigurera Gunicorn eller Uvicorn med systemd. Använd socketaktivering och en service-fil som läser miljöfilen, startar om vid krasch och loggar till journalen. Välj ASGI-worker bara om appen behöver async.
  10. Sätt upp Nginx och TLS. Nginx terminerar HTTPS, omdirigerar HTTP och skickar vidare X-Forwarded-Proto. Certifikat från Let’s Encrypt ska förnyas automatiskt med en timer.
  11. Byt trafik och verifiera. Peka symlänken current mot nya releasen, starta om Gunicorn och anropa hälsokontrollen, en enkel vy på /healthz/ som svarar 200. Kontrollera loggar och felrapporter de första minuterna.
  12. Dokumentera och spara rollbackvägen. Notera commit, migreringar och tidpunkt. Behåll minst två tidigare releaser och databasdumpen tills nästa release är verifierad och stabil i produktion.
  13. prepare-the-project

Checklistan passar dig som

  • kör Django på egen VPS eller managed VPS med Ubuntu eller Debian
  • vill förstå vad en PaaS gör bakom kulisserna
  • behöver en dokumenterad releaseprocess för revision

Mindre relevant om du

  • deployar via PaaS – där sköts proxy och processer åt dig
  • kör Kubernetes med färdiga Helm-diagram
  • bara testar lokalt med runserver

Miljövariabler och secrets

Inställningar som skiljer mellan miljöer läses från miljövariabler. Settings-filen innehåller inga hemligheter och kan ligga i Git; värdena ligger i en miljöfil som systemd läser in.

Produktionsinställningar som läser från miljön:

# minapp/settings.py (utdrag för produktion)
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = os.environ["DJANGO_ALLOWED_HOSTS"].split(",")
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "CONN_MAX_AGE": 60,
    }
}

STATIC_URL = "static/"
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", BASE_DIR / "staticfiles")
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", BASE_DIR / "media")

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 3600  # höj stegvis när HTTPS fungerar överallt

Miljöfil med platshållare:

# /etc/minapp/minapp.env  (chmod 640, ägare root:minapp – aldrig i Git)
DJANGO_SETTINGS_MODULE=minapp.settings
DJANGO_SECRET_KEY=<genereras på servern>
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DJANGO_STATIC_ROOT=/srv/minapp/shared/staticfiles
DJANGO_MEDIA_ROOT=/srv/minapp/shared/media
DB_NAME=minapp
DB_USER=minapp
DB_PASSWORD=<från din lösenordshanterare>

Generera en ny SECRET_KEY på servern:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Behöver du mer än en miljöfil – till exempel rotation och åtkomstloggning – är det ett argument för managed hosting för Django med inbyggd hemlighetshantering.

Gunicorn/ASGI och reverse proxy

Gunicorn kör appen som WSGI. Med systemd-socketaktivering håller systemd socketen öppen medan tjänsten startas om, så att förfrågningar köas. Behöver du websockets eller async-vyer byter du worker-klass till Uvicorn via paketet uvicorn-worker och pekar på asgi.py; för websockets krävs dessutom Upgrade-headers i Nginx.

# /etc/systemd/system/gunicorn.socket
[Unit]
Description=Gunicorn-socket för minapp

[Socket]
ListenStream=/run/gunicorn.sock
SocketUser=www-data
SocketMode=600

[Install]
WantedBy=sockets.target
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn för minapp
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
NotifyAccess=main
User=minapp
Group=minapp
WorkingDirectory=/srv/minapp/current
EnvironmentFile=/etc/minapp/minapp.env
ExecStart=/srv/minapp/current/.venv/bin/gunicorn --workers 3 --access-logfile - minapp.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
Restart=on-failure

[Install]
WantedBy=multi-user.target
# ASGI-variant: pip install uvicorn-worker
ExecStart=/srv/minapp/current/.venv/bin/gunicorn -k uvicorn_worker.UvicornWorker --workers 3 minapp.asgi:application

Nginx som reverse proxy med TLS (lägg till http2 enligt din Nginx-version):

# /etc/nginx/sites-available/minapp
upstream minapp_app {
    server unix:/run/gunicorn.sock fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    client_max_body_size 10m;

    location /static/ {
        alias /srv/minapp/shared/staticfiles/;
        expires 30d;
        access_log off;
    }

    location /media/ {
        alias /srv/minapp/shared/media/;
    }

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_redirect off;
        proxy_pass http://minapp_app;
    }
}

Snabbt beslut

  • A – Klassisk app utan async: Gunicorn med sync- eller gthread-workers.
  • B – Websockets eller async-vyer: Gunicorn med Uvicorn-worker, eller Daphne, och Upgrade-headers i Nginx.
  • C – Du deployar till en PaaS: använd plattformens startkommando och hoppa över Nginx och systemd.

Behöver du en server att följa checklistan på? Kontrollera aktuella VPS-priser i SEK hos GleSYS.

Se VPS hos GleSYS

Databasmigrering och statiska filer

Migreringar är den riskablaste delen av en release, eftersom de inte alltid går att backa. Använd mönstret expandera–migrera–kontrahera: lägg först till nya kolumner som tillåter null, deploya kod som skriver till både gammalt och nytt, och ta bort det gamla i en senare release. Då kan föregående kodversion köras mot det nya schemat.

database-migration-and-static-files

Releaseskript (förenklat, platshållare):

# Körs av deploy-användaren (inte appens användare); REL = ny releasekatalog
REL=/srv/minapp/releases/2026-10-12_1400
cd "$REL"
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
set -a; . /etc/minapp/minapp.env; set +a
.venv/bin/python manage.py check --deploy
PGPASSWORD="$DB_PASSWORD" pg_dump -h "${DB_HOST:-localhost}" -U "$DB_USER" -Fc   -f /srv/minapp/backups/pre_release.dump "$DB_NAME"
.venv/bin/python manage.py migrate --noinput
.venv/bin/python manage.py collectstatic --noinput
ln -sfn "$REL" /srv/minapp/current
sudo systemctl restart gunicorn
curl -fsS https://example.com/healthz/

Verifiera drift och rollback

En release är klar först när den är verifierad. Tabellen visar vad som ska vara uppfyllt i varje steg och hur du backar.

Releasesteg, klarkriterier och rollback
StegKlarkriteriumKontrollmetodRollback
ByggeBeroenden installerade, tester grönaCI-logg, pip checkAvbryt; inget har ändrats i produktion
KonfigurationInga varningar från deploycheckenmanage.py check --deployRätta miljöfilen och kör om
MigreringAlla migreringar körda, gammal kod fungerarmanage.py showmigrationsReversibel migrering bakåt, annars pg_restore från dump
Statiska filerCSS och JS laddas utan 404Webbläsarens nätverksflik, Nginx-loggPeka tillbaka symlänken
AppserverGunicorn aktiv, inga omstartslooparsystemctl status gunicorn, journalctl -u gunicornFöregående release + omstart
TrafikHälsokontroll svarar 200, felkvot normalcurl -fsS mot /healthz/, felspårningSymlänk tillbaka, omstart

Rollback av kod:

ln -sfn /srv/minapp/releases/<föregående release> /srv/minapp/current
sudo systemctl restart gunicorn

Exempel: release en tisdag kl. 14.00

14.00 dump av databasen (pg_dump -Fc). 14.03 migrering lägger till en kolumn som tillåter null. 14.05 collectstatic. 14.06 symlänken byts och Gunicorn startas om – socketen köar förfrågningar under omstarten. 14.07 hälsokontrollen svarar 200, men felspårningen visar fel i en vy. 14.09 symlänken pekas tillbaka; den gamla koden fungerar mot nya schemat tack vare bakåtkompatibel migrering. Ingen databasåterställning behövs.

Illustrativt scenario, inga uppmätta värden.

Sju vanliga fel i produktion:

  1. DEBUG påslaget. Felsidor exponerar inställningar, sökvägar och SQL. Styr DEBUG via miljön med avstängt som standardvärde och låt deploychecken stoppa releasen om det ändå är på.
  2. Fel eller tom ALLOWED_HOSTS. Ger 400 Bad Request på alla anrop efter deploy. Ange exakta domäner, inklusive www-varianten, och glöm inte CSRF_TRUSTED_ORIGINS för formulär över HTTPS.
  3. Saknad SECURE_PROXY_SSL_HEADER. Django tror att anropen kommer via HTTP och skapar omdirigeringsloopar med SECURE_SSL_REDIRECT. Sätt headern och se till att Nginx skickar X-Forwarded-Proto.
  4. Migreringar som inte går att backa. En borttagen kolumn kan inte återskapas med kod. Dela upp destruktiva ändringar över två releaser och ta alltid en dump före migrering.
  5. Statiska filer saknas. Glömd collectstatic eller fel alias i Nginx ger sidor utan CSS. Kontrollera sökvägen i konfigurationen och att Nginx-användaren har läsrätt till katalogen.
  6. Hemligheter i Git. En incheckad nyckel finns kvar i historiken även efter borttagning. Byt den omedelbart, rensa historiken vid behov och lägg in skanning av hemligheter i CI.
  7. Ingen övervakning efter release. Utan felspårning och larm märker du problem först när användare hör av sig. Följ felkvot och svarstider aktivt under minst en kvart efter varje release.

Har du inte valt plattform än? Jämför alternativen för django hosting i Sverige eller läs om skillnaderna mellan django hosting-modeller för små och stora projekt.

Sammanfattning

En säker Django-deploy bygger på tre principer: hemligheter utanför Git, upprepbara releaser i egna kataloger och migreringar som tål att koden backas. Med Gunicorn eller Uvicorn bakom Nginx, systemd-socketaktivering och en hälsokontroll kan du både släppa och backa på några minuter.

Vanliga frågor

Hur många Gunicorn-workers ska jag starta?

Gunicorns dokumentation föreslår (2 × antal kärnor) + 1 som startpunkt, alltså tre workers på en server med en vCPU. Varje worker laddar hela projektet, så kontrollera minnet efter start. Har appen mycket väntan på externa API:er kan trådade workers (gthread) eller ASGI ge mer genomströmning per megabyte.

Ska jag använda WhiteNoise eller Nginx för statiska filer?

Båda fungerar. Med Nginx framför Gunicorn är det naturligt att låta Nginx servera katalogen från collectstatic. WhiteNoise passar på en PaaS utan egen proxy, eftersom Django då själv levererar komprimerade och versionerade filer med långa cache-headers. Mediafiler från användare ska inte serveras via WhiteNoise.

Hur gör jag zero-downtime-deploy på en enda server?

Med systemd-socketaktivering håller systemd porten öppen medan Gunicorn startas om, så nya förfrågningar köas i stället för att avvisas. Kombinera det med separata releasekataloger, bakåtkompatibla migreringar och en hälsokontroll innan du byter symlänk. Helt avbrottsfritt kräver dock två servrar bakom en lastbalanserare.

Var ska SECRET_KEY förvaras?

Aldrig i Git. Generera nyckeln på servern, lägg den i en miljöfil som bara root och appens användare kan läsa, eller i plattformens hemlighetshantering. Läs in den med os.environ i settings. Har nyckeln läckt, byt den – alla sessioner och signerade länkar blir då ogiltiga, vilket är avsikten.