აპლიკაციის ჰოსტინგის მთელი აზრი ისაა, რომ zip ფაილი არასოდეს უნდა ატვირთო. Node აპლიკაცია GitHub რეპოზიტორიიდან სამი პარამეტრით დეპლოირდება: რომელი რეპოზიტორია და ბრენჩი, რა ბრძანება უშვებს მას და უნდა გადადეპლოირდეს თუ არა ამ ბრენჩზე push-ის შემდეგ. ამ პოსტში ყველაფერი დანარჩენი ამ სამის გარშემო არსებული დეტალებია - როგორი უნდა იყოს რეპოზიტორია, სანამ იმუშავებს, რომელ პორტზე უნდა მიუბა, სად მიდის საიდუმლოები და რვა რამ, რაც ვიღაცის პირველ დეპლოიზე ტყდება.
ეს დაწერილია კონტეინერზე დაფუძნებული ჰოსტისთვის პანელით, რასაც ახლა უმეტესობა აპლიკაციის ჰოსტინგში გულისხმობს. იგივე ფორმა ვრცელდება უბრალო VDS-ზეც systemd unit-ით და webhook-ით, და ეს სრულიად სხვა საქმეა, ვიდრე build pipeline, რომელიც კონტეინერის image-ს აწარმოებს - აქ image არ არის, მხოლოდ შენი წყარო და ბრძანება.
რას აკეთებს Git-იდან დეპლოი სინამდვილეში#
Git დეპლოი არის clone ან pull სერვერის საკუთარ ფაილურ სისტემაში, რასაც მოჰყვება გაშვების ბრძანება. ეს არის მთელი მექანიზმი და მისი გაგება დაბნეულობის უმეტეს ნაწილს ხსნის.
რა გამომდინარეობს აქედან:
- build სერვერი არ არსებობს. თუ შენს პროექტს build ნაბიჯი სჭირდება, ის იმავე კონტეინერზე სრულდება, რომელიც ტრაფიკს ემსახურება, იმავე მეხსიერებითა და CPU წილით. TypeScript-ის ან Next.js-ის build 1 GB გეგმაზე პირველი დეპლოის სიკვდილის ყველაზე გავრცელებული მიზეზია.
- სერვერზე ფაილები სამუშაო ასლია. ყველაფერი, რაც გაშვებისას რეპოზიტორიის დირექტორიაში იწერება - ატვირთვები, SQLite ფაილი, გენერირებული კონფიგურაცია - შემდეგი pull-ის საფრთხის ქვეშაა. გაშვებისას წარმოქმნილი მონაცემები checkout-ის გარეთ შეინახე, ან სულ მცირე იმის გარეთ, რასაც Git აკონტროლებს.
- შენი ბრენჩი არის შენი დადეპლოებული მდგომარეობა. ცალკე „release“ არ არსებობს. უკან დაბრუნება ნიშნავს revert-ის push-ს, ან სერვერის სხვა ბრენჩზე გადამისამართებასა და გადატვირთვას.
- პირად რეპოზიტორიას ავთენტიფიცირებული pull სჭირდება. სწორედ ამისთვის არსებობს GitHub App და სწორედ ამიტომ არის პირადი წვდომის token-ის კონფიგურაციის ფაილში ჩასმა არასწორი პასუხი.
თუ გინდა ცალკე ადგილი ამ ფორმის შესამოწმებლად, სანამ ის მოთამაშეებამდე ან კლიენტებამდე მივა, იმავე ანგარიშზე მეორე იაფი სერვერი, რომელიც staging ბრენჩზე მიუთითებს, საქმეს აკეთებს. Staging და production ერთ ანგარიშზე განმარტავს, როგორ არ დააშორო ისინი ერთმანეთისგან.
რეპოზიტორიის მომზადება#
პირველი წარუმატებელი დეპლოების უმეტესობა რეპოზიტორიის პრობლემაა და არა ჰოსტის. ხუთი რამ, რაც უნდა შეამოწმო, სანამ რამეს დააკავშირებ.
Commit-ში ჩასვი lockfile. package-lock.json რეპოზიტორიაში უნდა იყოს. მის გარეშე რეპროდუცირებადი ინსტალაცია არ არსებობს და npm ci საერთოდ ვერ გაეშვება.
იგნორირება გაუკეთე `node_modules`-ს. ის სერვერზე თავიდან აიგება, უზარმაზარია და Windows-ზე ან macOS-ზე აგებული commit-ირებული ასლი შეიცავს native ბინარებს, რომლებიც Linux-ზე არ მუშაობს.
გქონდეს რეალური start script და დარწმუნდი, რომ ის აგებულ შედეგს უშვებს და არა dev სერვერს. next dev, nodemon და ts-node production ბრძანებები არ არის: ისინი ფაილურ სისტემას აკვირდებიან, მეტ მეხსიერებას იყენებენ და ზოგი მათგანი გადატვირთვას სუფთად ვერ გადაურჩება.
გამოაცხადე შენი Node ვერსია. engines ველი დოკუმენტაციაა შენთვის იმდენადვე, რამდენადაც ინსტალერისთვის, და ის პირველია, რასაც ამოწმებ, როცა რაღაც ლოკალურად მუშაობს და სერვერზე არა.
`.env` გარეთ დატოვე. დაამატე .env .gitignore-ში და commit-ში ჩასვი .env.example გასაღებებით და მნიშვნელობების გარეშე, რომ შემდეგმა ადამიანმა იცოდეს, რა შეავსოს.
{ "name": "example-api", "private": true, "type": "module", "engines": { "node": ">=20" }, "scripts": { "build": "tsc -p tsconfig.json", "start": "node dist/server.js" }, "dependencies": { "express": "^4.21.2" }, "devDependencies": { "typescript": "^5.7.2" }}GitHub-ის სერვერთან დაკავშირება#
სერვერის GitHub ჩანართზე დააყენე აპლიკაცია შენს ანგარიშზე ან ორგანიზაციაზე, მიანიჭე მას რეპოზიტორიები, რომლებზეც წვდომა გინდა, შემდეგ აირჩიე ერთი რეპოზიტორია და ერთი ბრენჩი.
პანელი GitHub-თან საუბრობს GitHub App-ით მოკლევადიანი installation token-ებით და არა იმით, რომ პირადი წვდომის token ჩასვა. ეს ორი მიზეზით არის მნიშვნელოვანი. პირადი რეპოზიტორიები მუშაობს იმ credential-ის გარეშე, რომელიც შენი სერვერის კონფიგურაციაში ცხოვრობს, და token, რომლის გაუქმებაც უნდა გახსოვდეს, არის token, რომელიც არასოდეს უქმდება. GitHub-ში რეპოზიტორიაზე აპლიკაციის წვდომის მოხსნა მაშინვე მოქმედებს, სერვერის მხარეს გასაწმენდი არაფრით.
GitHub აქ ერთადერთი პროვაიდერია. თუ შენი კოდი GitLab-ზე, Gitea-ზე ან პირად სერვერზეა, ბრენჩი GitHub-ზე გააშენე mirror-ად ან დაბრუნდი SFTP-ით ატვირთვაზე - SFTP და ფაილ მენეჯერი განიხილავს, როგორ გააკეთო ეს არეულობის გარეშე.
გაშვების ბრძანება#
გაშვების ბრძანება კონტეინერის ყოველ გაშვებაზე სრულდება და არა მხოლოდ დეპლოისას. კომპილირებული პროექტისთვის commit-ირებული build შედეგით ეს არის მთლიანი ბრძანება:
npm ci --omit=dev && node dist/server.jsგამოიყენე npm ci და არა npm install. npm ci აყენებს ზუსტად იმას, რასაც lockfile ამბობს, და ხმამაღლა ვარდება, როცა lockfile და package.json ერთმანეთს არ ემთხვევა, სწორედ ის, რაც სერვერზე გინდა. npm install ჩუმად წყვეტს იმისგან განსხვავებულს, რაც შენ გატესტე, და შემდეგ წერს შეცვლილ lockfile-ს, რომელიც შენს რეპოზიტორიაში არ არის. --omit=dev flag devDependencies-ს გამოტოვებს, რაც ჩვეულებრივ ინსტალაციის დროსაც და დისკის გამოყენებასაც განახევრებს.
თუ build სერვერზე უნდა მოხდეს, ფორმა ასეთია:
npm ci && npm run build && npm prune --omit=dev && node dist/server.jsეს აყენებს ყველაფერს, კომპილატორის ჩათვლით, აგებს, სამუშაო პაკეტებს ისევ შლის და იწყებს. ის სწორია და ნელია: მოელოდე 30-დან 90 წამამდე, სანამ პროცესი მოუსმენს, ყოველ გადატვირთვაზე, და მეხსიერების პიკს build-ის დროს, რომელიც ხშირად მუშა აპლიკაციის საჭიროებაზე ორ-სამჯერ მეტია. 1 GB გეგმაზე TypeScript-ის ან bundler-ის build ყველაზე სავარაუდო რამაა, რაც ზღვარს დაეჯახება. ორი გზა გამოსვლისთვის, უპირატესობის მიხედვით: build გააკეთე GitHub Actions-ში და commit-ში ჩასვი ან გამოუშვი შედეგი, ან build-ების დროს მეხსიერებისთვის ერთი გეგმით ზემოთ გადადი და უკან ჩამოდი, თუ აღმოაჩენ, რომ არ გჭირდებოდა. Node-ის მეხსიერების ლიმიტები ახსნილი heap flag-ებს და იმას განიხილავს, რას აკეთებს კონტეინერის ლიმიტი სინამდვილეში.
პორტი და მის გარშემო გარემო#
შენს გეგმას მოჰყვება გამოყოფა - მისამართი და პორტი, რომელიც Network ჩანართზე ჩანს - და ეს არის პორტი, რომელსაც შენი აპლიკაცია უნდა უსმენდეს. ორი წესი, ორივე ხალხს ატყუებს:
პორტი გარემოდან წაიკითხე და არასოდეს ჩაწერო მყარად. პანელები გამოყოფილ პორტს პროცესს გარემოს ცვლადად აძლევენ, Node აპლიკაციები კი პირობითად კითხულობენ PORT-ს. მიიღე ორივე და ლოკალური განვითარებისთვის რაიმე გონივრულზე დაბრუნდი:
import express from "express";const app = express();const port = Number(process.env.PORT ?? process.env.SERVER_PORT ?? 3000);app.set("trust proxy", 1);app.get("/healthz", (req, res) => res.status(200).send("ok"));const server = app.listen(port, "0.0.0.0", () => { console.log(`listening on ${port}`);});process.on("SIGTERM", () => { server.close(() => process.exit(0));});მიბმა `0.0.0.0`-ზე და არა `127.0.0.1`-ზე. ეს არის ნომერ პირველი მიზეზი „მუშაობს, ლოგი წერს listening და არაფერი უკავშირდება“-სი. კონტეინერის შიგნით localhost ნიშნავს კონტეინერს და არაფერს სხვას. Express ნაგულისხმევად ყველა ინტერფეისზე დგას, თუ ჰოსტს გამოტოვებ, მაგრამ ბევრი framework და მაგალითი loopback-ზე დგას, ხოლო Vite-ის preview სერვერსა და Next.js-ს მისთვის flag-ები აქვთ.
გარემოს ცვლადები Startup ჩანართზეა. ისინი კონტეინერზე დგება, ამიტომ შენი კოდი მათ ჩვეულებრივად კითხულობს და არცერთი საიდუმლო არასოდეს იწერება იმაში, რასაც push-ავ. მონაცემთა ბაზის credential-ები, API გასაღებები, webhook URL-ები და ბოტების token-ები ყველა იქ უნდა იყოს - გარემოს ცვლადები და საიდუმლოები სრულ სიას განიხილავს და იმას, რა უნდა გააკეთო, როცა ერთი უკვე commit-ში მოხვდა.
ჩავარდი სწრაფად, როცა რომელიმე აკლია, იმის მაგივრად, რომ დაიწყო და პირველ მოთხოვნაზე აღმოაჩინო:
const required = ["DATABASE_URL", "SESSION_SECRET"];const missing = required.filter((name) => !process.env[name]);if (missing.length) { console.error(`missing environment variables: ${missing.join(", ")}`); process.exit(1);}აპლიკაციის გეგმა აქ ბაზის slot-ებსაც მოიცავს, რომლებიც პანელში იქმნება გენერირებული ჰოსტით, მომხმარებლითა და პაროლით, ამიტომ კავშირის სტრიქონი არის ის, რასაც Startup ჩანართზე აკოპირებ და არა ის, რასაც თავად იგონებ.
Auto update და deploy on push#
ორი დამოუკიდებელი ჩამრთველია და ისინი სხვადასხვა რამეს აკეთებენ.
Auto update ბრენჩს ყოველ ჯერზე pull-ავს, როცა კონტეინერი იწყება. ნებისმიერი მიზეზით გადატვირთვა - ღილაკს დააჭირე, განრიგმა იმუშავა, პროცესი crash-ავდა - თან მოაქვს უახლესი commit. გამოსადეგია და ღირს ცოდნა, რადგან ეს ნიშნავს, რომ გატეხილი commit შენს ბრენჩზე შეიძლება აიღოს გადატვირთვამ, რომელსაც დეპლოიდ არ თვლიდი.
Deploy on push რეაგირებს GitHub-ის შეტყობინებაზე ამ ბრენჩზე push-ის შესახებ: ის pull-ავს და სერვერს რთავს თავიდან. ის მხოლოდ იმ სერვერს გადატვირთავს, რომელიც უკვე მუშაობდა, ამიტომ სერვერი, რომელიც განზრახ გააჩერე, გაჩერებული რჩება. ეს ერთი დეტალი ბევრ დაბნეულობას გიხსნის, როცა იმაზე მუშაობ, რაც ჯერ ცოცხალი არ უნდა იყოს.
პანელი თითო დეპლოიზე ერთ ჩანაწერს ინახავს, რომელიც push-ის მოსვლისას იხსნება და იხურება, როცა კონტეინერი ისევ მუშაობს. ეს არის განსხვავება დეპლოის შორის, რომელიც გავიდა, და დეპლოის შორის, რომელიც დაეცა, და ესაა პირველი ადგილი, სადაც უნდა შეხედო, როცა საიტი ისევ გუშინდელ კოდს ემსახურება: ან ჩანაწერი არ არსებობს, მაშინ webhook ან ბრენჩის სახელია არასწორი, ან ჩანაწერი ღიაა და არასოდეს იხურება, მაშინ კონტეინერი ვერ ბრუნდება და მიზეზი კონსოლშია.
პატარა პროექტის გონივრული ნაგულისხმევი: deploy on push ჩართული შენს production ბრენჩზე, auto update-იც ჩართული, რომ crash-გადატვირთვამ ძველ checkout-ზე უკან არ დაგაბრუნოს. თუ main-ზე დღეში ბევრჯერ აკეთებ push-ს, deploy on push გამორთე და გამოიყენე release ბრენჩი, რომელშიც გააზრებულად აერთიანებ.
დომენი აპლიკაციის წინ#
აპლიკაციისა და ვებ გეგმები შეიცავს reverse-proxy slot-ს. მიუთითე ჰოსტის სახელი მისამართზე, რომელიც slot-ზე ჩანს, A ჩანაწერით, და სერტიფიკატი ავტომატურად გაიცემა და განახლდება - განახლება 21-დღიან ფანჯარაში ხდება, ამიტომ ყოველწლიურად დასამახსოვრებელი არაფერია.
სამი რამ, რაც შენს აპლიკაციაში უნდა გააკეთო, როცა ის proxy-ს უკან დგას:
- Proxy-ს ენდე. რეალური კლიენტის IP
X-Forwarded-For-ში მოდის. სანამ შენს framework-ს არ ეტყვი ამ header-ის ნდობას, ყველა მოთხოვნა proxy-დან მოსულად ჩანს, რაც არღვევს rate limiting-ს, გეოლოკაციასა და შენს ლოგებს. Express-ში ეს არისapp.set("trust proxy", 1). - აპლიკაციაში HTTPS-ს ნუ აიძულებ. Proxy TLS-ს წყვეტს და შენს კონტეინერს უბრალო HTTP-ით ელაპარაკება. აპლიკაცია, რომელიც ყველა არა-HTTPS მოთხოვნას HTTPS-ზე გადაამისამართებს, უსასრულოდ ციკლდება. შეამოწმე
X-Forwarded-Proto, თუ გჭირდება იცოდე. - აბსოლუტური URL-ები საჯარო ჰოსტის სახელიდან გენერირე, და არა მოთხოვნის host header-იდან, თორემ OAuth callback-ები და ელფოსტით გაგზავნილი ბმულები შიდა მისამართზე მიუთითებს.
მიუთითე დომენი შენს სერვერზე და მიიღე SSL სერტიფიკატი არის სრული გზამკვლევი DNS ჩანაწერებით და იმით, რატომ ვარდება გაცემა, ხოლო რას აკეთებს reverse proxy არის ფონი, თუ კონცეფცია შენთვის ახალია. თუ შენი აპლიკაცია WebSocket-ებს იყენებს, წაიკითხე WebSocket-ები reverse proxy-ს უკან, სანამ ყოველ ოც წამში გათიშვას გამართავ.
როცა პირველი დეპლოი ვარდება#
დაახლოებით იმ თანმიმდევრობით, როგორც ხშირად ხდება თითოეული:
Push-ზე არაფერი ხდება. შეამოწმე, რომ ბრენჩის სახელი ზუსტად ემთხვევა, რეგისტრის ჩათვლით. შეამოწმე, რომ GitHub App-ს ისევ აქვს წვდომა ამ რეპოზიტორიაზე. შეამოწმე, რომ სერვერი მუშაობდა - deploy on push გაჩერებულ სერვერს არ რთავს.
`npm ci` გადის `EUSAGE`-ით. lockfile და package.json ერთმანეთს არ ემთხვევა, ან lockfile აკლია. გაუშვი npm install ლოკალურად, commit-ში ჩასვი განახლებული lockfile, ხელახლა push-ავე.
Build ჩუმად მოკლულია. ეს არის მეხსიერების ლიმიტი. კონტეინერი ზღვართან ჩერდება და სუფთად გადაიტვირთება და არა swap-ის საშუალებას იღებს, ამიტომ სტეკის ტრეისის მაგივრად შეკვეცილ ლოგს იღებ. build სხვაგან გააკეთე ან ერთი გეგმით ზემოთ გადადი.
ლოგი წერს listening, არაფერი უკავშირდება. მიბმულია 127.0.0.1-ზე, ან პორტზე, რომელიც გამოყოფილისგან განსხვავდება. ზოგჯერ ორივეზე.
`Error: Cannot find module` რაღაცაზე, რაც package.json-შია. თითქმის ყოველთვის --omit=dev შლის პაკეტს, რომელიც შენს runtime-ს რეალურად სჭირდება - TypeScript-ის path alias-ები და ზოგი ORM runtime-ზე ინსტრუმენტებს იზიდავს. გადაიტანე პაკეტი dependencies-ში.
Native მოდული ვერ იტვირთება. bcrypt, sharp, better-sqlite3 და მსგავსები პლატფორმის წინააღმდეგ კომპილირდება. node_modules არასოდეს ჩასვა commit-ში; დაე npm ci-მ ისინი სერვერზე ააგოს. თუ build-ს კომპილატორი არ აქვს, გადადი წმინდა JS ალტერნატივაზე (bcryptjs) ან წინასწარ აგებულ პაკეტზე.
პროცესი ყოველ რამდენიმე წამში გადაიტვირთება. წაიკითხე კონსოლი თავიდან და არა ბოლოდან - სასარგებლო შეცდომა პირველია, ციკლი კი მას მარხავს. კონსოლის კითხვა სწორედ ამაზეა, ხოლო რატომ გადაიტვირთება შენი გეიმ სერვერი განუწყვეტლივ crash-ციკლისგან დაცვას განიხილავს, რომელიც ბოლოს ერთვება.
`EADDRINUSE`. წინა პროცესი არ გასულა. ეს არის SIGTERM handler-ის ნაკლებობა; იხილე ზემოთ server.close() ბლოკი და სუფთა გამორთვა და health check-ები.
FAQ#
მჭირდება Docker Node აპლიკაციის ასე დასადეპლოებლად?
არა. სერვერი შენს პროცესს უკვე კონტეინერში უშვებს; შენ აწვდი წყაროს და გაშვების ბრძანებას და არა image-ს. მორგებული image-ები ამ პროცესის ნაწილი არ არის. თუ კონკრეტულად გინდა runtime image-ის კონტროლი, ეს VDS-ის საქმეა - იხილე VDS-სა და გეიმ პანელს შორის არჩევა.
შემიძლია დეპლოი GitLab-იდან ან პირადი Git სერვერიდან?
პირდაპირ არა - აქ ინტეგრაცია მხოლოდ GitHub-ისაა, GitHub App-ის გავლით. ჩვეული გამოსავალი mirror-ია: push-ავე ორივე remote-ზე, ან დაამატე GitLab CI job, რომელიც GitHub-ის რეპოზიტორიაში push-ავს, რომელსაც სერვერი აკვირდება. სხვაგვარად ატვირთე აგებული აპლიკაცია SFTP-ით.
როგორ დავაბრუნო ცუდი დეპლოი?
Push-ავე revert. git revert commit-ს და push-ავე, და deploy on push მას ისევე გაგზავნის, როგორც შეცდომა გაგზავნა. თუ აპლიკაცია გათიშულია და ახლა გჭირდება, სერვერის წინა ბრენჩზე ან tag-ზე მიმართვა და გადატვირთვა უფრო სწრაფია, მაგრამ მერე დაიმახსოვრე უკან გადატანა, თორემ შემდეგი push არაფერს გააკეთებს.
გადის npm ci ყოველ გადატვირთვაზე?
დიახ, თუ ის შენი გაშვების ბრძანების ნაწილია, და ეს ჩვეულებრივ ისაა, რაც გინდა - ის გარანტიას იძლევა, რომ დაყენებული ემთხვევა lockfile-ს. ფასი არის 20-დან 60 წამამდე გაშვების დრო პატარა გეგმაზე. თუ შენი დამოკიდებულებები სტაბილურია და გაშვების დრო უფრო მნიშვნელოვანია, npm ci გაშვების ბრძანებიდან შეგიძლია ამოიღო და დამოკიდებულების ცვლილების შემდეგ ხელით გაუშვა, მაგრამ მაშინ pull, რომელიც დამოკიდებულებებს ცვლის, არასწორად დაყენებულებით დაიწყებს.
რა ემართება ატვირთვებს და ფაილებს, რომლებსაც ჩემი აპლიკაცია წერს?
ისინი სერვერის დისკზე ცხოვრობენ და გადატვირთვას გადაურჩებიან, მაგრამ ყველაფერი, რაც Git checkout-ის შიგნითაა, pull-მა შეიძლება გადააწეროს. მომხმარებლების ატვირთვები დაწერე დირექტორიაში შენი რეპოზიტორიის გზის გარეთ, ან დირექტორიაში, რომელიც .gitignore-შია, და ჩართე ისინი backup-ებში - backup slot-ები გეგმას მოჰყვება და განრიგით შეიძლება გაეშვას.
რამდენი მეხსიერება სჭირდება Node აპლიკაციას?
პატარა API ან Discord ბოტი კომფორტულად ეტევა 512 MB-დან 1 GB-მდე მოსვენებისას; build ნაბიჯს სჭირდება სივრცე. დაიწყე 1 GB-ით, ერთი კვირა თვალი ადევნე კონსოლის გრაფიკს და გადადი ზემოთ, თუ ზღვარს build-ების დროს დაეჯახები და არა ტრაფიკის დროს. გეგმის შერჩევა Discord ბოტისთვის და ვებ აპლიკაციის ზომა გაშვების დღისთვის რიცხვებს გადის.




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