Next.js 16 Self-Hosting Rehberi: Docker, Standalone Çıktı ve Üretim Dağıtımı

Next.js 16 self-hosting için tam rehber: standalone output, çok aşamalı Docker imajı, Redis ISR cache handler, Nginx ters proxy, graceful shutdown ve Kubernetes deployment.

Next.js 16 Self-Hosting: Docker Rehberi (2026)

Güncellendi: 8 Haziran 2026

Next.js 16 self-hosting, uygulamanızı Vercel dışında herhangi bir Node.js sunucusunda (Docker container, VPS, Kubernetes veya bare-metal) çalıştırmak anlamına gelir ve next.config.ts dosyasında output: 'standalone' ayarını etkinleştirerek minimal bir üretim bundle'ı üretmekle başlar. Geçen ay bir müşteri projesini Vercel'den kendi Hetzner sunucusuna taşırken aşağıda anlatacağım hemen hemen her tuzağa bizzat takıldım; bu rehberde çok aşamalı Docker imajı oluşturmaktan ISR önbelleğini Redis ile kalıcı hale getirmeye, Nginx ters proxy yapılandırmasına ve düzgün (graceful) kapatma akışına kadar üretim için ihtiyacınız olan her şeyi adım adım anlatıyorum.

  • output: 'standalone' modu, node_modules'u izleyip sadece gerekli dosyaları kopyalar; final Docker imajı 150-250 MB seviyesine düşer.
  • sharp paketini üretim bağımlılığı olarak kurmazsanız Next.js 16 Image Optimization yavaş, CPU-yoğun bir JS fallback'a düşer.
  • ISR ve revalidateTag'ı çoklu instance üzerinde çalıştırmak için özel bir cacheHandler (Redis tabanlı) gerekir, aksi halde her pod kendi cache'ini tutar.
  • Next.js 16 varsayılan olarak çoklu çekirdek kullanmaz; PM2 cluster modu veya Kubernetes replica'ları ile yatay ölçekleyin.
  • Nginx'de X-Forwarded-* başlıklarını forward etmeden request.headers.get('host') ve middleware geolocation hatalı sonuç verir.
  • Graceful shutdown için Node.js sinyal yakalama (SIGTERM) zorunludur, aksi takdirde rolling deploy sırasında istekler düşer.

Next.js 16 standalone output nedir ve neden gerekli?

Standalone output, Next.js'in build sırasında uygulamanızın ve node_modules ağacının bağımlılık grafiğini izleyerek (file tracing) yalnızca runtime için gereken dosyaları .next/standalone/ klasörüne kopyaladığı bir derleme modu. Tipik bir Next.js projesinin node_modules dizini 400-800 MB iken, standalone çıktısı 50-80 MB'a iner. Üretim Docker imajını küçültmenin ve cold-start süresini düşürmenin en etkili yolu da bu.

Resmi Next.js dağıtım dokümantasyonu bu modu özellikle Docker ve container tabanlı ortamlar için öneriyor. Aktif etmek için next.config.ts dosyasında tek satır yeterli:

// next.config.ts
import type { NextConfig } from 'next'

const config: NextConfig = {
  output: 'standalone',
  // Monorepo kullanıyorsanız izleme kök dizinini açıkça belirtin:
  // outputFileTracingRoot: path.join(__dirname, '../../'),
  experimental: {
    // Next.js 16: Cache Components açıksa
    cacheComponents: true,
  },
}

export default config

Build tamamlandığında .next/standalone/server.js çalıştırılabilir bir Node.js sunucusudur, yani next start'a ihtiyacınız yok. Tek püf nokta şu: .next/static ve public/ klasörleri ayrıca kopyalanmalı; bunlar tracing'e dahil edilmez çünkü browser tarafından doğrudan sunuluyor. İlk denememde bu adımı atlamıştım ve üretimde 404 hataları ile eksik CSS gördüm. Self-hosting'de en sık karşılaşılan problem bu.

Çok aşamalı Docker imajı nasıl oluşturulur?

Üretim için optimize edilmiş bir Docker imajı üç aşamadan oluşur: deps (bağımlılıkları yükler ve cache'ler), builder (kaynak kodu derler) ve runner (yalnızca standalone çıktısını içerir). Bu kalıp, kodda küçük bir değişiklik yaptığınızda npm ci'nin tekrar çalışmamasını sağlar ve final imajı 200 MB civarında tutar.

# Dockerfile
FROM node:22-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --include=dev

FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

RUN addgroup --system --gid 1001 nodejs \
 && adduser  --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

Bir şeyi kesinlikle atlamamalısınız: HOSTNAME=0.0.0.0. Varsayılan localhost binding container dışından erişime izin vermez ve docker run sonrası "connection refused" hatası alırsınız (bunu da bizzat ilk dakikada yaşadım). NEXT_TELEMETRY_DISABLED=1 opsiyonel ama CI ortamlarında network trafiğini azaltır.

Bir de .dockerignore dosyası var. İmaj boyutu kadar önemli; node_modules, .next, .git, .env*.local, coverage ve README.md gibi dosyaları dışlamalısınız. Bu olmadan build context yüzlerce MB'a şişer ve docker build dakikalarca sürebilir.

Image Optimization ve sharp kurulumu

Next.js 16, next/image bileşeni ile WebP/AVIF dönüşümü ve sunucu tarafında yeniden boyutlandırma yapar. Vercel üzerinde bu işlem yönetilen bir image servisi tarafından yürütülür; self-hosted bir sunucuda ise Next.js doğrudan Node.js process içinde sharp kütüphanesini kullanır. sharp kurulu değilse Next.js 16 ihtiyaç anında uyarı verir ve hızlı bir JS fallback'ına döner; bu fallback'ın CPU maliyeti üretim trafiğinde son derece yüksek.

npm install sharp

Alpine imajında sharp'ın native binary'leri için libc6-compat paketini eklemeniz gerekir (yukarıdaki Dockerfile'da mevcuttur). Ayrıca uzak alanlardaki görselleri optimize edeceksen images.remotePatterns'ı tanımlamalısın:

// next.config.ts
const config: NextConfig = {
  output: 'standalone',
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' },
    ],
    // 60 saniye cache TTL (varsayılan)
    minimumCacheTTL: 60,
    // AVIF desteklenmiyorsa WebP'ye düşer
    formats: ['image/avif', 'image/webp'],
  },
}

ISR ve özel cache handler (Redis)

Tek instance çalışan bir Next.js sunucusunda ISR (Incremental Static Regeneration) sorunsuz çalışır; yeniden oluşturulan sayfalar .next/cache dizinine yazılır. Birden fazla instance'ı yük dengeleyici arkasında çalıştırdığınızda ise her instance kendi cache'ini tutar, revalidateTag veya revalidatePath çağrısı yalnızca o isteği işleyen instance'ı günceller ve diğerleri eski içeriği sunmaya devam eder. Çözüm: paylaşımlı bir cache handler tanımlamak.

Caching ve revalidation stratejilerinin tam karşılaştırması için Next.js App Router önbellekleme ve revalidation rehberini okumanızı öneririm; burada doğrudan dağıtım tarafına odaklanacağız.

// cache-handler.mjs
import { createClient } from 'redis'

const client = createClient({ url: process.env.REDIS_URL })
await client.connect()

export default class RedisCacheHandler {
  async get(key) {
    const value = await client.get(`next:${key}`)
    return value ? JSON.parse(value) : null
  }

  async set(key, data, ctx) {
    await client.set(
      `next:${key}`,
      JSON.stringify({ value: data, lastModified: Date.now() }),
      ctx?.revalidate ? { EX: ctx.revalidate } : undefined,
    )
    // Tag -> key mapping
    for (const tag of ctx?.tags ?? []) {
      await client.sAdd(`next:tag:${tag}`, key)
    }
  }

  async revalidateTag(tag) {
    const keys = await client.sMembers(`next:tag:${tag}`)
    if (keys.length) {
      await client.del(keys.map((k) => `next:${k}`))
      await client.del(`next:tag:${tag}`)
    }
  }
}
// next.config.ts
const config: NextConfig = {
  output: 'standalone',
  cacheHandler: require.resolve('./cache-handler.mjs'),
  cacheMaxMemorySize: 0, // bellek içi cache'i devre dışı bırak (Redis kullanılacak)
}

Bu yapı ile revalidateTag('products') çağrısı tüm pod'lardaki ilgili sayfaları aynı anda geçersiz kılar. Edge Runtime tarafında middleware ile kimlik doğrulama veya yönlendirme yapıyorsanız Next.js middleware ve Edge Runtime rehberindeki kalıpları da gözden geçirin; cache invalidation ve middleware yönlendirmesi sık sık birlikte çalışır.

Nginx ters proxy ve başlık yapılandırması

Self-hosted Next.js'i doğrudan internete açmak yerine TLS sonlandırma, statik dosya cache'leme ve rate limiting için Nginx (veya Caddy/Traefik) arkasına koymak standarttır. Doğru başlık yapılandırması olmadan request.headers.get('host'), x-forwarded-for tabanlı IP geolocation ve middleware yönlendirmeleri yanlış sonuç verir. Nginx HTTP proxy modülü dokümantasyonu tüm yönergelerin detayını içerir.

# /etc/nginx/sites-available/nextjs
upstream nextjs_upstream {
  server 127.0.0.1:3000;
  keepalive 64;
}

server {
  listen 443 ssl http2;
  server_name example.com;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  # _next/static dosyaları immutable, doğrudan Nginx'ten servis et
  location /_next/static/ {
    proxy_cache_valid 200 365d;
    add_header Cache-Control "public, max-age=31536000, immutable";
    proxy_pass http://nextjs_upstream;
  }

  location / {
    proxy_http_version 1.1;
    proxy_set_header   Upgrade           $http_upgrade;
    proxy_set_header   Connection        "upgrade";
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Host  $host;
    proxy_read_timeout 60s;
    proxy_pass         http://nextjs_upstream;
  }
}

Graceful shutdown ve sağlık kontrolü

Container orchestrator'lar (Kubernetes, Docker Swarm) deploy sırasında pod'a önce SIGTERM gönderir, ardından bir terminationGracePeriod (varsayılan 30 saniye) sonra SIGKILL ile öldürür. Next.js'in standalone server.js'i bu sinyali handle ederse, devam eden HTTP istekleri bitirildikten sonra port serbest bırakılır ve hiçbir istek düşmez.

Next.js 16, standalone modda SIGTERM sinyalini varsayılan olarak yakalar ve aktif bağlantıları boşaltmaya çalışır. Ancak kendi server.js sarmalayıcınızı kullanıyorsanız bunu manuel yapmanız gerekir:

// custom-server.js
import { createServer } from 'node:http'
import next from 'next'

const app = next({ dev: false, hostname: '0.0.0.0', port: 3000 })
const handle = app.getRequestHandler()

await app.prepare()
const server = createServer((req, res) => handle(req, res))
server.listen(3000)

const shutdown = (signal) => {
  console.log(`${signal} alındı, bağlantılar kapatılıyor...`)
  server.close(() => process.exit(0))
  setTimeout(() => process.exit(1), 25_000).unref()
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))

Sağlık kontrolü için app/api/health/route.ts altında küçük bir Route Handler ekleyin; orchestrator bu endpoint'e liveness/readiness probe gönderir:

// app/api/health/route.ts
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

export async function GET() {
  return Response.json({ status: 'ok', uptime: process.uptime() })
}

Ortam değişkenleri ve sırlar (secrets)

Next.js'te ortam değişkenlerinin iki kategorisi vardır: build-time embed edilen NEXT_PUBLIC_* değişkenleri ve runtime'da okunan sunucu değişkenleri. NEXT_PUBLIC_API_URL gibi bir değer npm run build çalıştığında client bundle'ına yazılır; yani Docker imajını birden fazla ortama (staging/prod) dağıtmak istiyorsanız bu yaklaşım bozulur.

Üretim için pratik: build sırasında yalnızca public olmayan değerleri verin (örn. DATABASE_URL), NEXT_PUBLIC_* değerlerini ise her ortam için ayrı build üretin VEYA runtime-config kullanın. Sırları doğrudan Dockerfile'a yazmayın; BuildKit secret mount kullanın:

# docker build sırasında secret enjekte et
docker build \
  --secret id=db_url,src=./db_url.txt \
  -t myapp:latest .
# Dockerfile (builder stage içinde)
RUN --mount=type=secret,id=db_url \
    DATABASE_URL="$(cat /run/secrets/db_url)" \
    npm run build

Kubernetes ortamında ise Secret kaynağı ile envFrom kullanın; ConfigMap public, Secret hassas değerler için. Bir not: Edge Runtime üzerinde middleware çalıştırıyorsanız, process.env erişimi build-time inlining ile sınırlı kalabilir.

Yatay ölçekleme: PM2 cluster ve Kubernetes

Node.js tek thread çalıştığı için çok çekirdekli sunuculardan yararlanmak istiyorsanız ya birden fazla container instance'ı çalıştırın ya da PM2'nin cluster modunu kullanın. PM2, fork-temelli child process spawn eder ve aralarında round-robin yük dengeler:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'nextjs',
      script: '.next/standalone/server.js',
      instances: 'max',          // CPU çekirdek sayısı kadar
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
        HOSTNAME: '0.0.0.0',
      },
      // SIGTERM gönderildikten sonra graceful shutdown için ekstra süre
      kill_timeout: 25_000,
    },
  ],
}

Kubernetes'te ise her pod tek instance çalışır ve HorizontalPodAutoscaler ile CPU/memory metriklerine göre ölçeklenir. Tipik bir deployment manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs
spec:
  replicas: 3
  selector:
    matchLabels: { app: nextjs }
  template:
    metadata:
      labels: { app: nextjs }
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: nextjs
          image: ghcr.io/example/myapp:1.0.0
          ports: [{ containerPort: 3000 }]
          readinessProbe:
            httpGet: { path: /api/health, port: 3000 }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /api/health, port: 3000 }
            initialDelaySeconds: 15
            periodSeconds: 20
          resources:
            requests: { cpu: '250m', memory: '256Mi' }
            limits:   { cpu: '1000m', memory: '512Mi' }

Sıkça Sorulan Sorular

Next.js'i Vercel olmadan dağıtmak mümkün mü?

Evet, Next.js tamamen open source bir Node.js framework'üdür ve output: 'standalone' modu özellikle bu kullanım senaryosu için tasarlanmıştır. Docker, VPS (DigitalOcean, Hetzner, Linode), AWS ECS/Fargate, Google Cloud Run veya kendi bare-metal sunucunuzda sorunsuz çalışır. Yalnızca Image Optimization ve ISR için bazı ek yapılandırmalar gerekir.

Next.js Docker imajı tipik olarak ne kadar büyük olur?

Standalone output ve çok aşamalı build kullanarak Alpine tabanlı bir imajı 150-250 MB seviyesinde tutmak mümkündür. output: 'standalone' olmadan tüm node_modules kopyalandığı için imaj kolayca 1 GB'ı aşar. sharp ekleyince yaklaşık 30 MB daha eklenir.

Self-hosting için minimum sunucu özellikleri nelerdir?

Düşük trafikli bir Next.js 16 uygulaması için 1 vCPU ve 512 MB RAM yeterlidir; build aşaması için en az 2 GB RAM önerilir, aksi takdirde Turbopack OOM hatası verir. Üretimde 2 vCPU + 2 GB RAM, yüzlerce eşzamanlı istek için rahat bir başlangıçtır.

ISR self-hosted ortamda nasıl çalışır?

Tek instance'ta ISR doğrudan çalışır; yeniden oluşturulan sayfalar .next/cache'e yazılır. Çoklu instance senaryolarında cacheHandler seçeneği ile Redis veya başka bir paylaşımlı store kullanmanız gerekir, aksi halde her instance kendi izole cache'ini tutar ve revalidateTag yalnızca isteği alan instance'ı günceller.

Next.js standalone modunda next start kullanılır mı?

Hayır. Standalone build, .next/standalone/server.js adında çalıştırılabilir bir Node.js dosyası üretir ve doğrudan node server.js ile başlatılır. next start tüm node_modules'a ihtiyaç duyar ve standalone'un boyut avantajını ortadan kaldırır.

Editorial Team
Yazar Hakkında Editorial Team

Our team of expert writers and editors.