Kendi VPS'ine Next.js deploy eden her ekip aynı korkuyu yaşar: git push sonrası siteye gidip "acaba ayakta mı" diye bakmak. GitHub Actions ile kurulan bir CI/CD pipeline'ı bu korkuyu ortadan kaldırır — ama yalnızca doğru sırayla kurulursa: lint, test, build, sync, restart, health. Bu yazıda bu altı adımı, eşzamanlı build'in production'ı neden çökertebileceğini ve non-root bir servis kullanıcısıyla çalışırken dosya senkronunun hangi tuzaklara açık olduğunu anlatıyorum.
💡 Pro Tip: Deploy workflow'unu yazmadan önce tek bir soruyu cevapla: "Build başarısız olursa production'da ne çalışıyor olacak?" Cevap "eski build" değilse pipeline eksik demektir.
İçindekiler
- Pipeline şeması: lint → test → build → sync → restart → health
- Job'lar arası bağımlılık tablosu
- Build cache'i Actions'ta kalıcı kılmak
- Build-lock: eşzamanlı build neden production'ı çökertir
- Dosya senkronu ve sahiplik/izin tuzakları (non-root servis)
- Health-retry ve 'fail'de restart etme' kuralı
- CDN purge adımını pipeline'a bağlamak
- Secrets yönetimi ve deploy anahtarı hijyeni
- Kanıt: deploy log'u + canlı doğrulama
- SSS
- GitHub Actions ile kendi sunucuma nasıl deploy ederim?
- Next.js deploy'unda sıfır kesinti nasıl sağlanır?
- Eşzamanlı build'ler siteyi neden çökertir?
- Deploy başarısız olursa otomatik rollback nasıl kurulur?
- Build cache'i her deploy'da yeniden mi indirilir?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Pipeline şeması: lint → test → build → sync → restart → health
GitHub Actions workflow'ları repo kökünde .github/workflows/ dizini altında YAML dosyaları olarak tutulur — bu, GitHub'ın workflow dosyalarını nerede aradığının resmi tanımıdır (workflow syntax). on: anahtarı workflow'u tetikleyen event'i (örneğin push: branches: [main]) tanımlar; birden fazla event veya branch filtresi aynı anda tanımlanabilir. run-name ile her çalıştırmaya okunur bir isim verilebilir — CI panosunda hangi commit'in deploy edildiğini karışıklık olmadan görmek için küçük ama işe yarar bir alan.
Altı adımlık zinciri job'lara böl ve needs: ile sırala. Lint ve test birbirinden bağımsız paralel job'lar olarak çalışabilir; build ikisine de needs ile bağlanır; sync/restart/health ise build'e bağlanır. Bu, üçüncü parti bir araç gerektirmeyen, GitHub'ın resmi workflow modelinin doğrudan uygulamasıdır:
yaml
1name: Deploy to VPS2on:3 push:4 branches: [main]5run-name: "Deploy ${{ github.sha }} by ${{ github.actor }}"6 7jobs:8 lint:9 runs-on: ubuntu-latest10 steps:11 - uses: actions/checkout@v412 - run: npm ci13 - run: npm run lint14 15 test:16 runs-on: ubuntu-latest17 steps:18 - uses: actions/checkout@v419 - run: npm ci20 - run: npm test21 22 build:23 needs: [lint, test]24 runs-on: ubuntu-latest25 steps:26 - uses: actions/checkout@v427 - run: npm ci28 - run: npm run build29 - uses: actions/upload-artifact@v430 with:31 name: standalone-build32 path: .next/standalone33 34 deploy:35 needs: build36 runs-on: ubuntu-latest37 steps:38 - uses: actions/download-artifact@v439 with:40 name: standalone-build41 - run: echo "sync + restart + health burada"Örneklerdeki actions/* adımları @v4 ile sabitlendi; yazının tarihinde actions/checkout ve actions/cache'in daha yeni major sürümleri de yayındaydı — kendi runner'ına uyan bir major seç, sabitle ve yükseltmeden önce sürüm notlarını oku.
Sunucu tarafı kurulumunu (systemd unit + nginx reverse proxy) bu yazı tekrarlamıyor; o kısmı zaten Next.js'i systemd ve nginx ile production'da ayağa kaldırma yazısında adım adım anlattım. Buradaki odak, o kurulumun üstüne oturan otomasyon katmanı.
Job'lar arası bağımlılık tablosu
Job | needs | Amaç | Başarısız olursa |
|---|---|---|---|
lint | — | Kod stili + statik hata | Pipeline durur, deploy başlamaz |
test | — | Birim/entegrasyon testleri | Pipeline durur, deploy başlamaz |
build | lint, test | next build + artifact yükleme | Pipeline durur, eski build ayakta kalır |
deploy | build | sync → restart → health | Health fail'de restart YAPILMAZ (bkz. aşağı) |
Build cache'i Actions'ta kalıcı kılmak
GitHub Actions'ın bağımlılık cache mekanizması, paket yöneticilerinin (npm/yarn/pnpm) sık kullanılan dosyalarını run'lar arasında saklayarak yeniden indirmeyi önler (dependency caching). Kritik güvenlik kuralı nettir: restore edilen dosyalar güvenilmeyen girdi sayılmalı ve cache'e asla secret konmamalı. Bu, deploy anahtarlarının cache adımına değil GitHub Secrets'a ait olduğunun doğrudan gerekçesi.
yaml
1- uses: actions/cache@v42 with:3 path: ~/.npm4 key: npm-${{ hashFiles('package-lock.json') }}5 restore-keys: npm-Ayrı bir ayrım da önemli: build log'u gibi run sonrası saklanacak dosyalar cache değil artifact'tır — "Use artifacts when you want to save files produced by a job to use or view after a workflow run has ended, such as built binaries or build logs" (dependency caching). Yukarıdaki workflow örneğinde .next/standalone çıktısının actions/upload-artifact ile taşınması da bu ayrımın uygulamasıdır.
Build-lock: eşzamanlı build neden production'ı çökertir
GitHub Actions'ın concurrency anahtarı, aynı concurrency-group içinde aynı anda yalnız tek job veya workflow'un çalışmasını garanti eder (control workflow concurrency). Varsayılan davranışta, aynı gruba yeni bir run girdiğinde henüz başlamamış (pending) eski run iptal edilir ve yenisi onun yerini alır; cancel-in-progress: true verilirse şu an çalışan job da iptal edilip yenisi başlar.
yaml
1concurrency:2 group: deploy-${{ github.ref }}3 cancel-in-progress: falsePratikteki karşılığı, bu örnekteki gibi build'in ubuntu-latest runner'ında koştuğu kurulumlarda bile geçerli: art arda gelen push'lar aynı release dizinine paralel sync/restart job'ları çalıştırırsa yarım kalmış bir release servis edilebilir; build'in doğrudan VPS üzerinde koştuğu kurulumlarda ise düşük RAM'li bir sunucuda iki next build süreci aynı anda çalışması belleği tüketip build'in yarıda kesilmesine yol açabilir. concurrency bloğu bunu GitHub Actions seviyesinde engeller: aynı gruba yeni bir run girdiğinde bekleyen (henüz başlamamış) eski run iptal edilip yenisiyle değiştirilir; şu an çalışan run'ı da iptal etmek istiyorsan cancel-in-progress: true kullanılır. Sunucu tarafında ek olarak bir kilit dosyası veya pgrep -f 'next build' kontrolü eklemek savunma-derinliği katmanıdır ve tamamen operasyonel bir tercihtir.
Bu adım "main'e sık push" pratiğiyle doğrudan ilişkili: main'e sık push eden bir ekipte concurrency bloğu olmadan pipeline, art arda gelen push'larda üst üste binen build'ler üretebilir.
Dosya senkronu ve sahiplik/izin tuzakları (non-root servis)
Next.js'te ortam değişkenleri varsayılan olarak yalnız server tarafında okunur; bir değişkeni tarayıcıya açmak isteniyorsa NEXT_PUBLIC_ öneki zorunludur ve bu değerler next build sırasında JS bundle'ına gömülür (self-hosting). Bunun senkron adımı için pratik sonucu şu: .env dosyası build-time'da doğru olmalı, ama runtime'da da servis kullanıcısının erişebileceği bir yerde durmalı — world-readable olmadan.
Non-root bir servis kullanıcısıyla (systemd unit'inde User= ile tanımlı) çalışan bir kurulumda sync adımı üç şeyi aynı anda doğru yapmak zorunda:
- Sahiplik: kopyalanan dosyalar servis kullanıcısına ait olmalı,
root:rootkalmamalı. - İzin:
.envgibi sır içeren dosyalar640(sahibi okur-yazar, grup okur, diğerleri hiç) olmalı —644world-readable bırakır. - Atomiklik: dosyalar yarım kopyalanmış halde servis tarafından okunmamalı;
rsync+ geçici dizin +mvile değiştirme,cpile üstüne yazmaktan daha güvenli.
bash
1#!/usr/bin/env bash2# /usr/local/bin/app-sync.sh — sunucuda çalışır3set -euo pipefail4: "${GITHUB_SHA:?GITHUB_SHA deploy adımında env olarak geçilmeli}"5 6RELEASE="/srv/app/releases/${GITHUB_SHA}"7mkdir -p "$RELEASE"8rsync -a --delete /tmp/build-artifact/ "$RELEASE"/9chown -R appuser:appuser "$RELEASE"10chmod 640 "$RELEASE"/.env11echo "$RELEASE" > /srv/app/release-new.path12systemctl restart myapp-ssr@newGITHUB_SHA sunucu kabuğunda kendiliğinden tanımlı değildir; deploy adımındaki SSH çağrısı onu açıkça taşır: ssh deploy@sunucu "GITHUB_SHA='${{ github.sha }}' /usr/local/bin/app-sync.sh". Script'in başındaki :? kontrolü, değişken boş gelirse sync'i hiç başlatmadan durdurur — aksi halde her deploy aynı releases/ dizinine yazar ve önceki release'i ezerdi.
Burada current sembolik bağı HİÇ değiştirilmez; swap yalnız health-check script'inde, doğrulamadan sonra yapılır. Her release kendi commit SHA'sıyla ayrı bir dizine yazıldığı için bir önceki release diskte olduğu gibi kalır — geri dönüş hedefi, swap'tan hemen önce current'ın gösterdiği dizindir. myapp-ssr@new, aynı uygulamayı geçici NEW_PORT üzerinde ayrı bir systemd template instance'ı olarak çalıştırır; instance hangi release'i servis edeceğini açılışta /srv/app/release-new.path dosyasından okur, yani current'a değil doğrudan $RELEASE dizinine bakar. Bu yüzden her deploy'da start değil restart edilir (zaten çalışan bir instance yeni yolu ancak yeniden başlarken okur) ve health-check bittiğinde durdurulur; aksi halde bir sonraki deploy'un health-check'i önceki release'i doğrulardı.
Next.js dokümanı ayrıca self-host senaryosunda uygulamanın doğrudan internete açılmaması, önüne bir reverse proxy (nginx gibi) konması gerektiğini vurguluyor. Sync adımının "hangi kullanıcı hangi dizine yazıyor" sorusu da bu reverse-proxy + non-root servis modelinin doğal bir uzantısı — proxy dış dünyadan gelen isteği karşılar, uygulama süreci ise yalnızca kendi kullanıcısının yazabildiği dizinden çalışır.
Bu üç kural (sahiplik, izin, atomiklik) birbirinden bağımsız değil — üçü birden eksiksiz olmadığı sürece "sıfır kesinti" iddiası boş kalır. Örneğin sahiplik doğru ama izin 644 bırakılırsa .env içindeki sırlar sunucudaki her kullanıcı tarafından okunabilir hale gelir; atomiklik yoksa (doğrudan cp ile üstüne yazma) servis, sync sürerken yarım kalmış bir dizini okuyup hatalı yanıt verebilir. rsync'in --delete bayrağı da dikkat gerektirir: eski release'de olup yeni release'de olmayan dosyaları siler, bu yüzden geçici bir dizine yazıp ln -sfn ile symlink değiştirmek, canlı dizini doğrudan rsync --delete ile hedeflemekten daha güvenlidir — sync sırasında hiçbir an "yarım dizin" servis edilmez.
Health-retry ve 'fail'de restart etme' kuralı
Next.js'in dinamik olarak render ettiği sayfalar Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate header'ı taşır (self-hosting). Bu davranış, health-check adımında "yeni build gerçekten devrede mi" sorusuna teknik bir doğrulama noktası sağlıyor — beklenen header'ı dönmeyen bir yanıt, eski süreçlerin hâlâ ayakta olduğunun işareti olabilir.
Restart'ı yalnız health-check geçtikten SONRA tetikle: restart süreci anında öldürür, yeni build bozuksa restart sonrası site tamamen düşer. Bu sırayı korumak, "kötü deploy" senaryosunda bile eski (çalışan) sürecin ayakta kalmasını korur.
bash
1#!/usr/bin/env bash2set -euo pipefail3 4RELEASE=$(cat /srv/app/release-new.path)5NEW_PORT=40016LIVE_PORT=40007PREVIOUS=$(readlink -e /srv/app/current 2>/dev/null || true) # -e: hedef yoksa (ilk deploy) boş8 9# geçici instance başarı da olsa başarısızlık da olsa her çıkışta durur10trap 'systemctl stop myapp-ssr@new' EXIT11 12for i in 1 2 3; do13 if curl -fsS -o /dev/null "http://127.0.0.1:${NEW_PORT}/api/health"; then14 echo "new release healthy, swapping symlink and restarting"15 ln -sfn "$RELEASE" /srv/app/current16 systemctl restart myapp-ssr17 sleep 318 if curl -fsS -o /dev/null "http://127.0.0.1:${LIVE_PORT}/api/health"; then19 echo "OK: restarted service serving healthy new release"20 exit 021 fi22 echo "FAIL: restart verification failed"23 if [ -n "$PREVIOUS" ]; then24 echo "rolling back to $PREVIOUS"25 ln -sfn "$PREVIOUS" /srv/app/current26 systemctl restart myapp-ssr27 else28 echo "FAIL: no previous release to roll back to (first deploy)"29 fi30 exit 131 fi32 echo "health check $i/3 failed, retrying..."33 sleep 534done35 36echo "FAIL: new release health check did not pass, service NOT restarted, old build stays up"37exit 1Bu retry-then-restart deseni, restart'ın kendisini bir "commit noktası" gibi ele alıyor: yalnız yeni build zaten sağlıklı yanıt veriyorsa restart tetiklenir. Health-check'in her 3 denemesi de fail ederse script çıkış kodu 1 ile durur, ama servis dokunulmamış kalır — site ayakta kalmaya devam eder, sadece deploy başarısız sayılır. Not: tek süreçli bu kurulumda systemctl restart yine de birkaç saniyelik kısa bir devir penceresi bırakır — buradaki "sıfır kesinti" vaadi bozuk build'in hiç production'a yansımamasını garanti eder, restart anının kendisini değil; gerçek kesintisiz devir iki süreç (LIVE_PORT + NEW_PORT) ve reverse-proxy upstream değişimi gerektirir.
CDN purge adımını pipeline'a bağlamak
Purge adımı, deploy workflow'una health-check başarılı olduktan SONRA çalışan ayrı bir job olarak eklenir; gereken API kimlik bilgisi (token, zone ID) GitHub Secrets üzerinden workflow'a taşınır, asla YAML'a açık yazılmaz.
yaml
1purge-cdn:2 needs: deploy3 runs-on: ubuntu-latest4 steps:5 - name: Purge CDN cache6 env:7 CDN_API_TOKEN: ${{ secrets.CDN_API_TOKEN }}8 CDN_ZONE_ID: ${{ secrets.CDN_ZONE_ID }}9 run: |10 echo "CDN purge adımı burada — sağlayıcıya özgü API çağrısı"CDN sağlayıcısının kendi API'sini nasıl çağıracağın, kullandığın sağlayıcının resmi dokümantasyonuna bağlı; bu yazı yalnız "purge, pipeline'ın health-check'ten SONRA çalışan son adımlarından biri olmalı, önce değil" prensibini sabitliyor — purge önce çalışırsa CDN, henüz restart olmamış eski build'i cache'lemeye devam edebilir.
Secrets yönetimi ve deploy anahtarı hijyeni
Repository, environment veya organization seviyesinde bir secret oluşturmak GitHub CLI ile de yapılabilir: gh secret set alt komutu bunun için var (using secrets in GitHub Actions). Loglara sızabilecek hassas değerleri maskelemek için ::add-mask::VALUE kullanılır — bu, değeri bir GitHub secret'ı gibi işaretleyip log'lardan otomatik olarak siler.
bash
1gh secret set DEPLOY_SSH_KEY < deploy_key.pem2printf '%s' "$TOKEN" | gh secret set CDN_API_TOKENResmi doküman ayrıca net bir uyarı içeriyor: secret'ları komut satırı argümanı olarak süreçler arasında geçirmekten mümkün olduğunca kaçınılmalı, çünkü komut satırı süreçleri ps komutuyla başka kullanıcılar tarafından görülebilir — bunun yerine ortam değişkeni ya da standart girdi (stdin) tercih edilmeli. VPS/SSH deploy senaryosunda bunun karşılığı: deploy SSH anahtarı GitHub Secrets'ta tutulur, ssh-agent veya appleboy/ssh-action gibi bir action'a ortam değişkeni olarak verilir, asla ssh -i /path/key.pem ... şeklinde komut satırında açık yazılmaz. Mümkün olduğu durumlarda uzun ömürlü statik secret yerine OIDC ile buluta doğrudan kimlik doğrulama da resmi dokümanın önerdiği bir alternatif — VPS/SSH senaryosunda doğrudan karşılığı yok, ama bulut sağlayıcı entegrasyonu eklenirse (ör. CDN API'si bir bulut hesabından çağrılıyorsa) değerlendirilmeli.
Secret hijyeni pratiğe döküldüğünde birkaç basit kural yeterli oluyor; aşağıdaki tablo bunları özetliyor.
Ne | Doğru pratik | Yanlış pratik |
|---|---|---|
Deploy SSH anahtarı | GitHub Secrets, action'a env olarak | Komut satırında -i /path/key.pem |
CDN/API token | gh secret set ile eklenmiş secret | Workflow YAML içinde açık metin |
Log çıktısı | ::add-mask::VALUE ile maskelenmiş | Token'ı doğrudan echo ile bastırmak |
Cache içeriği | Yalnız bağımlılık dosyaları | Secret veya .env cache'e dahil edilmiş |
Kanıt: deploy log'u + canlı doğrulama
Build binary'leri ve log'lar gibi run sonrası incelenmesi gereken dosyalar workflow artifact'ı olarak saklanmalı — bu, "her deploy'un log kanıtı olmalı" ilkesinin GitHub Actions'taki resmi karşılığı. Yukarıdaki build job'ında actions/upload-artifact ile yüklenen .next/standalone çıktısı, bir deploy'un tam olarak neyi production'a taşıdığının kalıcı kaydını tutar.
Buna ek olarak health-check adımında görülen Cache-Control header'ı canlı doğrulamanın teknik parçasıdır: deploy bitince, CDN'i baypas edip doğrudan origin'e, dinamik render edilen bir SAYFA rotasına (bir Route Handler'a değil) atılan
bash
1curl -sI http://127.0.0.1:4000/dashboard | grep -i cache-controlçıktısının beklenen private, no-cache, no-store, max-age=0, must-revalidate değerini içermesi, "yeni process gerçekten yanıt veriyor" iddiasının kanıtı. Public alan adına atılan bir curl bunu kanıtlamaz — CDN edge'i origin'in header'ını ezebilir. Bu header ayrıca yalnız dinamik render edilen SAYFALAR için geçerli, /api/health gibi bir Route Handler için resmi dokümanın vaat ettiği bir davranış değil; statik prerender edilen bir sayfada da aynı header'ı bekleme — resmi doküman header değerini yalnız dinamik render edilen sayfalar, ISR yanıtları ve değişmez (immutable) statik varlıklar için tanımlıyor. Log kanıtı (artifact) + canlı response header kanıtı (curl) birlikte, "deploy bitti" iddiasının iki bağımsız doğrulamasını oluşturur — biri pipeline içinde, biri pipeline dışında.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Bu yazıyı okuyup kendi VPS pipeline'ını kuracaksan, deploy'a geçmeden önce kontrol etmen gereken maddeleri tek yerde topladım. Aşağıdaki listeyi kendi .github/workflows/deploy.yml dosyanla karşılaştır; her madde işaretlenmeden production'a otomatik deploy açma.
SSS
GitHub Actions ile kendi sunucuma nasıl deploy ederim?
Repo kökünde .github/workflows/deploy.yml dosyası oluşturup on: push: branches: [main] ile tetikleyiciyi tanımlarsın; build job'unda next build çalıştırıp çıktıyı artifact olarak yükler, ayrı bir deploy job'unda bu artifact'ı SSH üzerinden VPS'e senkronlar, ardından servisi (systemd unit) restart eder ve health-check ile doğrularsın. Sunucu tarafındaki systemd + nginx kurulumu ayrı bir konudur — Next.js standalone kurulumu yazısında adım adım anlattım.
Next.js deploy'unda sıfır kesinti nasıl sağlanır?
Sıfır kesinti, restart komutunun yalnız yeni build'in health-check'i geçtiği doğrulandıktan sonra çağrılmasıyla sağlanır. Sync adımı atomik olmalı (geçici dizine kopyala, sonra symlink değiştir), restart health-check'e bağımlı olmalı, health-check ise en az birkaç deneme yapmalı. Bu üçü birlikte, bozuk bir build'in production'a hiç yansımamasını garanti eder.
Eşzamanlı build'ler siteyi neden çökertir?
Düşük RAM'li bir VPS'te iki next build süreci aynı anda çalışırsa bellek tükenir, build'lerden biri (veya ikisi) yarıda kesilir ve bu da yarım kalmış bir .next dizininden servis edilmeye çalışılan bir sürece yol açabilir. GitHub Actions'ın concurrency anahtarı bunu workflow seviyesinde engeller: aynı concurrency-group'ta aynı anda yalnız tek job çalışır.
Deploy başarısız olursa otomatik rollback nasıl kurulur?
Bu yazının deploy script'i iki ayrı fail dalını farklı ele alır: yeni release'in health-check'i (3 denemenin) tamamı fail ederse restart hiç çağrılmaz, servise dokunulmaz — eski (çalışan) süreç zaten devrede kalır, yeni build'e hiç geçilmemiştir. Restart SONRASI doğrulama düşerse ise script symlink'i swap'tan hemen önce kaydettiği önceki release dizinine geri döndürüp servisi yeniden restart eder; her iki durumda da geçici @new instance'ı çıkışta durdurulur. İlk deploy'da geri dönülecek önceki release olmadığından script yalnız hata koduyla çıkar.
Build cache'i her deploy'da yeniden mi indirilir?
Hayır — actions/cache ile bağımlılık cache'i lock dosyasının hash'ine göre anahtarlanır; lock dosyası değişmediği sürece aynı cache restore edilir ve bağımlılıklar yeniden indirilmez. Cache'e asla secret veya kimlik bilgisi konulmamalı; restore edilen içerik güvenilmeyen girdi sayılmalı.
Güncelleme (Eylül 2026)
Bu yazının orijinal gövdesi 2026-03-10 tarihli 16.1.x sürüm hattına göre yazıldı. O tarihten bu yana en somut değişiklik Next.js 16.3'te: Turbopack'in dosya-sistemi cache'i artık next build için de varsayılan açık, bazı projelerde CI'da tekrar-build süresini büyük oranda kısaltıyor — Next.js ekibi bunu "5.5x faster builds on CI" olarak ifade etti (Next.js 16.3 duyurusu). Bu kazanç yalnız build cache'i (.next/cache) build'ler arasında korunduğunda geliyor; VPS'te kalıcı working directory bunu otomatik sağlar, GitHub Actions'ın geçici runner'ında ise cache adımı hâlâ gerekli — resmi doküman da CI sağlayıcılarına .next/cache'i build cache olarak yapılandırmalarını öneriyor (Turbopack filesystem cache).
Ayrıca immutable static asset desteği eklendi: bir deployment adapter'ı config.supportsImmutableAssets özelliğini açtığında Next.js, deploy'lar arası paylaşılan, content-addressed statik dosyalar üretiyor — bu, symlink-swap tipi sıfır-kesinti deploy'larda eski/yeni build asset çakışması (asset-skew) riskini azaltan bir mekanizma, ama düz next start ile koşan bir VPS kurulumunda otomatik gelmiyor (immutable static assets). GitHub Actions tarafında çekirdek YAML modeli değişmedi; concurrency ile eşzamanlı deploy'u engelleme hâlâ önerilen yöntem. Tek ek: 2026-05-06'da GitHub, aynı concurrency-group'ta birden fazla bekleyen run'a izin veren opsiyonel queue alanını dokümante etti — queue: single (varsayılan) tek bekleyen run'ı korurken, queue: max en fazla 100 run'ın kuyrukta beklemesine izin verir; queue: max ile cancel-in-progress: true birlikte kullanılamaz, bu kombinasyon workflow doğrulama hatası verir (control workflow concurrency). Next.js 16.3'ün getirdiği diğer cache ve routing değişikliklerini Next.js 16.3'te instant navigation ve cache components yazısında, Edge Runtime'ın kaldırılmasını ise Next.js 16.3'te Edge Runtime'dan Node'a geçiş yazısında ayrıca ele aldım.
Sonuç
Sıfır kesintili bir deploy pipeline'ı, tek bir büyük özellik değil; art arda sıralanmış küçük garantilerin toplamı: concurrency bloğu eşzamanlı build'i engeller, atomik sync dosya senkronunu yarım bırakmaz, restart yalnız health-check'in izniyle tetiklenir, secret'lar hiçbir zaman açık metin olarak dolaşmaz. Bu altı adımı GitHub Actions'ın resmi workflow modeliyle kurduğunda, git push'tan sonra siteye bakma alışkanlığından kurtulursun.
Sunucu kurulumunun temelini önce Next.js'i systemd ve nginx ile production'da ayağa kaldırma yazısıyla atmanı öneririm.
Kaynaklar
- Workflow syntax for GitHub Actions —
on,run-name, workflow dosya konumu için resmi referans. - Dependency caching in GitHub Actions — cache vs artifact ayrımı ve cache güvenlik kuralları.
- Control workflow concurrency —
concurrencyvecancel-in-progressdavranışı. - How to self-host your Next.js application — env var davranışı, reverse proxy önerisi,
Cache-Controlheader'ı. - Using secrets in GitHub Actions —
gh secret set, log maskeleme, OIDC önerisi. - Next.js 16.3 duyurusu — Turbopack FileSystem Cache ve build hızlanması.
- Immutable static assets — deploy'lar arası paylaşılan content-addressed asset mekanizması.
- Turbopack filesystem cache — build cache'in nasıl çalıştığı ve CI sağlayıcılarına yönelik yapılandırma önerisi.

