Deploya Django: checklista för säker produktionssättning
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.
- Lås beroendena. Fäst versioner i
requirements.txteller 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. - Kör testerna och deploychecken. Kör testsviten och
python manage.py check --deploymot produktionsinställningarna. Åtgärda varningar om DEBUG, HSTS, säkra cookies ochALLOWED_HOSTSinnan du går vidare. - 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.
- 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
.envi.gitignorefrån första commit. - 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.
- Checka ut releasen i egen katalog. Varje release får en katalog med tidsstämpel under
/srv/minapp/releasesoch en egen virtuell miljö. Då kan föregående version startas igen på sekunder. - Ta backup och kör migreringar. Gör en databasdump och kör sedan
migrateinnan ny kod tar trafik. Migreringarna ska vara bakåtkompatibla så att gammal kod fungerar under bytet. - Samla statiska filer. Kör
collectstatictill en delad katalog som Nginx serverar. Versionerade filnamn, till exempel viaManifestStaticFilesStorage, gör att webbläsare inte visar gammal CSS efter releasen. - 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.
- 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. - Byt trafik och verifiera. Peka symlänken
currentmot 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. - 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.

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 överalltMiljö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:applicationNginx 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 GleSYSDatabasmigrering 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.

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.
| Steg | Klarkriterium | Kontrollmetod | Rollback |
|---|---|---|---|
| Bygge | Beroenden installerade, tester gröna | CI-logg, pip check | Avbryt; inget har ändrats i produktion |
| Konfiguration | Inga varningar från deploychecken | manage.py check --deploy | Rätta miljöfilen och kör om |
| Migrering | Alla migreringar körda, gammal kod fungerar | manage.py showmigrations | Reversibel migrering bakåt, annars pg_restore från dump |
| Statiska filer | CSS och JS laddas utan 404 | Webbläsarens nätverksflik, Nginx-logg | Peka tillbaka symlänken |
| Appserver | Gunicorn aktiv, inga omstartsloopar | systemctl status gunicorn, journalctl -u gunicorn | Föregående release + omstart |
| Trafik | Hälsokontroll svarar 200, felkvot normal | curl -fsS mot /healthz/, felspårning | Symlä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:
- 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å.
- 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_ORIGINSför formulär över HTTPS. - 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 skickarX-Forwarded-Proto. - 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.
- Statiska filer saknas. Glömd
collectstaticeller felaliasi Nginx ger sidor utan CSS. Kontrollera sökvägen i konfigurationen och att Nginx-användaren har läsrätt till katalogen. - 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.
- 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.
