Express aplikace, která funguje na tvém počítači, je asi patnáct řádků od té, která funguje na serveru, a každý z těch řádků tutoriály přeskakují, protože lokálně nezáleží na ničem z nich. Aplikace se musí navázat na port, který jí hosting přidělil, a to na všech rozhraních. Musí vědět, že běží za proxy, jinak je req.ip adresa proxy a tvůj rate limiter počítá všechny jako jednoho člověka. Potřebuje error handler, který zachytí odmítnuté promise, vypnutí, které dokončí rozpracované požadavky, a keep-alive časování, které nezávodí s tím, co stojí před ní.
Tohle je ten seznam, v pořadí, v jakém kousá. Platí pro každý HTTP server v Node - Fastify, Koa, holé http.createServer - ale konkrétně je tu Express, protože ten nasazuje nejvíc lidí.
Poslouchej na správném portu a správném rozhraní#
Dva řádky, oba jsou ve výchozím tutoriálu špatně:
const port = Number(process.env.PORT) || 3000;app.listen(port, () => { console.log(`listening on ${port}`);});Port se bere z prostředí. Hostingová platforma ti nějaký přidělí a oznámí ho přes proměnnou; napevno zapsaná 3000 znamená, že se aplikace buď nenaváže, nebo se naváže na port, na který nic nesměruje. Na RE:NODE obsahují tarify pro aplikace jednu alokaci portu, zobrazenou na záložce Network a dostupnou procesu jako proměnná prostředí nastavená na záložce Startup.
Rozhraní je ta záludnější půlka. app.listen(port) bez hostitele se naváže na všechna rozhraní, což chceš. app.listen(port, "127.0.0.1") se naváže na loopback uvnitř kontejneru a pak se nemůže připojit nic zvenčí - včetně reverzní proxy před ním. Příznak je aplikace, která naběhne bez chyby, zaloguje „listening“ a nic neodpovídá: connection refused od proxy, 502 v prohlížeči a ani řádek v tvém vlastním logu. Pokud chceš být explicitní, napiš "0.0.0.0".
Totéž platí pro cokoli dalšího, co poslouchá. Endpoint s metrikami, debug server, cesta pro upgrade na WebSocket: všechno žije za touto jednou alokací, takže to připoj na stejný server, místo abys otevíral druhý port, který nemáš.
Za proxy: trust proxy a skutečná IP klienta#
V produkci tvoje aplikace nemluví s prohlížečem. Mluví s reverzní proxy, která ukončila TLS, a podrobnosti původního požadavku přežijí jen jako hlavičky.
Bez konfigurace Express hlásí jako req.ip adresu proxy, jako req.protocol hodnotu http a jako req.secure hodnotu false. Všechno, co na tom závisí, je pak tiše špatně: rate limity podle IP se změní v jeden globální koš, auditní logy zapisují jednu adresu pro všechny uživatele a přesměrování na kanonického hostitele posílá lidi na http://.
app.set("trust proxy", 1);Jednička znamená „věř jednomu skoku“. Říká Expressu, aby za klienta vzal poslední záznam v X-Forwarded-For, protože ho přidala přesně jedna proxy. Spočítej si skoky: jedna proxy je 1, CDN před proxy je 2. Můžeš také předat podsíť, seznam nebo jednu z pojmenovaných předvoleb, například loopback.
Se správným nastavením je req.ip skutečná adresa klienta, req.protocol je https a req.secure je true. Na RE:NODE proxy slot přeposílá adresu klienta v X-Forwarded-For a certifikát řeší sám - nasměruj A záznam na zobrazenou adresu a certifikát se vystaví a obnovuje automaticky v 21denním okně. Mechanika je v článcích co reverzní proxy doopravdy dělá a nasměrování domény na server.
Pokud tvoje API servíruje WebSockety, musí být proxy ochotná propustit upgrade a na časových limitech nečinnosti záleží víc než u běžných požadavků. Konkrétní případ je WebSockety za reverzní proxy.
Middleware ve správném pořadí#
Express spouští middleware v pořadí registrace a několik produkčních problémů jsou problémy pořadí, ne konfigurace.
import express from "express";import helmet from "helmet";import compression from "compression";import cors from "cors";import pinoHttp from "pino-http";const app = express();app.set("trust proxy", 1);app.use(pinoHttp()); // log everything, including failuresapp.use(helmet()); // headers before any response bodyapp.use(compression()); // before the routes that produce bodiesapp.use(cors({ origin: ["https://app.example.com"], credentials: true }));app.use(express.json({ limit: "100kb" }));Co přesně každý z nich dělá:
- helmet nastavuje skupinu hlaviček odpovědi a odstraňuje
X-Powered-By. Pro JSON API jsou důležitéX-Content-Type-Options: nosniff,X-Frame-Options, referrer policy a HSTS. Jeho výchozí Content Security Policy míří na HTML, takže pokud ze stejné aplikace servíruješ dokumentaci nebo administrační stránku, policy nastav, místo abys helmet smazal. Než někomu řekneš konkrétní číslo, zkontrolujmax-agepro HSTS v nainstalované verzi. - compression gzipuje odpovědi nad určitou velikostí. U JSON jakékoli velikosti se vyplatí, u obrázků, videa nebo čehokoli už komprimovaného je zbytečná. Brotli nepodporuje. Také buffruje, což rozbije server-sent events, pokud po každém zápisu nezavoláš
res.flush(), a je nadbytečná, pokud už komprimuje proxy před tebou - dělat to dvakrát stojí CPU zbytečně. - cors musí běžet před tvými trasami, protože preflight
OPTIONSse k nim nikdy nedostane.credentials: truenejde kombinovat s wildcard originem; prohlížeče takovou dvojici odmítnou, takže své originy vypiš explicitně. - `express.json` má výchozí limit těla 100 KB. Zvyšuj ho vědomě, pokud přijímáš větší payloady, a nezvyšuj ho globálně jen proto, že jeden endpoint přijímá uploady - limit těla je nejlevnější obrana proti denial-of-service, jakou máš. Zbytek té plochy pokrývá rate limity a zneužití.
Logování jde první, aby se zalogoval i požadavek, který selže uvnitř jiného middlewaru. Loguj v JSON na stdout: konzole v panelu ho ukáže živě a strukturované řádky jsou rozdíl mezi grepováním a hádáním. Co si nechat a na jak dlouho najdeš v článku logy, které stojí za to uchovat.
Chyby a handler, který pravděpodobně nemáš#
Express pozná error handler podle počtu argumentů: čtyři, registrovaný po všem ostatním.
app.use((req, res) => { res.status(404).json({ error: "not_found" });});app.use((err, req, res, next) => { req.log.error({ err }, "request failed"); if (res.headersSent) return next(err); res.status(err.status || 500).json({ error: "internal_error" });});O tom, jestli to funguje, rozhodují tři detaily.
Async handlery. V Expressu 4 odmítnutou promise uvnitř async (req, res) => {} Express vůbec nezachytí. Stane se z ní unhandled rejection, která v současném Node ukončí proces - takže jedno neúspěšné volání databáze shodí všechny rozpracované požadavky. Opravy jsou: handlery obalit, použít balíček, který to záplatuje, nebo přejít na Express 5, který odmítnutí z async handlerů předává tvému error middlewaru. Pokud upgraduješ, měj na paměti, že Express 5 změnil i parser vzorů tras, takže pár starších vzorů cest je potřeba přepsat; když trasy přestanou odpovídat, podívej se do migračních poznámek.
Tělo odpovědi. Vestavěný error handler Expressu zahrne do odpovědi stack trace, pokud není NODE_ENV nastaveno na production. Stack trace prozradí útočníkovi strukturu adresářů, verze závislostí a často i databázový ovladač. Nastav NODE_ENV=production na záložce Startup a vracej kód chyby, ne zprávu z výjimky.
Handlery na úrovni procesu. Zaloguj a pak rozhodni, místo abys chybu spolkl:
process.on("unhandledRejection", (err) => { console.error("unhandled rejection", err);});process.on("uncaughtException", (err) => { console.error("uncaught exception", err); process.exit(1);});Neodchycená výjimka nechá proces v neznámém stavu, takže správná reakce je skončit a nechat platformu proces restartovat. Správné není chybu zachytit a jet dál, tak API končí obsluhováním požadavků z procesu s napůl zavřeným poolem databázových spojení.
Konfigurace, která selže rychle#
Konfiguraci čti jednou, při startu, a když něco chybí, odmítni nastartovat:
const required = ["DATABASE_URL", "SESSION_SECRET"];const missing = required.filter((name) => !process.env[name]);if (missing.length) { console.error(`missing configuration: ${missing.join(", ")}`); process.exit(1);}Proces, který se za dvě sekundy ukončí s jasným řádkem, se diagnostikuje nekonečně snáz než ten, který nastartuje, přijímá provoz a vrací 500, protože connection string byl undefined. Dobře to také funguje s pravidly restartu: server, který nemůže nastartovat, se netočí ve smyčce a nepředstírá, že funguje.
Tajné údaje patří do proměnných prostředí na záložce Startup, ne do repozitáře a ne do commitnutého .env. Kam dát tajné údaje na aplikačním serveru to obhajuje a popisuje, co dělat v den, kdy jeden unikne. Přihlašovací údaje k databázi dostaneš z panelu, když vytvoříš slot databáze - tarify pro aplikace obsahují dva - a spojení se poolují, což je místo, kde většina API udělá první kapacitní chybu. Connection pooly a limity vysvětlují, proč je pool o velikosti 100 na malé databázi pomalejší než pool o velikosti 10.
Start a zastavení bez ztracených požadavků#
Každé nasazení tvůj proces zastaví. Jestli to uživatele bude stát chybu, rozhoduje to, co se stane v posledních dvou sekundách.
const server = app.listen(port);server.keepAliveTimeout = 65_000;server.headersTimeout = 70_000;process.on("SIGTERM", () => { server.close(() => process.exit(0)); server.closeIdleConnections(); setTimeout(() => process.exit(1), 10_000).unref();});server.close() přestane přijímat nová spojení a počká na dokončení rozpracovaných požadavků. Samo o sobě se může zaseknout, protože nečinná keep-alive spojení jsou pořád spojení; closeIdleConnections() (Node 18.2 a novější) je uklidí a časovač je záchranná síť pro požadavek, který nikdy neskončí.
Ty dva timeouty jsou oprava konkrétního a k zešílení otravného příznaku: občasné 502 při slabém provozu a nic v logu aplikace. Výchozí keepAliveTimeout v Node je 5 sekund. Pokud proxy před tebou drží nečinná spojení déle, občas pošle požadavek po spojení, které Node ve stejnou chvíli zavírá, a proxy ohlásí bad gateway. Když je keep-alive okno Node delší než to proxy, závod zmizí, a headersTimeout musí být ještě větší, jinak spojení zavře dřív.
Přidej health endpoint, který nesahá na databázi:
app.get("/healthz", (req, res) => res.json({ ok: true, uptime: process.uptime() }));Liveness odpovídá na „žije tenhle proces“ a víc by dělat neměl. Health check, který spouští dotaz, změní pomalou databázi ve smyčku restartů, a ta změní zhoršenou službu ve výpadek. Pokud chceš zveřejnit readiness, udělej z ní druhý, samostatný endpoint. Rozdíl probírá graceful shutdown a health checky.
Nasazení#
Startovací příkaz instaluje z lockfilu a spouští jeden proces:
npm ci --omit=dev && node dist/server.jsnpm ci nainstaluje přesně to, co říká package-lock.json, a selže, pokud se lockfile a manifest neshodují, což na serveru chceš; npm install vyřeší něco nového a tak se nedotčená aplikace rozbije při restartu. Pokud je projekt v TypeScriptu, buď kompiluj v CI a nasazuj výstup, nebo vynech --omit=dev, protože kompilátor je devDependency. Detaily jsou v článku npm ci vs npm install.
Připoj repozitář, místo abys nahrával soubory. Na RE:NODE je to GitHub, přes GitHub App s krátkodobými tokeny, takže fungují i soukromé repozitáře, a se dvěma nezávislými přepínači: stáhnout větev při každém startu a nasadit při pushi, což restartuje už běžící server. Jeden řádek na každé nasazení ti řekne, který push je živý. Postup je v článku nasazení Node.js aplikace z GitHubu.
Správce procesů nepotřebuješ. Panel už proces restartuje, když skončí, ukazuje log a vykresluje graf paměti a CPU proti limitům, což je většina toho, kvůli čemu lidé instalují PM2 - upřímné srovnání je PM2 nebo hostingový panel. Pravděpodobně nepotřebuješ ani clustering: node:cluster násobí procesy, aby využil více jader, a na tarifu s půl jádrem přidává paměť a přepínání kontextu se záporným přínosem. Nejdřív měř a přečti si proč tvoje Node aplikace umírá na 2 GB na tarifu se 4 GB, než budeš předpokládat, že se paměť chová tak, jak čekáš.
Restart není okamžitý, takže nasazení stojí krátkou mezeru. S jedním procesem ji nejde udělat nulovou, ale jde ji udělat malou: udrž instalaci rychlou, nasazuj, když je provoz nízký, a přečti si nasazení bez výpadku na serveru, který má od všeho jen jeden kus, kde jsou postupy, které na jednom stroji skutečně fungují.
Co se rozbije první pod zátěží#
API, které je v pohodě při deseti požadavcích za sekundu a nepoužitelné při padesáti, obvykle selže jedním ze čtyř způsobů a žádný z nich nespraví větší tarif.
Odchozí volání bez timeoutu. Výchozí chování fetch je čekat donekonečna. Jeden pomalý upstream pak drží všechny požadavky, které na něm závisí, spojení se hromadí a proces dojde paměť, když je drží. Dej každému odchozímu volání termín:
const response = await fetch(url, { signal: AbortSignal.timeout(3_000) });Když už na tom jsi, znovu používej spojení. Globální fetch v Node drží spojení naživu podle originu, ale knihovna, která vytváří nového agenta pro každé volání, platí TLS handshake pokaždé, což při pár stech požadavcích za minutu je měřitelný kousek tvého CPU.
Žádný limit souběžnosti. Deset souběžných požadavků, z nichž každý otevře databázové spojení a alokuje megabajt, je v pořádku. Pět set ne. Přirozené místo, kde to omezit, je pool, protože požadavek čekající na spojení je levný, zatímco požadavek, který ho drží, ne. Velikost poolu dimenzuj podle toho, co databáze skutečně zvládne, ne podle toho, co by si přála tvoje aplikace.
Nic cachovatelného není označené jako cachovatelné. Hlavička Cache-Control u odpovědi, která se mění jednou za hodinu, odstraní většinu provozu dřív, než se k tobě dostane, a stojí jeden řádek. ETag je v Expressu ve výchozím stavu zapnutý a opakované požadavky mění na 304, ale pomáhá jen tehdy, když klienti provádějí revalidaci. Celý obraz je v článku HTTP cachovací hlavičky vysvětleně; stručně, nejlevnější požadavek je ten, který nikdy neobsloužíš.
Synchronní práce v event loopu. Parsování velkého JSON payloadu, hashování hesla s vysokým cost faktorem, změna velikosti obrázku, sestavení PDF: dokud něco z toho běží, proces neobsluhuje nikoho. Hashování hesel by mělo používat asynchronní API té knihovny, kterou sis vybral. Všechno těžší patří do workeru nebo do fronty.
Nad tím vším sedí limit paměti. Na RE:NODE se kontejner, který dosáhne limitu, zastaví a čistě restartuje, místo aby se nechal swapovat, takže únik paměti se projeví jako restart každých pár hodin, ne jako pomalý pokles, a tři neočekávané restarty za hodinu vyvolají varování a automaticky otevřou ticket. Takové chování je dobrý výchozí stav a důvod sledovat po každém nasazení graf paměti v konzoli, a ne až když už je něco špatně.
FAQ#
Proč moje API lokálně funguje, ale na serveru vyprší časový limit?
Téměř vždy kvůli bind adrese. localhost uvnitř kontejneru není dostupný zvenčí. Poslouchej na 0.0.0.0 nebo argument hostitele úplně vynech a použij port z prostředí místo napevno zapsaného.
Potřebuju před Expressem nginx?
Potřebuješ něco, co ukončí TLS, a na spravované platformě to už existuje. Spouštět vlastní nginx uvnitř kontejneru to zdvojuje. Co potřebuješ, je správně nastavené trust proxy, aby aplikace věděla, co udělala proxy před ní.
Je NODE_ENV=production opravdu důležité?
Ano. Zabrání Expressu vracet stack trace v odpovědích s chybou, zapne cache pohledů a čte ho dlouhý seznam knihoven, aby vypnuly vývojové chování. Je to jedna proměnná prostředí a mění skutečné chování.
Proč dostávám občasné chyby 502 a v logu nic nevidím?
Jde o výše popsaný keep-alive závod. Node zavře nečinné spojení ve stejnou chvíli, kdy ho proxy znovu použije. Nastav server.keepAliveTimeout nad limit nečinnosti proxy a server.headersTimeout nad něj.
Mám spouštět několik procesů Node přes cluster?
Jen když máš víc než jedno jádro k využití, a až po měření. Jeden event loop zvládne velké množství práce vázané na I/O; clustering pomáhá, když jsi vázaný na CPU, a stojí paměť na každý proces. Odhady provozu na počet workerů převádí článek dimenzování webové aplikace na den spuštění.
Kam patří uploady a generované soubory?
Ne do adresáře nasazení, protože ti při dalším pullu překážejí. Zapisuj na cestu mimo sledovaný strom nebo do objektového úložiště a pamatuj, že disk na malém tarifu se měří v jednotkách gigabajtů. Strukturovaná data zvládne slot databáze; případ, kdy databáze přeroste aplikační server, pokrývá hosting databází.




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.