Django projekt jde do produkce změnou pěti věcí, zbytek se nechává být: vypnuté DEBUG, nastavené ALLOWED_HOSTS, tajný klíč pocházející z prostředí, statické soubory nasbírané na místo, které umí číst webový server, a gunicorn místo runserver. O většině zbytku ti Django řekne samo, když se zeptáš příkazem python manage.py check --deploy.
Dál projdeme každou z těch věcí popořadě a k tomu to, co kontrolní seznam nepokrývá: kolik workerů se vejde do paměti, kterou sis koupil, co spouštět při každém restartu a co jen jednou, jak se chová databáze, když si každý ze čtyř worker procesů otevře vlastní spojení, a pět chyb, které způsobují téměř každé nepovedené nasazení Djanga.
Nejdřív se zeptej Djanga, co je špatně#
Před čímkoli jiným spusť kontrolu nasazení nad produkčním nastavením. Přečte tvoji konfiguraci a vypíše každé nastavení, které je nebezpečné nebo chybí:
$ DJANGO_SETTINGS_MODULE=myproject.settings.production \ python manage.py check --deploy --fail-level WARNINGBude si stěžovat na DEBUG, na chybějící nastavení HSTS, na cookies, které nejsou označené jako secure, a na SECRET_KEY, který vypadá jako ten vygenerovaný příkazem startproject. Každé varování je skutečný problém s zdokumentovanou opravou a --fail-level WARNING způsobí nenulový návratový kód, takže ho můžeš vložit do deploy skriptu a nechat se jím zastavit.
Nezachytí ale všechno. Neví nic o tom, že jsou tvoje statické soubory nedostupné, o počtu databázových spojení ani o tom, jestli proxy před tebou nastavuje hlavičky, které tvoje nastavení očekává. To jsou sekce níže.
Nastavení: DEBUG, ALLOWED_HOSTS a tajný klíč#
DEBUG = False je řádek, na kterém záleží nejvíc. Když je zapnutý, výjimka vykreslí stránku s tvým nastavením, prostředím a stack trace včetně lokálních proměnných každému, kdo ji vyvolal. Navíc Django drží v paměti každý SQL dotaz, který spustí, po celou dobu života procesu, což vypadá přesně jako únik paměti.
Vypnutí mění ještě dvě věci, které lidi překvapí. Django přestane úplně servírovat statické soubory a začne vynucovat ALLOWED_HOSTS. Požadavek, jehož hlavička Host v tomto seznamu není, dostane prosté 400 a v logu záznam DisallowedHost.
import osDEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]ALLOWED_HOSTS = ["app.example.com", "www.example.com"]CSRF_TRUSTED_ORIGINS = ["https://app.example.com", "https://www.example.com"]Čtení tajného klíče přes os.environ[...] místo .get(...) je záměrné: chybějící klíč zastaví proces už při importu s jasnou chybou, místo aby tiše běžel s None a zneplatnil všechny relace. CSRF_TRUSTED_ORIGINS vyžaduje od Djanga 4.0 schéma a je příčinou hlášení „CSRF verification failed - Origin checking failed“, která se objevují v den, kdy web přejde za HTTPS.
Produkční nastavení drž v samostatném modulu, místo abys uvnitř jednoho souboru větvil podle proměnné prostředí. Běžné uspořádání je myproject/settings/base.py a vedle development.py a production.py, které z něj importují. Díky tomu se na otázku „co je v produkci vlastně nastavené“ dá odpovědět přečtením jednoho krátkého souboru. Samotné hodnoty pocházejí z prostředí - proměnné prostředí a tajné údaje popisují, kam je dát a co dělat, když jedna unikne.
Spuštění: gunicorn, workery a paměť#
Django mluví WSGI, takže výchozí odpověď je gunicorn a myproject/wsgi.py už v projektu máš:
$ gunicorn myproject.wsgi:application \ --bind 0.0.0.0:${SERVER_PORT:-8000} \ --workers 3 --timeout 30 --access-logfile -Navázání na 0.0.0.0 místo 127.0.0.1 je to, co aplikaci zpřístupní zvenčí kontejneru; je to nejčastější příčina situace „běží to, ale nic se nepřipojí“. Port se bere z prostředí proto, že ti ho panel nebo platforma přidělí - název proměnné najdeš na záložce Startup.
Každý worker je samostatný proces s vlastní kopií Djanga, tvých modelů, šablon a všech nainstalovaných aplikací třetích stran. Skromný projekt zabere 100-200 MB na worker; s Django REST Framework, knihovnou na PDF a zpracováním obrázků je to víc. Měř pomocí ps -o rss= -p PID, nehádej.
| Paměť tarifu | Podíl CPU | Workery | Poznámky |
|---|---|---|---|
| 1 GB | 0.5 core | 1 | Místo druhého workeru přidej --threads 4 |
| 2 GB | 1 core | 2 | Pohodlné pro malý web |
| 4 GB | 1.5 cores | 3 | Obvyklá pracovní velikost |
| 6-8 GB | 2-3 cores | 4-6 | Za touto hranicí je limitem databáze, ne Django |
Dva přepínače si své místo zaslouží. --threads 4 spolu s --worker-class gthread umožní jednomu workeru obsloužit čtyři požadavky čekající na databázi za mnohem méně paměti než čtyři procesy. --max-requests 1000 --max-requests-jitter 100 pravidelně recykluje workery, což zamaskuje pomalý únik paměti, dokud ho nenajdeš.
Pokud vážně používáš Channels, websockety nebo asynchronní view, spouštěj myproject.asgi:application pod uvicornem. Všechno ostatní v tomto článku platí dál, mění se jen server. Srovnání obou modelů je v článku nasazení FastAPI nebo Flasku.
Statické soubory: collectstatic, WhiteNoise a média#
Ve vývoji Django najde statické soubory, ať leží kdekoli. V produkci je nehledá vůbec. collectstatic zkopíruje všechny statické soubory ze všech nainstalovaných aplikací do jednoho adresáře a ten pak musí servírovat něco jiného.
STATIC_URL = "/static/"STATIC_ROOT = BASE_DIR / "staticfiles"STATICFILES_DIRS = [BASE_DIR / "assets"]MEDIA_URL = "/media/"MEDIA_ROOT = BASE_DIR / "media"$ python manage.py collectstatic --noinputNejjednodušší způsob, jak výsledek na jednom serveru servírovat, je WhiteNoise, který posílá statické soubory přímo z procesu Djanga se správnými cache hlavičkami. Potřebuje jednu závislost, jeden řádek middlewaru hned za SecurityMiddleware a jedno nastavení úložiště:
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",]STORAGES = { "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", },}Slovník STORAGES je podoba z Djanga 4.2 a novějších; starší projekty používají místo něj STATICFILES_STORAGE, který byl v Djangu 5.1 odstraněn. Backend s komprimovaným manifestem přidá do názvů souborů hash a zapíše manifest, takže se assety dají cachovat navždy a nasazení cache zneplatní změnou názvů. Také selže build, pokud CSS soubor odkazuje na neexistující obrázek, což se napoprvé zdá nepřátelské a podruhé ti ušetří čas.
Média jsou jiný případ a WhiteNoise je servírovat nebude. Nahrané soubory uživatelů jdou do MEDIA_ROOT na trvalém disku, počítají se do diskového limitu tvého tarifu a nejsou v repozitáři, takže jsou to právě ty, na které lidé zapomínají při zálohování. Pokud na uploadech záleží, patří do plánu záloh spolu s databází.
Databáze, migrace a spojení#
Django otevírá nové databázové spojení pro každý požadavek, pokud mu neřekneš jinak. Na serveru, kde je databáze na jiném hostiteli, je ten handshake měřitelnou částí každé odpovědi:
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["DB_HOST"], "PORT": os.environ.get("DB_PORT", "5432"), "CONN_MAX_AGE": 600, "CONN_HEALTH_CHECKS": True, }}CONN_MAX_AGE drží spojení otevřené pro opakované použití deset minut. CONN_HEALTH_CHECKS, přidané v Djangu 4.1, způsobí, že Django znovu použité spojení před předáním požadavku otestuje, což zastaví chyby „server closed the connection unexpectedly“, které trvalá spojení jinak po nečinnosti produkují.
Na co si dát pozor v počtech: trvalá spojení jsou na jeden worker proces, takže tři workery gunicornu drží tři spojení, a drží je, ať přichází provoz, nebo ne. Přidej naplánovanou úlohu a shell relaci a malá instance PostgreSQL je blíž svému limitu, než bys čekal. Connection pooly a limity obsahují výpočty; PostgreSQL tarif v sekci hostingu databází je odpověď ve chvíli, kdy databázový slot v panelu už nestačí.
Migrace se spouštějí dřív, než nový kód začne obsluhovat provoz, ne zevnitř něj:
$ python manage.py migrate --noinputNa jednom serveru je to jeden řádek ve startovacím příkazu a doběhne dřív, než se gunicorn naváže na port. Zajímavé to začne být, když migrace zamkne vytíženou tabulku - přidání sloupce s výchozí hodnotou nebo index bez CONCURRENTLY - a všechny požadavky se zařadí do fronty za ní. Migrace bez výpadku je dlouhá verze; krátká zní, že každou migraci nad velkou tabulkou je třeba před spuštěním jednou přečíst.
Vytvoření prvního administrátora bez interaktivního shellu:
$ DJANGO_SUPERUSER_USERNAME=admin \ DJANGO_SUPERUSER_EMAIL=admin@example.com \ DJANGO_SUPERUSER_PASSWORD=... \ python manage.py createsuperuser --noinputHTTPS, cookies a provoz za proxy#
Tvoje aplikace téměř jistě dostává prosté HTTP od proxy, která ukončila TLS. Django to musí vědět, jinak bude věřit, že každý požadavek byl nezabezpečený, bude sestavovat URL s http:// a s proxy se bude přetahovat o přesměrování:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")USE_X_FORWARDED_HOST = TrueSESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueSECURE_SSL_REDIRECT = TrueSECURE_HSTS_SECONDS = 31536000SECURE_HSTS_INCLUDE_SUBDOMAINS = TrueSECURE_HSTS_PRELOAD = TrueSECURE_PROXY_SSL_HEADER nastav jen tehdy, když tuto hlavičku nastavuje u každého požadavku proxy, kterou ovládáš. Pokud je aplikace dostupná i přímo, klient si hlavičku může poslat sám a Django jí uvěří. A SECURE_SSL_REDIRECT zapni až poté, co hlavička od proxy funguje, jinak dostaneš nekonečnou smyčku přesměrování: Django přesměruje na HTTPS, proxy přepošle stejný prostý požadavek, Django přesměruje znovu.
U HSTS stojí za to chvíli přemýšlet, ne jen kopírovat. Roční SECURE_HSTS_SECONDS řekne každému prohlížeči, který tě navštívil, aby rok odmítal prosté HTTP, a pokud je zahrneš, i na subdoménách. To je správné nastavení pro web, který zůstane na HTTPS navždy, a bolestivé, pokud ještě přesouváš názvy hostitelů. Začni na pár hodinách, ověř, že nic nepadá, a pak hodnotu zvyš.
Na RE:NODE je proxy slot v tarifu pro aplikace to, co stojí na začátku toho obrázku: nasměruj A záznam na zobrazenou adresu, certifikát se vystaví a obnovuje automaticky v 21denním okně a skutečná adresa klienta přichází v X-Forwarded-For. Nasazení z Gitu jdou z GitHubu přes GitHub App s krátkodobými tokeny, takže fungují i soukromé repozitáře, aniž bys někam vkládal trvalý token.
Deploy: co běží při každém startu#
Rozhodni, co patří do startovacího příkazu a co do jednorázové úlohy, protože startovací příkaz běží při každém restartu procesu - i po pádu ve tři ráno, když má databáze špatný den.
$ pip install -r requirements.txt --no-cache-dir \ && python manage.py migrate --noinput \ && python manage.py collectstatic --noinput \ && gunicorn myproject.wsgi:application --bind 0.0.0.0:${SERVER_PORT:-8000} \ --workers 3 --access-logfile -Bezpečné při každém startu: instalace připnutých requirements (pip přeskočí to, co už je splněné), migrate (bez efektu, když není co aplikovat) a collectstatic --noinput (pár sekund, idempotentní). Nebezpečné při každém startu: cokoli, co načítá fixtures, cokoli, co posílá e-mail, a jakýkoli příkaz na opravu dat, který někdo kdysi napsal kvůli incidentu.
Připnutí verzí drží celé tohle poctivé. Pokud má requirements.txt rozsahy místo přesných verzí, restart ve špatnou chvíli nainstaluje jinou verzi něčeho, než jakou jsi testoval - lock soubory, které opravdu zamykají, najdeš v článku Python requirements a virtualenvy. Do prostředí přidej i PYTHONUNBUFFERED=1, jinak konzole nic neukáže, dokud se nezaplní výstupní buffer.
Logování by mělo jít na standardní výstup, aby ho měla konzole. Výchozí konfigurace Djanga jen posílá chyby e-mailem na ADMINS:
LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": {"console": {"class": "logging.StreamHandler"}}, "root": {"handlers": ["console"], "level": "INFO"},}E-mail si zaslouží vlastní varování: Django posílá resety hesel a hlášení chyb přes SMTP a hostingový tarif není poštovní server. Použij externího poskytovatele, EMAIL_HOST a spol. nastav z prostředí a reset hesla vyzkoušej před spuštěním, ne až po prvním uživateli, který se zamkne venku.
Práce na pozadí: plány, příkazy a Celery#
Django nemá vestavěnou frontu úloh. Možnosti, seřazené podle rostoucí složitosti:
- Management command na časovači.
python manage.py clearsessionsnebo vlastní příkaz, spouštěný cronem na stroji, kde ho máš, nebo malým plánovačem běžícím vedle webového procesu. Pokryje noční úklidy, generování reportů a souhrnné e-maily a nepotřebuje žádnou další službu. django-q2,hueynebo podobná odlehčená fronta s brokerem v databázi. Jeden proces navíc, žádná služba navíc.- Celery s vyhrazeným brokerem. Výkonné, a druhá služba, kterou musíš provozovat a platit. Má smysl, když jsou úlohy časté, potřebují opakování a musí přežít restart.
Buď k sobě upřímný, které z toho tvoje aplikace potřebuje. Většina malých Django webů chce první možnost, prodá se jim třetí a skončí s brokerem, který nikdo nemonitoruje. Úlohy na pozadí na malém serveru a Redis, když ho potřebuješ vedou stejný argument obšírněji.
Ať zvolíš cokoli, worker na úlohy je druhý proces, který soupeří o stejný podíl CPU a paměť jako gunicorn. Na tarifu s 1 GB jsou jeden worker gunicornu a jeden worker úloh už celý rozpočet.
Co se může pokazit#
Prosté 400 a `DisallowedHost` v logu. Hlavička Host není v ALLOWED_HOSTS. Přidej přesný název hostitele, který lidé používají, včetně www, pokud na něj míří DNS.
Web funguje, ale nemá styly. collectstatic neproběhl, STATIC_ROOT je špatně, nebo ten adresář nic neservíruje. Zkontroluj, že soubor fyzicky leží v STATIC_ROOT, a pak že je middleware WhiteNoise přítomný a na správném místě.
`collectstatic` selže s „could not be found“. Úložiště s manifestem řeší odkaz uvnitř CSS souboru, který ukazuje na chybějící asset. Oprav odkaz; alternativou je vypnout manifest a přijít o cache busting.
CSRF verification failed u formuláře, který včera fungoval. Obvykle chybí schéma v CSRF_TRUSTED_ORIGINS, nebo se cookie označená jako secure posílá přes spojení, které Django považuje za prosté HTTP. Nejdřív zkontroluj hlavičku od proxy.
Too many redirects. SECURE_SSL_REDIRECT bez fungujícího SECURE_PROXY_SSL_HEADER.
`OperationalError: too many connections`. Workery krát trvalá spojení překročily limit databáze. Sniž CONN_MAX_AGE, sniž počet workerů nebo přejdi na tarif s vyšším limitem.
Paměť stále roste, až se server restartuje. Zkontroluj, že je DEBUG opravdu False v tom modulu nastavení, který se skutečně načítá - nejčastěji za to může logování dotazů. Pokud si nejsi jistý, vypiš settings.DEBUG z manage.py shell.
FAQ#
Potřebuju vedle gunicornu i nginx?
Na jednom serveru s WhiteNoise ne, pokud před tebou už něco ukončuje TLS. Gunicorn obsluhuje aplikaci a WhiteNoise servíruje statické soubory se správným cachováním. Vlastní proxy přidej, když potřebuješ limity velikosti požadavků, rate limiting nebo cachování, které by Django dělat nemělo.
Kolik RAM potřebuje Django web?
Jeden worker gunicornu zabere typicky 100-200 MB, takže 1 GB utáhne malý web s jedním workerem a rezervou a 2-4 GB je pohodlné rozmezí pro ten skutečný. Číslo roste se závislostmi, ne s provozem, takže změř svůj vlastní projekt a nespoléhej na tabulku.
Mají se migrace spouštět při deployi automaticky?
Na jednom serveru ano - jako krok před startem gunicornu, aby žádný požadavek neobsluhoval kód, jehož schéma ještě nedorazilo. Riskantní je to u migrace, která zamyká velkou tabulku; ta by se měla zkontrolovat a spustit vědomě, ne ve chvíli, kdy se náhodou restartuje.
Kam jdou nahrané soubory uživatelů?
Do MEDIA_ROOT na disku serveru, který je oddělený od statických souborů a WhiteNoise ho neservíruje. Počítají se do disku tvého tarifu, nejsou v repozitáři a musí být v plánu záloh, jinak se nezálohují vůbec.
Můžu provozovat Celery na stejném serveru jako webovou aplikaci?
Můžeš a bude sdílet CPU a paměť tarifu s gunicornem. Těžší otázka je broker: Celery ho potřebuje a to je další služba. Pokud jsou tvoje úlohy periodické a ne řízené událostmi, udělá stejnou práci management command v plánu, bez jakéhokoli brokeru.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.