Next.js'i kendi sunucunda yayınlamak: Dokploy ve Docker ile

Next.js'i kendi sunucunda yayınlamak: Dokploy ve Docker ile

Vercel'den çıkmak ne zaman mantıklı? output standalone, çok aşamalı Dockerfile, şema göçlerini neden derlemede değil açılışta yapman gerektiği ve SIGTERM'i yutan tek satır.

Furkan Özay·2 Eylül 2026·6 dk okuma

Vercel çoğu proje için doğru cevap: sıfır yapılandırma, otomatik önizleme, küresel ağ. Bu yazı onu bırakmanı önermiyor. Ama bırakman gereken bir an varsa — sunucu maliyeti öngörülemez hâle geldiyse, veritabanı aynı makinede olmalıysa ya da veri yurt içinde kalmak zorundaysa — işin gerçekte neye benzediğini anlatıyor.

Aşağıdakiler student.czay.dev'i kendi sunucumda yayınlarken karşılaştığım şeyler. Kolay kısmı Docker imajı yazmak; zor kısmı kapsayıcının açılış sırası.

Dokploy ne yapıyor?

Kendi sunucuna kurduğun bir PaaS: git deposunu bağlıyorsun, her push'ta imajı derleyip yayına alıyor. Arkada Docker ve Traefik var — alan adı yönlendirmesi ve Let's Encrypt sertifikası otomatik. Yani Vercel'in yaptığı işin büyük kısmını tek bir VPS üzerinde yapıyor.

💬Ne kazanıyorsun, ne kaybediyorsun

Kazanç: Sabit ve öngörülebilir maliyet, veritabanı ile uygulamanın aynı ağda olması, veri konumu üzerinde tam kontrol, çalışma süresi sınırı yok.

Kayıp: Küresel kenar ağı yok, ölçekleme senin işin, sunucunun güncellemesi ve yedeği senin sorumluluğun. Tek bir makine tek bir arıza noktası.

1. output: "standalone"

Bu satır olmadan imajın gereksiz yere yüzlerce megabayt oluyor.

TypeScript
// next.config.ts
const nextConfig = {
	output: "standalone" as const,
};

Next.js böylece yalnız gerçekten kullanılan node_modules dosyalarını toplayıp .next/standalone altında kendi kendine çalışan bir klasör üretiyor. Çalıştırma imajına node_modules'ın tamamını kopyalaman gerekmiyor.

Monorepo'daysan bir satır daha lazım:

TypeScript
import path from "node:path";
 
const nextConfig = {
	output: "standalone" as const,
	outputFileTracingRoot: path.join(import.meta.dirname, "../.."),
};

Bunu atlarsan imaj derleniyor ama çalışma anında modül bulunamadı hatasıyla patlıyor: pnpm bağımlılıkları depo köküne hoist ediyor, Next.js ise varsayılan olarak yalnız uygulama klasörüne bakıyor.

2. Çok aşamalı Dockerfile

Üç aşama: bağımlılıklar, derleme, çalıştırma.

🧪Bağımlılık aşaması
dockerfile
FROM node:22-alpine AS base
RUN corepack enable
WORKDIR /repo
 
FROM base AS deps
# Önce yalnız manifest dosyaları: kaynak kod değiştiğinde bu katman
# önbellekten geliyor ve pnpm install tekrar çalışmıyor.
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/student/package.json apps/student/
COPY packages/ui/package.json packages/ui/
 
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
    pnpm install --frozen-lockfile --filter student...

İki ayrıntı derleme süresini dakikalardan saniyelere indiriyor:

  • Manifest'leri önce kopyala. Docker katman önbelleği dosya değişimine bakıyor. Kaynak kodu pnpm install'dan önce kopyalarsan her kod değişikliğinde bütün bağımlılıklar baştan kuruluyor.
  • --mount=type=cache ile pnpm deposunu derlemeler arasında sakla.

3. Çalıştırma imajı: küçük ve yetkisiz

dockerfile
FROM node:22-alpine AS runner
WORKDIR /app
 
ENV NODE_ENV=production
ENV PORT=3003
ENV HOSTNAME=0.0.0.0
 
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
 
COPY --from=build --chown=nextjs:nodejs /repo/apps/student/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /repo/apps/student/.next/static ./apps/student/.next/static
COPY --from=build --chown=nextjs:nodejs /repo/apps/student/public ./apps/student/public
 
USER nextjs
EXPOSE 3003

HOSTNAME=0.0.0.0 şart: varsayılan localhost olursa sunucu yalnız kapsayıcının içinden erişilebilir oluyor ve Traefik bağlanamıyor. "Kapsayıcı çalışıyor ama 502 alıyorum" hatasının en sık sebebi bu.

USER nextjs de öyle: kapsayıcıyı root çalıştırmak, bir açıkta zararın büyüklüğünü belirleyen şey.

⚠️Yalnız temiz klonda çıkan hata

public/ klasörüm boştu ve git boş klasörleri izlemiyor. Yerelde klasör durduğu için her şey çalışıyordu; sunucudaki temiz klonda klasör hiç yoktu ve yukarıdaki COPY "not found" ile patlıyordu. Derleme aşamasına tek satır:

dockerfile
RUN mkdir -p apps/student/public

Yerel ortamın durumu ile temiz klonun durumu arasındaki fark, dağıtımda yakalanan hataların iyi bir kısmını açıklıyor.

4. Asıl mesele: açılış sırası

Uygulamanın bir veritabanı varsa şema göçlerini ne zaman uygulayacaksın?

Derleme sırasında değil. İmaj derlenirken veritabanına erişimin yok ve olmamalı da: aynı imaj başka bir veritabanına da açılabilmeli. Ayrıca Dokploy'un yeniden başlatması, ölçeklemesi ve geri alması hep aynı imajı kullanıyor — şemayı imaja gömemezsin.

Doğrusu bir açılış betiği:

🧪docker-entrypoint.sh
Bash
#!/bin/sh
set -e
 
cd /app/apps/student
 
echo "[giris] şema göçleri uygulanıyor…"
node scripts/migrate.cjs
 
# Bu ikisi dağıtımı DÜŞÜRMÜYOR
set +e
node scripts/seed-content.cjs --dagitim
node scripts/create-admin.cjs --dagitim
set -e
 
echo "[giris] sunucu açılıyor…"
cd /app
exec node apps/student/server.js

Buradaki üç karar da bilinçli:

Hangi hata dağıtımı düşürmeli? Şema göçü başarısızsa kapsayıcı hiç açılmamalı — eski şemayla açılan sunucu ilk istekte patlar ve sebebi görünmez olur. İçerik tohumu ya da yönetici hesabı başarısızsa dağıtım devam etmeli: içeriksiz bir panel açılabilir, ikisi de sonradan elle düzeltilebilir.

set -e / set +e bu ayrımı kuran şey: ilk blokta herhangi bir hata betiği durduruyor, ikinci blokta durdurmuyor.

🚨exec olmadan Dokploy kapsayıcıyı öldürüyor

Son satırdaki exec süslemesi değil. exec olmadan Node, kabuk betiğinin alt süreci olarak çalışıyor ve Docker'ın gönderdiği SIGTERM kabuğa gidip Node'a hiç ulaşmıyor.

Sonuç: durdurma isteği zaman aşımına düşüyor, Docker kapsayıcıyı SIGKILL ile öldürüyor. Her dağıtımda uygulaman düzgün kapanmıyor — açık veritabanı bağlantıları ve yarım kalan istekler öyle kalıyor.

exec süreci değiştiriyor: Node PID 1 oluyor ve sinyali doğrudan alıyor.

5. Sağlık denetimi göçleri beklemeli

dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3003)+'/giris').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

--start-period=40s kritik: şema göçleri sürerken uygulama henüz cevap vermiyor. Bu süre olmadan Dokploy kapsayıcıyı "sağlıksız" sayıp yeniden başlatıyor, göçler yeniden başlıyor ve bir döngüye giriyorsun.

Denetimin gerçek bir sayfayı istemesi de bilinçli: kök adres yönlendirme dönebiliyor ve uygulamanın çalıştığını kanıtlamıyor.

6. Ortam değişkenleri: derleme mi, çalışma anı mı?

Bu ayrım Docker'da çok daha çok canını yakıyor.

NEXT_PUBLIC_ derleme anında gömülür

NEXT_PUBLIC_ önekli değişkenler istemci paketine derleme sırasında yazılıyor. Kapsayıcıya çalışma anında verirsen hiçbir etkisi olmuyor — imajın içindeki JavaScript'te eski değer duruyor.

Bunları Dokploy'da derleme argümanı (build arg) olarak geçmen gerekiyor. Değiştirdiğinde de yeniden derleme şart; yeniden başlatma yetmiyor.

Sunucu tarafı değişkenleri (DATABASE_URL, API anahtarları) ise tam tersi: çalışma anında verilmeli ve imaja asla girmemeli.

.dockerignore bu yüzden önemli:

plaintext
node_modules
**/node_modules
**/.next
**/.turbo
.git
 
# Ortam dosyaları imaja girmez
.env
.env.*
**/.env
**/.env.*
!**/.env.example

.env dosyasını imaja kopyalarsan gizli anahtarların imajın katmanlarında kalıyor — imajı silsen bile kayıtta duruyor.

Kontrol listesi

📋Yayına almadan önce
  • output: "standalone" açık mı? Monorepo'daysan outputFileTracingRoot da?
  • HOSTNAME=0.0.0.0 verildi mi? (Yoksa 502)
  • Kapsayıcı root olarak mı çalışıyor? (Çalışmamalı)
  • Şema göçü açılışta mı, derlemede mi? (Açılışta)
  • Son satırda exec var mı? (Yoksa her dağıtımda SIGKILL)
  • --start-period göçler için yeterli mi?
  • NEXT_PUBLIC_* build arg olarak mı geçiyor?
  • .env .dockerignore içinde mi?
  • Veritabanı yedeği kurulu mu? (Vercel'de bu senin işin değildi, artık öyle)

Son madde en çok atlanan. Kendi sunucuna geçmek maliyeti düşürüyor ama sorumluluğu sana devrediyor: güncelleme, yedek ve izleme artık senin işin. Bunu yapacak zamanın yoksa Vercel'de kalmak daha ucuz — para değil, zaman olarak.

İlgili yazı: Turborepo ile monorepo yönetimi

Bu yazıyı faydalı bulduysan paylaşabilirsin.

Furkan Özay

Yazılım geliştirici ve eğitmen; Next.js, React ve altyapı üzerine yazıyor.

czay.dev

Bunu da oku

Yorumlar