10 — Dağıtım
Bu sayfada
Bu belge bir JSkelet uygulamasını yayına almayı anlatır: prod build ve start akışı, ayarlanması gereken ortam değişkenleri, çalışan bir Docker kurulumu, ters proxy ve trust proxy notları, sağlık kontrolü ucunun nasıl eklendiği ve ölçekleme sırasında önbelleğin nasıl davrandığı. Build adımlarının içeriği Build'de, önbellek davranışı Cache'de.
Prod akışı
npm ci
npm run build # jskelet build
npm start # jskelet start
jskelet build NODE_ENV verilmemişse production ayarlar ve tüm adımları çalıştırır: fontlar, ikon sprite, CSS, client JS, görseller, manifest, precompress.
jskelet start önce .jskelet/manifest.json dosyasına bakar; yoksa build'i kendisi çalıştırır. Docker imajında build zaten yapıldığı için bu bir no-op; amaç npm startı doğrudan çalıştıran birinin stilsiz bir sayfayla karşılaşmaması.
Sunucu hazır olduğunda tek satır basar:
jskelet → http://localhost:3000 (production)
Süreç iki güvenlik ağıyla korunur: unhandledRejection ve uncaughtException loglanır ve süreç ayakta kalır. Bir haber sitesinde tek sayfanın hatası tüm siteyi indirmemeli. Kendi hata izleme aracınıza (Sentry vb.) bağlanmak istiyorsanız aynı olaylara kendi dinleyicinizi de ekleyebilirsiniz.
Ortam değişkenleri
Zorunlu hiçbir değişken yok; hepsinin makul bir varsayılanı var. Prod'da ayarlamayı düşünmeniz gerekenler:
| Değişken | Öneri | Neden |
|---|---|---|
NODE_ENV | production | Şablon cache'i, manifest'in bir kez okunması, bozuk route modülünde fırlatma |
PORT | 3000 | Orkestratörünüzün beklediği port |
HOST | 0.0.0.0 | Yalnızca IPv4 dinlemek gerekiyorsa. Varsayılan :: zaten çift yığın dinler |
PREWARM_MAX | Site boyutuna göre | Açılışta ısıtılacak sayfa sayısı |
PREWARM_INTERVAL_SECONDS | 0 ya da uzun bir değer | Hiç ziyaret edilmeyen sayfaları sıcak tutmak isterseniz |
DEV_TOKEN | Yalnızca staging'de | Yayına açılmamış ortamı gizler |
JSKELET_S3_* | Access log'u S3'e yazıyorsanız | Bucket + credential; ayrıntı 07 |
Production'da dosya veya S3 sink açıldığında HTTP access log middleware otomatik mount edilir (logs.kinds içinde http varsa). Admin paneli ring'i ayrıdır — disk/S3'e yazılan satırlar panele akmaz.
Tam liste ve prewarm ayarlarının öncelik sırası: Yapılandırma.
CLI --env-file-if-exists=.env ile çalıştığı için .env dosyası varsa otomatik yüklenir; yoksa hata verilmez. Kapsayıcıda genelde bu dosya yerine ortam değişkenleri doğrudan enjekte edilir. İki kaynağı birlikte kullanmak hangi değerin geçerli olduğunu belirsizleştirir; prod imajında .env bulundurmamak en temizidir.
Gizli anahtarlar clientEnv listesine konmamalıdır: oradaki değerler client bundle'a düz metin olarak gömülür (Build).
Docker
Çok aşamalı bir imaj: build aşaması dev bağımlılıklarıyla derler, çalışma aşaması yalnızca üretim bağımlılıklarını ve build çıktısını taşır.
# syntax=docker/dockerfile:1
# ---------- build ----------
FROM node:22-bookworm-slim AS build
WORKDIR /app
# Bağımlılıklar ayrı katmanda: kaynak değişince yeniden kurulum yapılmasın.
COPY package.json package-lock.json ./
RUN npm ci
# `public/fonts/` commit edilmiş olmalı: build'in ağa çıkması gerekmesin.
COPY . .
ENV NODE_ENV=production
RUN npx jskelet build
# ---------- runtime ----------
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOST=0.0.0.0
COPY package.json package-lock.json ./
# sharp ve tailwind yalnızca build zamanı gerekli; çalışma imajına girmesin.
# images.remote kullanıyorsanız sharp'ı production dependencies'e alın.
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/jskelet.config.mjs ./jskelet.config.mjs
COPY --from=build /app/jsconfig.json ./jsconfig.json
COPY --from=build /app/routes ./routes
COPY --from=build /app/views ./views
COPY --from=build /app/lib ./lib
COPY --from=build /app/public ./public
COPY --from=build /app/.jskelet ./.jskelet
# Root olmayan kullanıcı.
USER node
EXPOSE 3000
# Sağlık kontrolü: aşağıdaki route'u eklediğinizi varsayar.
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/healthcheck').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["npx", "jskelet", "start"]
Notlar:
client/vestyles/çalışma imajına gerekmez: çıktılarıpublic/assets/altında.views/veroutes/gerekir, çünkü render çalışma anında yapılıyor.lib/yalnızca projenizde varsa kopyalayın..jskelet/gerekir:manifest.jsonolmadanasset()hash'li URL'leri bulamaz vejskelet startbuild'i baştan çalıştırmaya kalkar.sharpçalışma imajında genelde gerekmez: yalnızca build zamanı görsel optimizasyonu için.--omit=devile dışarıda kalır (devDependency olarak kurulmuşsa).images.remoteaçıksa sharp runtime bağımlılığıdır — productiondependencies'e alın ya da runtime imajında ayrıca kurun; yoksa optimizer kaynak URL'ye 302 yönlendirir.jskelet startınpxolmadan çağırmak istersenizCMD ["node", "node_modules/jskelet/bin/jskelet.mjs", "start"]de çalışır.
.dockerignore:
node_modules
.git
.jskelet
public/assets
.env
Build aşaması npx jskelet build ile bunları kendisi üretir.
Depo alt dizininden dağıtım
Bu depodaki örnekler jskelet'i npm'den değil "jskelet": "file:../.." ile alıyor. Coolify, Railway, Render gibi araçlarda "base directory" olarak examples/marketing verilirse build context yalnızca o dizin olur, ../.. context'in dışında kalır ve kurulum npm cide düşer. Doğru ayar: base directory /, Dockerfile konumu /examples/marketing/Dockerfile. Çalışan örnek examples/marketing/Dockerfile içinde ve context'i depo kökü kabul eder:
docker build -f examples/marketing/Dockerfile -t jskelet-marketing .
docker run --rm -p 3000:3000 -e SITE_URL=https://example.com jskelet-marketing
Kendi uygulamanızda jskelet normal bir bağımlılık olacağı için bu kısıt yoktur; yukarıdaki çok aşamalı imaj yeterli.
Sağlık kontrolü
Framework hazır bir sağlık kontrolü ucu eklemez; kendi route'unuza koymanız gerekir. Varsayılan devGateBypass listesi /api/healthcheck yolunu içerdiği için bu adı kullanmak en az sürprizli seçenektir: DEV_TOKEN ayarlı bir ortamda bile erişilebilir kalır.
// routes/00-health.mjs
import { getHtmlCacheSize } from "jskelet";
export default function register(app) {
app.get("/api/healthcheck", (req, res) => {
res.setHeader("Cache-Control", "no-store");
res.json({
ok: true,
uptime: process.uptime(),
cache: getHtmlCacheSize(),
});
});
}
Dosya adındaki 00- öneki, bu route'un herhangi bir yakalayıcıdan önce kaydedilmesini sağlar (Routing).
Farklı bir yol kullanacaksanız devGateBypass listesini güncelleyin, aksi hâlde staging'de orkestratör 404 görür:
devGateBypass: ["/healthz", "/robots.txt", "/sitemap.xml", "/favicon.ico"]
Isıtma turu sağlık kontrolünü etkilemez: prewarm başarısız olsa bile süreç ayakta kalır ve sayfalar (soğuk da olsa) servis edilir.
Hazırlık (readiness) ile canlılık (liveness) ayrımı gerekiyorsa ısıtmanın durumunu de raporlayabilirsiniz:
import { prewarmProgress } from "jskelet";
app.get("/api/ready", (req, res) => {
const warmedUp = !prewarmProgress.active && prewarmProgress.finishedAt !== null;
res.status(warmedUp ? 200 : 503).json({ warmedUp, ...prewarmProgress });
});
Bu ucun yolunu prewarmSkip ile ısıtma dışında bırakmayı unutmayın (varsayılan /api/ öneki zaten kapsıyor).
Ters proxy
Express uygulaması trust proxyyi açık olarak kurar (app.set("trust proxy", true)). Bunun sonuçları:
req.protocolX-Forwarded-Protobaşlığından okunur, yani proxy TLS'i sonlandırıyorsahttpsdoğru döner.req.ipX-Forwarded-Forzincirinden çözülür.res.redirect()ile üretilen mutlak URL'ler doğru şemayı taşır.
Bu ayar proxy'nin bu başlıkları güvenilir biçimde yazdığını varsayar. Uygulamayı doğrudan internete açacaksanız istemcinin X-Forwarded-* başlıklarını uydurabileceğini unutmayın; her zaman bir proxy ya da yük dengeleyici arkasında çalıştırın ve proxy'nin gelen X-Forwarded-For başlığını üzerine yazdığından emin olun.
Örnek nginx yapılandırması:
upstream jskelet {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name ornek.com;
# Yanıt gövdeleri zaten sıkıştırılmış geliyor; ikinci kez sıkıştırma yapma.
gzip off;
location / {
proxy_pass http://jskelet;
proxy_http_version 1.1;
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 Connection "";
# Sıkıştırılmış yanıt alabilmek için upstream'e ilet.
proxy_set_header Accept-Encoding $http_accept_encoding;
}
}
Önemli noktalar:
- Sıkıştırmayı iki kez yapmayın. JSkelet brotli/gzip pazarlığını kendisi yapıyor ve önbelleklenmiş sayfalarda sıkıştırılmış gövdeyi saklıyor. nginx'in kendi
gzipini açık bırakmak brotli'yi çözüp yeniden gzip'lemeye yol açabilir. Accept-Encodingi iletin, yoksa uygulama sıkıştırma yapmaz ve önbellekteki hazır sıkıştırılmış gövdeler kullanılmaz.Vary: Accept-Encodinguygulama tarafından yazılır; proxy önbelleği bunu dikkate alır.
CDN ile birlikte
Önbelleklenebilir sayfalara yazılan başlık:
Cache-Control: public, max-age=0, s-maxage=<revalidate>, stale-while-revalidate=60
max-age=0 tarayıcıda saklamayı kapatır, s-maxage CDN'e süreyi bildirir. Yani aynı tazelik modeli iki katmanda birlikte çalışır: CDN s-maxage boyunca kendi kopyasını verir, süresi geçtiğinde origin'e sorar ve origin de kendi önbelleğinden anında yanıtlar.
X-JSkelet-Cache başlığı hangi katmanın yanıtladığını teşhis etmeyi kolaylaştırır; CDN'in kendi cache başlığıyla birlikte okuyun (Cache).
Statik varlıklar (/assets/, /fonts/) immutable işaretli olduğu için CDN'de süresiz tutulabilir; hash değiştiğinde URL de değişir.
Ölçekleme
HTML önbelleği süreç belleğinde yaşar. Birden fazla kopya çalıştırdığınızda:
- Her kopyanın kendi önbelleği olur; bellek kullanımı kopya sayısıyla çarpılır (en fazla 500 girdi + sıkıştırılmış kopyaları).
- Her kopya açılışta kendi ısıtma turunu yapar.
PREWARM_MAXvePREWARM_CONCURRENCYdeğerlerini upstream API'nizin kopya sayısıyla çarpılmış yükü kaldırabileceği şekilde ayarlayın. clearHtmlCache()yalnızca çağrıldığı süreci etkiler. Tüm kopyaları temizlemek gerekiyorsa bunu orkestratör düzeyinde (yeniden başlatma) ya da kendi yazacağınız bir yayın mekanizmasıyla çözmeniz gerekir.- Önünde bir CDN varsa çoğu istek origin'e hiç gelmez ve kopya başına önbellek farkı görünmez hâle gelir.
Tek kopyanın kapasitesini artırmak için revalidate sürelerini yükseltmek, kopya eklemekten genellikle daha etkilidir: önbellek isabet oranı arttıkça istek başına iş neredeyse sıfıra iner.
Yayın öncesi kontrol listesi
- [ ]
NODE_ENV=production - [ ]
npm run buildçalıştı ve.jskelet/manifest.jsonüretildi - [ ]
public/fonts/içindeki woff2 dosyaları commit edilmiş (Build) - [ ]
styles/globals.cssiçindeki@sourcedirektifleri tüm şablon dizinlerini kapsıyor - [ ]
hooks.notFound()tanımlı ve 404 şablonu var - [ ]
hooks.metadata()içindesiteUrlvar (görelicanonicallar mutlaklaşsın) - [ ]
cache().htmldesenleri sitenin tazelik profiline uygun - [ ]
hooks.prewarmPaths()en önemli sayfaları başa koyuyor - [ ]
headers()içinde CSP ve güvenlik başlıkları tanımlı - [ ] Sağlık kontrolü ucu var ve
devGateBypasslistesinde - [ ] Staging'de
DEV_TOKENayarlı, prod'da ayarlı değil - [ ] Ters proxy
Accept-Encodingi iletiyor ve kendi sıkıştırmasını yapmıyor - [ ]
clientEnvlistesinde gizli anahtar yok
Sırada ne var
- Önbellek ayarları ve prewarm: Cache
- Ortam değişkenlerinin tamamı: Yapılandırma
- Next.js'ten taşıma: Taşıma