Next.js თვითჰოსტინგზე ორი ბრძანებით მუშაობს: next build ქმნის .next დირექტორიას, next start კი მას Node HTTP სერვერით აწვდის შენ მიერ არჩეულ პორტზე. ყველაფერი დანარჩენი, რაც არასწორად მიდის, ოთხი ფაქტის შედეგია. build-ს გაცილებით მეტი მეხსიერება და CPU სჭირდება, ვიდრე მომუშავე აპლიკაციას. build-ს სჭირდება devDependencies, რომლებიც მომუშავე აპლიკაციას არ სჭირდება. გარემოს ზოგი ცვლადი output-ში ჩაიზუსტება, ზოგი კი გაშვებისას იკითხება, და არაფერი გეუბნება, რომელია რომელი. და cache, რომელიც incremental static regeneration-ს სწრაფს ხდის, დისკზე დირექტორიაა, ამიტომ მას აზრი აქვს restart-ებზე და ერთზე მეტ instance-ზე.
აი რას ნიშნავს თითოეული ეს ერთ სერვერზე reverse proxy-ის უკან, რაც Next.js-ის გაშვების ჩვეულებრივი გზაა, როცა იმ პლატფორმაზე არ ხარ, რომლისთვისაც ის დაიწერა.
Build და start: ორი ბრძანება ძალიან განსხვავებული მადით#
$ npm ci # devDependencies included, on purpose$ npm run build # next build$ npm run start -- -p 3000 -H 0.0.0.0პირველი სიურპრიზია, რომ npm ci --omit=dev && npm run build არ მუშაობს. TypeScript, Tailwind, PostCSS, ESLint და შენი type პაკეტები devDependencies-ია და next build-ს სჭირდება. ან ყველაფერი დააინსტალირე და build სერვერზე გააკეთე, ან სხვაგან ააგე და output გაგზავნე. npm ci vs npm install ფარავს, რატომ არის npm ci ორივე შემთხვევაში სწორი installer.
მეორეა რესურსების გამოყენება. საშუალო აპლიკაციის კომპილაცია და type-check-ი შეიძლება ერთ გიგაბაიტზე მეტ heap-ს იკავებდეს და ყველა core-ს გამოიყენებს, რაც კი იპოვის, ხოლო იგივე აპლიკაცია, ტრაფიკს რომ ემსახურება, შემდეგ რამდენიმე ასეულ მეგაბაიტში ეტევა. build 1 GB container-ზე კლასიკური ჩავარდნაა: მთავრდება JavaScript heap out of memory-ით, ან container ლიმიტზე ჩერდება და log უბრალოდ წყდება, რაც exit code 137-ად ჩანს. არც ერთი შეტყობინება არ ამბობს "იყიდე მეტი მეხსიერება ოთხმოცდაათი წამით", მაგრამ ეს არის მისი მნიშვნელობა.
სამი გამოსავალი, იმ თანმიმდევრობით, რამდენადაც ძვირი ჯდება:
- გააკეთე build CI-ში და გაგზავნე output. სერვერი მხოლოდ production დამოკიდებულებებს აყენებს და გაშვებას აკეთებს. ყველაზე სწრაფი და ყველაზე ნაკლებად მოულოდნელი ვარიანტი და სწორი არჩევანი, თუ CI უკვე გაქვს.
- გააკეთე build დროებით უფრო დიდ tier-ზე. უფრო მაღალ tier-ზე გადასვლა უკვე არსებულ სერვერზე ლიმიტს ცვლის, ამიტომ გეგმა, რომელიც build-ისთვის გულუხვია და აპლიკაციისთვის საკმარისი, კანონიერი პასუხია, როცა build-ები იშვიათია.
- შეზღუდე heap და იმედი გქონდეს.
NODE_OPTIONS=--max-old-space-size=1536V8-ს აიძულებს, უფრო აგრესიულად შეაგროვოს garbage და არ გაიზარდოს ჭერისკენ, რომელიც ფაქტობრივად არ აქვს. ის ზოგ ჩავარდნას ნელ build-ად აქცევს. მეხსიერებას არ ქმნის. რატომ კვდება შენი Node აპლიკაცია 2 GB-ზე 4 GB-იან გეგმაზე ხსნის ორ ჭერს, რომლებიც აქ მონაწილეობს.
Build-ის დრო წილად CPU-ზე ამ გარიგების მეორე ნახევარია. build, რომელიც ლეპტოპზე 40 წამს გრძელდება, ნახევარ core-ზე რამდენიმე წუთი შეიძლება გაგრძელდეს, ეს კი რამდენიმე წუთის გათიშვაა, თუ build იმავე სერვერზე კეთდება, რომელიც საიტს ემსახურება.
Standalone output და ორი საქაღალდე, რომელსაც ის არ აკოპირებს#
next.config.js-ში output: "standalone"-ის დაყენება build-ს აიძულებს, გამოსცეს დამოუკიდებელი დირექტორია: პატარა server.js და მხოლოდ ის node_modules ფაილები, რომლებიც tracing-მა აუცილებლად მიიჩნია. deploy-სთვის ეს ჩვეულებრივ მთელი ხის გაგზავნაზე ასობით მეგაბაიტით პატარაა.
const nextConfig = { output: "standalone", compress: false, // the proxy in front already does this poweredByHeader: false,};export default nextConfig;ნაწილი, რომელიც ყველას ებმევა, ისაა, რომ standalone build განზრახ არ აკოპირებს ორ დირექტორიას, რადგან ისინი CDN-ის მიერ უნდა მოიცეს: public და .next/static. თუ standalone საქაღალდეს სადმე გადაიტან და ისე გაუშვებ, საიტი იტვირთება CSS-ის, JavaScript-ისა და სურათების გარეშე, კონსოლი კი /_next/static/...-ის 404-ებით ივსება. ჩააკოპირე:
$ cp -r public .next/standalone/public$ cp -r .next/static .next/standalone/.next/static$ node .next/standalone/server.jsგენერირებული სერვერი გარემოდან კითხულობს PORT-სა და HOSTNAME-ს. დააყენე ორივე აშკარად - განსაკუთრებით HOSTNAME=0.0.0.0, რადგან container-ის შიგნით loopback-ზე მიბმული სერვერი proxy-სთვის მიუწვდომელია, ჩავარდნა კი ქსელის პარამეტრს კი არა, გატეხილ აპლიკაციას ჰგავს. აღნიშნე ისიც, რომ next start standalone build-ის გაშვების გზა არ არის; build გეუბნება, server.js პირდაპირ გაუშვა.
Standalone ღირს, როცა დისკი ან ინსტალაციის დრო შეზღუდულია. როცა არა, ის კიდევ ერთი მოძრავი ნაწილია, ხოლო monorepo-ში output file tracing root workspace-ის root-ზე უნდა მიუთითებდეს, თორემ traced bundle აპლიკაციის დირექტორიის გარეთ მყოფ პაკეტებს გამოტოვებს.
გარემოს ცვლადები: build-ზე ჩაზუსტებული, გაშვებისას წაკითხული#
ეს Next.js-ის თვითჰოსტინგის ყველაზე დამაბნეველი ნაწილია, და მის ქვეშ მარტივი წესი დევს.
| ცვლადი | როდის იკითხება | შეცვლას სჭირდება |
|---|---|---|
NEXT_PUBLIC_* | build-ზე ჩაეშენება client bundle-ში | rebuild |
| სერვერის მხარის ცვლადები დინამიკურ კოდში | ყოველ მოთხოვნაზე | restart |
| სერვერის მხარის ცვლადები სტატიკურად prerender-ებულ გვერდებზე | build-ზე, HTML-ში | rebuild |
NEXT_PUBLIC_API_URL გაშვებულ აპლიკაციაში ცვლადი არ არის. ეს სტრიქონია, რომელიც build-ისას შენს JavaScript-ში ჩაიწერა. Startup tab-ზე სხვა მნიშვნელობის დაყენება და restart არაფერს ცვლის, რადგან შესაცვლელი აღარაფერია. თუ მნიშვნელობა staging-სა და production-ს შორის უნდა განსხვავდებოდეს და browser-ში გამოიყენება, ან ორჯერ ააგე, ან გაშვებისას API route-იდან წაიღე.
შებრუნებული ხაფანგი არის სერვერის მხარის ცვლადი გვერდზე, რომელიც სტატიკურად prerender-დება. build-ის დროს არსებული მნიშვნელობა HTML-შია. თუ მნიშვნელობა მიმდინარე უნდა იყოს, route dynamic-ად მონიშნე, ან მნიშვნელობა მოთხოვნაზე მიბმული გზიდან წაიკითხე.
საიდუმლოები NEXT_PUBLIC_ პრეფიქსის გარეშე browser-ს არ ეჩვენება, პრეფიქსის მთელი აზრი ესაა, და ისინი გარემოს ცვლადებში უნდა იყოს Startup tab-ზე და არა repository-ში. სად შევინახოთ საიდუმლოები აპლიკაციის სერვერზე ზოგადი არგუმენტია და აქ განსაკუთრებული ძალით მოქმედებს, რადგან გაჟონილი NEXT_PUBLIC_ მნიშვნელობა უკვე ყველა ვიზიტორის browser-შია.
Cache: ISR, revalidation და რას კარგავს restart#
Incremental static regeneration გვერდს ერთხელ არენდერებს, შენახულ ასლს გასცემს და ფონურად ხელახლა არენდერებს, როცა ის მოძველდება. თვითჰოსტინგზე ეს საცავი დისკზე დირექტორიაა .next/cache-ის ქვეშ. აქედან სამი შედეგი გამომდინარეობს.
ახალი build ახალი cache-ია. ყოველ build-ს საკუთარი იდენტიფიკატორი აქვს, ამიტომ წინა deploy-ის მიერ გენერირებული გვერდები ხელახლა არ გამოიყენება. deploy-ის შემდეგ თითოეულ route-ზე პირველი ვიზიტორი არენდერისთვის იხდის. საიტზე, რომელსაც ბევრი ISR route აქვს, ეს თვალსაჩინო ცივი პერიოდია, რაც არგუმენტია, რომ მნიშვნელოვანი გვერდები deploy-ის შემდეგ რამდენიმე მოთხოვნით გაათბო და ნამდვილ მომხმარებელს არ დაუტოვო მათი პოვნა.
Restart-ები წესრიგშია, ეფემერული დისკები არა. cache restart-ს უძლებს, რადგან დირექტორია გადარჩება. ის rebuild-ს ვერ უძლებს და საერთოდ არ არსებობს, თუ ფაილური სისტემა გაშვებებს შორის იშლება. ჩვეულებრივ სერვერზე მუდმივი საცავით ეს პრობლემა არ არის, და ეს ერთ-ერთი ჩუმი უპირატესობაა Next.js-ის ჩვეულებრივ მანქანაზე გაშვებისა.
ერთზე მეტი instance ერთზე მეტ cache-ს ნიშნავს. ორი პროცესიდან თითოეული საკუთარს ინახავს, ამიტომ ერთი და იგივე URL ერთზე შეიძლება მოძველებული იყოს და მეორეზე ახალი. სწორედ ამისთვისაა next.config.js-ში cacheHandler პარამეტრი: მიუთითე გაზიარებულ საცავზე და ყველა instance თანხმდება. ერთ სერვერზე ის არ გჭირდება, კონფიგურაციის სახელი კი გამოშვებებს შორის შეიცვალა, ამიტომ თუ იყენებ, შეამოწმე დოკუმენტაცია შენი major ვერსიისთვის.
Revalidation თავად ორი ფორმით არსებობს: დრო, რომელიც export const revalidate = 3600-ით ყენდება route-ზე ან თითო fetch-ზე, და მოთხოვნით, revalidatePath ან revalidateTag route handler-იდან ან server action-იდან, რასაც რამის შეცვლის შემდეგ იძახებ. კონტენტისთვის, რომელიც იცვლება, როცა ადამიანი save-ს აჭერს, მოთხოვნით თითქმის ყოველთვის უკეთესია.
next/image და CPU, რომელიც არავის ჰქონდა ბიუჯეტში#
next/image სურათებს მოთხოვნაზე ოპტიმიზირებს მომუშავე სერვერში: ზომას უცვლის, თანამედროვე ფორმატში აკოდირებს და შედეგს .next/cache/images-ის ქვეშ ქეშავს. ეს ნამდვილი CPU და ნამდვილი დისკია, იმავე container-ზე, რომელიც გვერდებს არენდერებს.
- დააინსტალირე
sharp. მის გარეშე optimiser ძალიან ნელია და Next გაშვებისას ამის შესახებ აფრთხილებს. - გარე წყაროებს
images.remotePatternsსჭირდება, თორემ მოთხოვნები ნათელი შეტყობინებით ვარდება, რომ hostname არ არის გამართული. - cache-ის გასაღები არის წყარო, სიგანე და ხარისხი, ამიტომ კომპონენტი, რომელიც hero სურათის რვა ზომას ითხოვს, რვა ენკოდინგს ქმნის.
deviceSizesდაimageSizesაკონტროლებს, რამდენი ვარიანტი შეიძლება არსებობდეს. minimumCacheTTLგანსაზღვრავს, რამდენ ხანს ინახება ოპტიმიზებული ფაილი, სანამ ხელახლა შეიქმნება.unoptimized: trueმთელ ამას თიშავს, რაც სწორი პასუხია, როცა სურათები უკვე სწორი ზომისაა და სხვაგანაა განთავსებული.
ნახევარ core-იან გეგმაზე გვერდი, რომელსაც თორმეტი ოპტიმიზაციის გარეშე წყარო სურათი აქვს, პირველ მოთხოვნას ნელს გახდის და cache დირექტორიას ხელახალი ენკოდინგებით შეავსებს. ეს ყველაზე ხშირი მიზეზია, რის გამოც თვითჰოსტინგზე Next საიტი ლოკალურად კარგად გამოიყურება და პატარა სერვერზე ნელი. რას ცვლის NVMe სინამდვილეში აქ მოულოდნელი გზით არის რელევანტური: ენკოდინგი CPU-ზეა დამოკიდებული, ამიტომ სწრაფი დისკი მას არ იხსნის.
რა უნდა გააკეთოს წინა proxy-მ#
Next-ის საკუთარი სერვერი HTTP-ს ამუშავებს; წინ მდგარი proxy TLS-სა და hostname-ს. RE:NODE-ზე ეს არის proxy სლოტი, რომელიც ყველა app გეგმაშია: მიუთითე A ჩანაწერი ნაჩვენებ მისამართზე და სერტიფიკატი ავტომატურად გაიცემა და განახლდება 21-დღიან ფანჯარაში, კლიენტის მისამართი კი X-Forwarded-For-ში გადაიცემა. რას აკეთებს reverse proxy სინამდვილეში და დომენის მიმართვა შენს სერვერზე ორივე ნახევარს ფარავს.
ოთხი რამ, რაც ამ საზღვარზე სწორად უნდა გააკეთო:
- შეკუმშვა ერთხელ. Next პასუხებს ნაგულისხმევად ჯერ კუმშავს. თუ proxy-ც კუმშავს,
next.config.js-ში დააყენეcompress: falseდა proxy-ს მიანდე, თორემ ერთსა და იმავე ბაიტებს ორჯერ იხდი. - გადაცემული პროტოკოლი. კოდმა, რომელიც აბსოლუტურ URL-ებს აგებს, უნდა იცოდეს, რომ თავდაპირველი მოთხოვნა HTTPS იყო. წაიკითხე გადაცემული header-ები და ნუ დაუშვებ, თორემ შენი canonical ბმულები და redirect-ები
http://-ზე მიუთითებს. - Streaming. App Router-ის პასუხები stream-დება და Suspense საზღვრები თანდათან მოდის. proxy, რომელიც პასუხებს სრულად buffer-ავს, ამას ერთ გვიან პასუხად აქცევს - გვერდი მაინც მუშაობს, მაგრამ აღქმული სიჩქარის გაუმჯობესება ქრება.
- სტატიკური ფაილები.
/_next/static-ის ქვეშ ფაილები content-hash-ით არის დასახელებული და უცვლელია, ამიტომ მათი ქეშირება მკაცრად და დიდი ხნით შეიძლება. HTTP caching header-ები ახსნილი ფარავს, რომელი header-ები აკეთებს ამას, ხოლო TTFB, Core Web Vitals და ჰოსტინგი გულახდილია, Lighthouse-ის ქულის რომელ ნაწილს ცვლის ჰოსტინგი რეალურად.
Deploy და restart#
გაშვების ბრძანება ჰოსტზე, რომელიც სერვერზე აგებს, ასე გამოიყურება:
npm ci && npm run build && npm run start -- -p $PORT -H 0.0.0.0ეს გულწრფელია, მაგრამ ნელი, რადგან build ყოველ გაშვებაზე სრულდება. უკეთესი მოწყობაა build-ის გაკეთება მაშინ, როცა კოდი იცვლება და არა მაშინ, როცა პროცესი იწყება: ააგე CI-ში, დააკომიტე ან ატვირთე output და გაშვების ბრძანება იყოს npm ci --omit=dev && npm run start.
ფაილების ატვირთვის ნაცვლად repository მიაერთე. RE:NODE-ზე Git ინტეგრაცია მხოლოდ GitHub-ისაა, GitHub App-ით მოკლევადიანი token-ებით, ამიტომ კერძო repository-ები მუშაობს, და ორი გადამრთველია: branch-ის pull ყოველ გაშვებაზე და deploy on push, რომელიც უკვე გაშვებულ სერვერს restart-ს უკეთებს. თითო deploy-ზე ერთი ჩანაწერი გეუბნება, რომელი push არის რეალურად ცოცხალი. ინსტრუქცია არის პოსტში Node.js აპლიკაციის deploy GitHub-იდან.
Deploy დაცარიელებას ჯდება: პროცესი ჩერდება, ახალი აგებს ან იწყება და სანამ ის არ უსმენს, proxy-ს ესაუბრებოდეს არაფერი აქვს. ერთი პროცესით მას ნულს ვერ გახდი, მხოლოდ მოკლეს. build წინასწარ გააკეთე, deploy დაბალი ტრაფიკისას და წაიკითხე zero-downtime deploy სერვერზე, რომელსაც ყველაფერი ერთი აქვს, რა ეხმარება ნამდვილად. თუ შენი Next აპლიკაცია API route-ებსაც გამოაქვეყნებს, რომლებზეც სხვა რამეები არის დამოკიდებული, Express API-ის production-ზე deploy-ის საოპერაციო რჩევა - timeout-ები, სუფთა გამორთვა, health endpoint-ები - უცვლელად მოქმედებს.
პრობლემების მოგვარება#
`Could not find a production build in the '.next' directory`. გაუშვი build-ის გარეშე, ან build output იმის ნაწილი არ იყო, რაც deploy-დ გაიგზავნა. .next ჩვეულებრივ .gitignore-შია, ამიტომ repository-ის pull მას არ მოაქვს.
build შეცდომის გარეშე ჩერდება, exit code 137. container-მა build-ის დროს მეხსიერების ლიმიტს მიაღწია. ააგე სხვაგან, ან უფრო დიდ tier-ზე.
საიტი სტილების გარეშე იტვირთება. standalone build, სადაც public და .next/static არ არის ჩაკოპირებული.
`EADDRINUSE` გაშვებისას. წინა პროცესი ჯერ კიდევ იკავებს პორტს. გააჩერე სწორად; პანელში მეორე ასლის გაშვების ნაცვლად გამოიყენე Stop.
გვერდი არ განახლდება. გაარკვიე, რომელი cache. სტატიკურად გენერირებულ გვერდს revalidation ან rebuild სჭირდება; fetch-ის შედეგი შეიძლება შენი Next ვერსიის ნაგულისხმევებით იყოს ქეშირებული; browser-მაც შეიძლება ინახავდეს. შეამოწმე ამ თანმიმდევრობით.
გარემოს ცვლადი არაფერს ცვლის. თუ ის NEXT_PUBLIC_-ით იწყება, ის build-ის დროს bundle-ში ჩაიზუსტა და rebuild სჭირდება.
სურათები 404-ს ან hostname-ის შეცდომას იძლევა. images.remotePatterns წყაროს არ ჩამოთვლის.
FAQ#
მჭირდება თუ არა კონკრეტული პლატფორმა Next.js-ის გასაშვებად?
არა. next build და next start თვითჰოსტინგის მხარდაჭერილი გზაა და Node პროცესი reverse proxy-ის უკან framework-ს ისე უშვებს, როგორც დაგეგმილია. რასაც შენ იღებ, საოპერაციო მხარეა: build-ები, restart-ები, cache დირექტორია და სერტიფიკატი, რაზეც ამ პოსტის დანარჩენი ნაწილია.
რატომ ამოიწურება build-ის მეხსიერება, როცა აპლიკაცია მხოლოდ 200 MB-ს იყენებს?
იმიტომ, რომ კომპილაცია, type checking და bundling ერთდროულად ხდება და მთელ module გრაფს ინახავს. მომუშავე აპლიკაცია ამის თითქმის არაფერს ინახავს. ზომა build-ს მოარგე, ან build სერვერიდან გადაიტანე.
უნდა გამოვიყენო თუ არა standalone output?
გამოიყენე, თუ ინსტალაციის დრო ან დისკი შეზღუდულია, ან თუ image-ს აგებ გასაგზავნად. გახსოვდეს, რომ public და .next/static გენერირებული server.js-ის გვერდით ჩააკოპირო და HOSTNAME და PORT აშკარად დააყენო.
შემიძლია Next.js საიტის განთავსება სტატიკურ ან PHP გეგმაზე?
მხოლოდ თუ export-ს გააკეთებ. output: "export" ქმნის უბრალო HTML-ს, CSS-სა და JavaScript-ს სერვერის გარეშე, რომელსაც ნებისმიერი სტატიკური host ემსახურება - და ამით უარს ამბობს სერვერულ რენდერზე, ISR-ზე, route handler-ებსა და სურათების ოპტიმიზაციაზე. თუ შენი საიტი კონტენტია, რომელიც build-ისას იცვლება, ეს გაცვლა ხშირად კარგია; სტატიკური საიტის ჰოსტინგი და web ჰოსტინგი ამას ფარავს.
რამდენი მეხსიერება სჭირდება Next.js სერვერს გაშვებისას?
პატარა საიტს აგების შემდეგ 512 MB-დან 1 GB-მდე კომფორტულად ჰყოფნის და ის იზრდება concurrency-ით, route table-ის ზომითა და სურათების cache-ით. პიკი build-ია და არა მომსახურება. ერთი დღე უყურე მეხსიერების გრაფიკს კონსოლში, სანამ გადაწყვეტ, და წაიკითხე web აპლიკაციის ზომა გაშვების დღისთვის, თუ პიკს ელი.
რატომ არის ჩემი გვერდი მოძველებული კონტენტის შეცვლის შემდეგ?
იმიტომ, რომ რაღაცამ განზრახ დააქეშა. გაარკვიე, არის ეს სტატიკურად გენერირებული გვერდი, რომელიც revalidation ფანჯარას ელოდება, ქეშირებული fetch, თუ browser. revalidatePath-ით მოთხოვნით revalidation კონტენტის შეცვლის შემდეგ გამოცნობას აშორებს.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.