Ibrohim.dev
Barcha yozuvlar
4 daqiqa o‘qish

Uch servis, bitta VPS: deploy rejasida yo‘q bo‘lgan olti narsa

Next.js sayt, adminka va Go API’ni Docker, Caddy va GitHub Actions bilan bitta serverga chiqarish - va “deploy bo‘ldi” degani aslida nimani anglatishini o‘rgatgan oltita xato.

deploydockergithub-actionsgonextjs

Bu sayt uchta servisdan iborat: siz o‘qiyotgan Next.js sayt, Next.js’dagi adminka va PostgreSQL oldida turgan Go API. Ular o‘zim boshqaradigan bitta VPS’da, Caddy orqasida, konteynerlarda ishlaydi. Vercel ham, tayyor (managed) baza ham yo‘q.

Reja bir varaqqa sig‘adi. Har repoda bitta workflow bor: mainga push bo‘lsa - testlar, image qurish, GHCR’ga yuklash, serverga SSH orqali kirib o‘sha bitta servisni yangilash.

main'ga push
   │
   ▼
 test ──► image qurish ──► GHCR'ga push ──► ssh: pull + up -d ──► tekshirish
                                                  │
            ┌─────────────────────────────────────┘
            ▼
   Caddy (80/443, TLS)
     ├─ fayzullayev.uz        → web        (Next.js)
     ├─ admin.fayzullayev.uz  → dashboard  (Next.js)
     └─ api.fayzullayev.uz    → api        (Go)  ──► db (PostgreSQL, faqat ichki tarmoq)

Quyida - o‘sha varaqda yo‘q bo‘lgan narsalar. Hech biri g‘alati emas va hech biri rejada yo‘q edi.

1. Mening kompyuterimda ishlagan lockfile

Birinchi CI build hech narsa kompilyatsiya qilmasdan yiqildi:

ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile"

Test asboblarini qo‘shgan edim, noutbukdagi pnpm install esa node_modulesni jimgina yangilagan, lockfile o‘sha holatda commit qilinmagan. Lokalda hech narsa shikoyat qilmaydi, chunki oddiy install lockfile’ni o‘zi to‘g‘rilab qo‘yadi. CI esa --frozen-lockfile bilan ishlaydi va bunga yo‘l qo‘ymaydi.

Bu bayroq muammo emas, aynan kerakli narsa: image ichidagi kutubxonalar men sinab ko‘rgan versiyalar ekaniga yagona kafolat shu. Yechim - lockfile’ni qayta generatsiya qilib commit qilish va package.json o‘zgarganda push’dan oldin lokalda ham pnpm install --frozen-lockfile ishlatish.

2. Go versiyasi haqida kelisha olmagan ikki fayl

go.modda go 1.26.4 turgan. Dockerfile’da esa FROM golang:1.23-alpine. Noutbukimda yangi Go bor, shuning uchun go build ishlagan. Image esa qurilmadi.

Ikki o‘zgarish: build image golang:1.26-alpine bo‘ldi, go.mod esa 1.26.4 dan 1.26 ga tushdi. go.moddagi patch raqami toolchain’ni bu loyihaga kerak bo‘lganidan qattiqroq bog‘lab qo‘yadi.

3. Bo‘sh qurilgan sayt

Birinchi deploy muvaffaqiyatli o‘tdi, blogda esa birorta post yo‘q edi.

Ro‘yxat sahifalari statik generatsiya qilinadi va generateStaticParams() kontentni API’dan so‘raydi. CI’da API yo‘q. Men ataylab bu so‘rov yiqilmasdan bo‘sh ro‘yxat qaytaradigan qilgandim - build ishlab turgan backend’ga bog‘liq bo‘lmasin deb. Buning narxi: build ichida hech narsa yo‘q saytni ham bemalol qurib beradi.

Yechim build’ni yiqitish emas. Yechim - kontent build paytida o‘qiladi degan fikrdan voz kechish: sahifalar har 60 soniyada qayta tekshiriladi, deploy’dan keyingi birinchi so‘rov ularni haqiqiy API’dan to‘ldiradi. Bungacha vaqtinchalik yechim har deploy’dan keyin konteynerni qo‘lda restart qilish edi.

4. Yashil bo‘lib tugagan, lekin hech narsa qilmagan deploy

Run yashil tugadi - test, build, deploy - sayt esa hamon eski matnni ko‘rsatardi.

Ishora davomiylikda edi: deploy job o‘n ikki soniya ishlagan. Yangi Next.js image’ini pull qilish o‘n ikki soniyada bo‘lmaydi.

Mening health-check’im domenga curl yuborib 200 kutardi. Eski konteyner ham 200 qaytaradi. Tekshiruv nimadir ishlab turganini isbotlardi - bunga esa hech kim shubha qilmagan.

Endi deploy ishlayotgan narsani hozirgina pull qilingan narsa bilan solishtiradi:

want=$(docker image inspect "$REGISTRY/portfolio:$TAG" --format '{{.Id}}')
got=$(docker inspect "$(docker compose ps -q web)" --format '{{.Image}}')
[ "$want" = "$got" ] || exit 1

Shu qo‘yilgach keyingi deploy qizil bo‘ldi va asl sababni ko‘rsatdi. Image tegi butun compose fayl uchun eksport qilinadi, web esa depends_on: api deb e’lon qilingan. Demak up -d web API’ni sayt commit’ining xeshi bilan ko‘tarishga uringan - bunday image mavjud emas - yiqilgan va eski web konteynerini joyida qoldirgan. --no-deps buni tuzatdi. Image tekshiruvi bo‘lmaganida bunga ishora qiladigan hech narsa yo‘q edi.

5. Commit qilinmagan generatsiya kodi

API sqlc ishlatadi: men SQL yozaman, u Go kod generatsiya qiladi. Migratsiyadan keyin kodni lokalda qayta generatsiya qildim, hammasi kompilyatsiya bo‘ldi, push qildim. CI p.TranslationKey undefined ko‘rinishidagi oltita xato bilan yiqildi.

Generatsiya qilingan paket diskimda bor edi, commit’da yo‘q. Test job buni image qurilishidan oldin ushladi - test job aynan shuning uchun bor. Lekin oltita chalkash kompilyatsiya xatosi “git add ni unutding” deyishning yomon usuli. Endi CI buni to‘g‘ridan-to‘g‘ri aytadi:

sqlc generate
git diff --exit-code internal/db || {
  echo "internal/db eskirgan. 'make generate' ni ishga tushirib commit qiling."; exit 1; }

6. Uch haftadan keyin muddati tugagan token

Uch hafta hammasi ishladi. Keyin deploy shu xabar bilan yiqildi:

Error response from daemon: error from registry: denied

Server image’larni sozlash paytida bir marta qilingan docker logindagi shaxsiy token bilan tortib olardi. Tokenning muddati bor ekan. Hech narsa ogohlantirmaydi; sayt eski konteynerda ishlayveradi. Buni o‘sha kuniyoq bilganimning yagona sababi - 4-bo‘limdagi tekshiruv run’ni qizil qilgani.

Uzoqroq muddatli token sanani suradi, xolos. Endi workflow o‘zining qisqa muddatli tokenini serverga uzatadi va faqat shu bitta pull uchun login qiladi:

auth=$(mktemp -d)
echo "$GHCR_TOKEN" | docker --config "$auth" login ghcr.io -u "$GHCR_USER" --password-stdin
docker --config "$auth" pull "$REGISTRY/portfolio:$TAG"
rm -rf "$auth"

Run uchun yaratilib, run tugagach tashlab yuboriladigan ruxsat ikki run orasida eskirib qola olmaydi.

Bundan nima oldim

Oltitadan to‘rttasi bitta xato edi: muvaffaqiyatni muvaffaqiyatsizlikdan ajrata olmaydigan signalga ishonganman. Boshqa lockfile bilan yashil bo‘lgan build. Noto‘g‘ri konteynerdan kelgan 200. CI ko‘rmagan fayllar bilan mening kompyuterimda o‘tgan kompilyatsiya. Hech kim kuzatmagan sanagacha ishlagan login.

Hozir amal qiladigan qoidam: deploy pipeline yashil bo‘lganda emas, nima ishlayotganini isbotlay olganimda tugaydi. Image id’larini solishtir. Nomuvofiqlikda baland ovozda yiqil. Ruxsatga uni ishlatadigan job’ning umrini ber.

Tayyor platformalar bu ro‘yxatdagi har bir bandni yashiradi va buning uchun pul to‘lash adolatli. Har biriga bir marta duch kelgach, hech bo‘lmasa nima uchun to‘layotgan bo‘lishimni bilaman.

Deploy ishning faqat yarmi edi. Qolgan yarmi - repoda bor, serverda esa yo‘q bo‘lgan backup, watchdog va uptime pingi - yozib qo‘yib, hech qachon yoqmagan monitoringim haqidagi postda.