Next.js'i Kendi Sunucunda Çalıştırmak: Neler Değişir?
Vercel'de her şey çalışırken siteyi kendi sunucuna taşımak gereksiz bir iş gibi görünür. Ama insanları bu işe iten üç tipik sebep var: Hobby planı ticari kullanıma izin vermiyor, iş sunucusuz fonksiyon sınırlarına sıkışıyor, ya da veriyi kendi kontrolündeki bir makinede tutman gerekiyor. Vercel ücretsiz planının gerçek sınırlarını ayrı bir yazıda sayılarla anlatmıştım; bu yazı bir sonraki soruyu cevaplıyor.
Kısa cevap: sandığından az şey değişiyor. Uzun cevap sessizce bozulan birkaç ayrıntıda gizli, ve asıl mesele tam olarak o ayrıntılar.
Asgari gereksinim: tek bir Node.js süreci
Next.js'in kendi dağıtım rehberi bu konuda şaşırtıcı derecede net: Next.js'i çalıştırmak için bir platformun ihtiyacı olan tek şey bir Node.js sunucusudur. Tek bir next start süreci Server Components, ISR, PPR, Cache Components, Server Actions, Proxy ve after() dahil bütün özellikleri doğru şekilde çalıştırır.
Yani "Vercel'den çıkarsan ISR'ı kaybedersin" cümlesi doğru değil. Kaybettiğin özellik değil, o özelliğin arkasındaki altyapıyı senin yerine kuran taraf. CDN, edge compute ve paylaşımlı önbellek gibi ek katmanlar doğruluk için değil, performans ve çok sunuculu tutarlılık için gerekiyor.
Hangi özellik neye ihtiyaç duyuyor
Dokümandaki özellik matrisi, tek sunucuda neyin bedava geldiğini net gösteriyor:
| Özellik | Streaming | Paylaşımlı önbellek |
|---|---|---|
| Server Components | Gerekli | Hayır |
| ISR (zaman tabanlı) | Hayır | Önerilir |
| ISR (on-demand) | Hayır | Önerilir |
| Partial Prerendering | Gerekli | Önerilir |
| Cache Components | Gerekli | Önerilir |
| Proxy / Middleware | Hayır | Hayır |
| Server Actions | Gerekli | Hayır |
after() | Hayır | Hayır |
"Paylaşımlı önbellek önerilir" satırları yalnızca birden fazla sunucu örneği çalıştırıyorsan anlamlı. Tek makinede bu sütunun tamamı görmezden gelinebilir. Buna karşılık "streaming gerekli" satırları tek sunucuda da geçerli ve ters proxy'ni yanlış kurarsan ilk kırılan şey bu olur.
nginx'i önüne koy, ama tamponlamayı kapat
Next.js dokümanı sunucuyu doğrudan internete açmamayı, önüne nginx gibi bir ters proxy koymayı öneriyor: bozuk istekleri, yavaş bağlantı saldırılarını, gövde boyutu sınırını ve hız kısıtlamayı orada halledersin, render sunucusu da işine bakar. Docker Compose ile Next.js ve Django production kurulumu yazısında bu düzenin tamamını kurmuştum.
Sorun şu: nginx yukarı akıştan gelen yanıtı varsayılan olarak tamponlar. Tamponlanan bir yanıt streaming değildir. Sayfa yine çalışır ama parça parça değil, tek seferde gelir; Suspense sınırlarının erken gösterdiği iskelet ekranlar kullanıcıya hiç ulaşmaz ve ilk baytın gecikmesi tam render süresine eşitlenir. Yani bir şey patlamaz, sadece hızın kaybolur ve nedenini bulamazsın.
Next.js bunu yanıt başlığıyla çözmeyi öneriyor:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*{/}?',
headers: [
{
key: 'X-Accel-Buffering',
value: 'no',
},
],
},
]
},
}Zincirin tamamının streaming'e izin vermesi gerekiyor. Araya bir yük dengeleyici koyduysan onun da parçalı aktarımı (chunked transfer encoding) ya da HTTP/2 akışını desteklemesi, yanıtı tamponlamaması şart.
ISR önbelleği artık senin diskinde
Vercel'de ISR önbelleğinin nerede durduğunu hiç düşünmezsin. Kendi sunucunda düşünmen gerekiyor.
Next.js, üretilen statik sayfaları ve yeniden doğrulanan içeriği varsayılan olarak sunucu örneğinin yerel diskinde ve bellekte tutar; bellek tarafı varsayılan olarak 50 MB ile sınırlıdır. Kalıcı diski olan tek bir next start süreci için bu kendiliğinden çalışır, ekstra yapılandırma gerekmez.
İki durumda bozulur:
Geçici (ephemeral) konteynerler. Her dağıtımda dosya sistemi sıfırlanıyorsa önbellek de sıfırlanır. İşlevsel bir hata değil ama her dağıtımdan sonra ilk ziyaretçiler soğuk render bedelini öder. Önbellek dizinini kalıcı bir birime bağlamak bunu çözer.
Birden fazla örnek. Her pod kendi önbelleğini tutar. revalidateTag() çağrısı yalnızca çağrının düştüğü örneği temizler; diğerleri eski içeriği servis etmeye devam eder. Çözüm, paylaşımlı bir depoya yazan özel bir önbellek işleyicisi:
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // varsayılan bellek içi önbelleği kapat
}Etiket geçersizleştirmesinin örnekler arasında yayılması için işleyicide refreshTags() metodunu da uygulaman gerekiyor; bu metot her istekten önce çağrılır ve etiket durumunu paylaşımlı depodan senkronize eder. Next.js ISR ve on-demand revalidation yazısında anlattığım revalidatePath akışı tek sunucuda aynen çalışır, çoklu sunucuda bu ek işi ister.
Pratik tavsiyem: gerçekten yatay ölçeklenmiyorsan bu katmanı hiç kurma. Kalıcı diski olan tek bir süreç, karmaşık bir Redis önbellek işleyicisinden çok daha az bakım ister.
Bir de build tarafı var: Next.js derlemeler arasında paylaşılan bir önbelleği .next/cache altında tutar. CI'ın bu dizini derlemeler arasında saklamıyorsa her build sıfırdan başlar ve gereksiz yere uzar.
Önüne CDN koyacaksan rsc başlığına dokunma
Kendi sunucunun önüne bir CDN koyanların en sık düştüğü tuzak burada.
Next.js, rota tipine göre standart Cache-Control başlıkları üretir: statik sayfalar için bir yıllık s-maxage, ISR sayfaları için s-maxage artı stale-while-revalidate, dinamik sayfalar için önbelleklemeyi tamamen kapatan bir başlık. Bu direktiflere saygı duyan bir CDN statik ve ISR sayfalarını uçta önbellekleyebilir.
Ama iki şeyi bilmen gerekiyor.
On-demand yenileme CDN'e ulaşmaz. revalidateTag() ve revalidatePath() Next.js sunucusunun önbelleğini geçersiz kılar; CDN kendi kopyasını s-maxage süresi dolana kadar servis etmeye devam eder. İçeriği saniyeler içinde güncellemek istiyorsan yenileme çağrısının yanında CDN'in temizleme API'sini de çağırman gerekir, hem HTML hem RSC varyantı için.
rsc başlığı istemciden sunucuya ulaşmalı. Bu başlık sunucuya HTML yerine React Server Components yükü dönmesini söyler. CDN onu düşürürse istemci tarafı yönlendirici RSC beklerken HTML alır ve istemci içi gezinme kırılır, her tıklama tam sayfa yüklemesine döner.
Aynı sebeple _rsc sorgu parametresinin önbellek anahtarında yer alması gerekir; bu parametre HTML ile RSC varyantlarını birbirinden ayıran ayırt edici değerdir. Bazı CDN'ler sorgu parametrelerini önbellek anahtarından varsayılan olarak siler — o ayarı kapatman lazım.
Bu, Vercel'de hiç karşılaşmadığın bir sınıf hata. Belirtisi de yanıltıcıdır: site açılır, sayfalar gelir, ama gezinme tuhaf şekilde yavaştır.
Görsel optimizasyonu ve sharp
next/image üzerinden görsel optimizasyonu, next start ile çalışırken sıfır yapılandırmayla kendi sunucunda da çalışır. Next.js'in tek ek bağımlılığı sharp paketidir.
İki ayrıntı:
output: "standalone" kullanıyorsan — Docker imajını küçültmek için kullanmalısın — native ikili dosyaların izleme çıktısına dahil edildiğinden emin ol:
// next.config.js
module.exports = {
outputFileTracingIncludes: {
'/*': ['node_modules/sharp/**/*'],
},
}glibc tabanlı Linux dağıtımlarında sharp aşırı bellek tüketebilir; bellek ayırıcı tarafında ek yapılandırma gerekebilir. Küçük bir VPS'te bu, "sunucu neden birden şişti" sorusunun cevabı olabilir.
Ortam değişkenleri build'e gömülür
Vercel'de ortam değişkenini panelden değiştirip yeniden dağıtırsın. Kendi sunucunda da mantık aynı ama sonucu daha çok canını yakar: NEXT_PUBLIC_ önekli değişkenler next build sırasında JavaScript paketinin içine gömülür. Yani aynı imajı staging ve production'a farklı değerlerle taşıyamazsın; her ortam için ayrı build gerekir.
Tek imajı birden çok ortama terfi ettirmek istiyorsan değeri sunucu tarafında, çalışma zamanında oku. Dinamik render'a geçtiğini açıkça belirtmek için connection() kullanılır:
import { connection } from 'next/server'
export default async function Component() {
await connection()
const value = process.env.MY_VALUE
// ...
}Birden fazla sunucuya çıkarsan
Tek makinede kalıyorsan bu bölümü atla. Yük dengeleyici arkasında iki veya daha fazla örnek çalıştıracaksan üç ayar zorunlu hale geliyor.
Server Actions şifreleme anahtarı. Next.js, Server Function kapanış değişkenlerini istemciye göndermeden önce şifreler ve anahtarı varsayılan olarak her build için yeniden üretir. Farklı anahtarlara sahip iki örnek birbirinin verisini çözemez; sonuç "Failed to find Server Action" hatasıdır. Bütün örneklerin aynı anahtarı kullanması gerekir:
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=uretilmis-anahtar next buildAnahtar base64 kodlu ve geçerli bir AES uzunluğunda olmalı: 16, 24 veya 32 bayt. Next.js varsayılan olarak 32 baytlık anahtar üretir.
Sürüm kayması. Aşamalı dağıtım sırasında bazı kullanıcılar eski, bazıları yeni sürümden dosya ister. Eksik JavaScript dosyaları, tanınmayan Server Function kimlikleri ve bozuk gezinmeler bundan çıkar. deploymentId ayarlandığında Next.js statik dosyalara dağıtım kimliğini ekler, gezinme isteklerinde başlıkla karşılaştırır ve uyuşmazlık görürse istemci içi gezinme yerine tam sayfa yenilemesi tetikler:
// next.config.js
module.exports = {
deploymentId: process.env.DEPLOYMENT_VERSION,
}Tutarlı build kimliği. Her ortam için ayrı ayrı build alıyorsan konteynerlerin aynı kimliği paylaşması için generateBuildId ile sabitle; git commit hash'i tipik seçimdir.
Kapatırken acele etme
Sunucuyu durdururken SIGINT veya SIGTERM gönder ve bekle. Next.js işlemekte olan istekleri bitirir ve bekleyen after() geri çağrılarını çalıştırıp öyle çıkar. Dokümanın önerdiği boşaltma süresi 10-30 saniye.
Bu ayrıntıyı atlayan konteyner kurulumları e-posta gönderimi, analitik yazımı, log gönderimi gibi after() içine taşınan işleri sessizce yarıda keser. Hata mesajı görmezsin; iş sadece yapılmaz.
Vercel'in senin yerine yaptığı işler
Teknik olarak hiçbir Next.js özelliğini kaybetmiyorsun. Kaybettiğin şey operasyon:
- HTTPS sertifikası. Alma ve yenileme artık senin işin. AWS Lightsail üzerinde Django deploy yazısında Let's Encrypt ile bu düzeni kurmuştum; Next.js tarafında da aynı yaklaşım geçerli.
- Küresel dağıtım. Tek bölgede tek sunucu, uzaktaki kullanıcılar için daha yavaştır. Önüne CDN koyabilirsin ama yukarıdaki başlık kurallarına uyman şartıyla.
- Önizleme dağıtımları. Her dal için otomatik adres yok. İstiyorsan CI tarafında kendin kurarsın.
- Geri alma. "Önceki dağıtıma dön" düğmesi yok. İmajları etiketleyip saklaman ve geri dönüş yolunu önceden denemen gerekir.
- Zamanlanmış işler. Cron yönetimi sistem cron'una veya bir görev kuyruğuna geçer.
- Log ve metrik. Sunucu logları senin diskinde birikir. Döndürme (rotation) ayarlamazsan bir gün disk dolar.
Bu listenin hiçbir maddesi tek başına zor değil. Hepsi birlikte, hiç bitmeyen bir bakım yükü demek. Taşıma kararını verirken karşılaştırman gereken şey aylık sunucu ücreti değil, bu yük.
Ne zaman taşımalı, ne zaman taşımamalı
Taşımayı destekleyen durumlar nettir: ticari bir site Hobby planında duramaz, uzun süren arka plan işleri sunucusuz fonksiyon tavanına sığmaz, veri ikamet kuralları makinenin yerini belirliyor olabilir, ya da zaten kendi sunucunda bir Django veya PostgreSQL çalıştırıyorsundur ve her şeyi tek yerde toplamak istiyorsundur.
Taşımamayı destekleyen tek bir durum var ama güçlü: kişisel bir blog veya portföy sitesi ücretsiz planın sınırlarına yaklaşmaz bile. Böyle bir siteyi kendi sunucuna almak, ayda birkaç dolar tasarruf için kendine bir operasyon işi satın almaktır.
Emin değilsen hisle değil ölçüyle karar ver: mevcut kullanımına bak, sınırın neresinde olduğunu gör, kararı ondan sonra ver.
Özet
Next.js kendi sunucunda eksiksiz çalışır; bir Node.js süreci yeter. Ama Vercel'in arkada hallettiği dört şeyi devralırsın: ters proxy'de streaming'i açık tutmak, ISR önbelleğinin nerede durduğunu bilmek, CDN koyduysan rsc başlığını ve _rsc parametresini korumak, kapanışta sunucuya boşaltma süresi tanımak.
Bunların hiçbiri gizemli değil ama hiçbiri de kendiliğinden olmuyor — ve dördü de patlamak yerine sessizce yavaşlayarak ya da eksik çalışarak bozulduğu için, sonradan teşhis etmek kurmaktan zordur. Taşıyacaksan bu dört maddeyi ilk gün halledip öyle devam et.