Drizzle ORM ile üretimde şema ve göç yönetimi

Drizzle ORM ile üretimde şema ve göç yönetimi

generate mi push mu migrate mi? Göçleri kapsayıcı açılışında çalıştırmak, bağlantı havuzunun Next dev'de sızması ve next build'in veritabanı isteyip kırılması — 17 göçlük gerçek bir panelden.

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

Drizzle'ın öğreticileri şemayı tanımlayıp bir sorgu çalıştırmakla bitiyor. Üretimde asıl işi başlatan sorular ondan sonra geliyor: göçleri ne zaman uygulayacaksın, next build veritabanına erişemediğinde ne olacak, ve geliştirme sunucusu neden bir süre sonra bağlantı sınırına takılıyor?

Bu yazı student.czay.dev panelinden — 1000 satırlık şema, 17 numaralı göç ve Docker'la kendi sunucumda çalışan bir kurulum.

Üç komut, üç farklı iş

Drizzle Kit'in en çok karıştırılan yeri bu.

KomutNe yapıyorNerede kullanılır
drizzle-kit generateŞemayla veritabanı arasındaki farktan SQL dosyası üretirHer şema değişikliğinde
drizzle-kit migrateÜretilmiş SQL dosyalarını sırayla uygularYerelde ve üretimde
drizzle-kit pushŞemayı doğrudan veritabanına iter, dosya üretmezYalnız prototip
🚨push'u üretimde kullanma

push hızlı ve baştan çıkarıcı: şemayı değiştir, it, bitti. Ama geriye hiçbir kayıt bırakmıyor. Ne uygulanan değişikliğin geçmişi var, ne geri alma imkânı, ne de başka bir ortamda aynı adımları tekrarlama yolu.

Üretim veritabanı olan bir projede generate + migrate kullan. push tek kişilik bir prototipte, atılacak bir veritabanında anlamlı.

Göç dosyalarına anlamlı isim vermek de sonradan işe yarıyor. Bu paneldeki klasör şöyle görünüyor:

plaintext
drizzle/
  0000_taban.sql
  0001_dogum_tarihi.sql
  0002_ders_serisi.sql
  0003_grup_panosu.sql
  0004_saklama_ve_silme.sql

  0015_sinif_odulleri.sql
  0016_sosyal_profiller.sql

Altı ay sonra "şu alan ne zaman eklendi" sorusunun cevabı klasörde duruyor.

Göçleri ne zaman uygulamalı?

Cevap: derlemede değil, kapsayıcı açılışında.

💬Neden derlemede değil?

İmaj derlenirken veritabanına erişimin yok ve olmamalı da — aynı imaj başka bir veritabanına da açılabilmeli. Ayrıca dağıtım aracının yeniden başlatması, ölçeklemesi ve geri alması hep aynı imajı kullanıyor; şemayı imaja gömemezsin.

Göç, kodun değil veritabanının durumu. Kodun sürümü imajda, şemanın durumu veritabanında.

Açılış betiği sunucudan önce göçleri uyguluyor:

Bash
#!/bin/sh
set -e
 
echo "[giris] şema göçleri uygulanıyor…"
node scripts/migrate.cjs
 
echo "[giris] sunucu açılıyor…"
exec node apps/student/server.js

Göç başarısızsa set -e sayesinde kapsayıcı hiç açılmıyor. Bu bilinçli: yarım şemayla istek karşılayan bir sürüm, açılmayan bir sürümden kötüdür — hata ilk kullanıcı isteğinde ve sebebi görünmeden çıkar.

Göç klasörünü betiğe göre çöz

Küçük ama üretimde canını yakan bir ayrıntı:

TypeScript
import path from "node:path";
 
// Çalışma dizinine göre DEĞİL, betiğin konumuna göre
const gocKlasoru = path.join(__dirname, "..", "drizzle");

Kaynakta betik db/ altında, kapsayıcıda paketlenmiş hâli scripts/ altında duruyor. İkisinde de bir üstteki drizzle/ doğru; ama çalışma dizinine güvenirsen kapsayıcı içinde klasörü bulamıyor.

İki koşucu, tek kayıt

Yerelde drizzle-kit migrate, üretimde kendi paketlenmiş betiğim çalışıyor. İkisi de aynı drizzle.__drizzle_migrations tablosunu kullandığı için hangisi çalışırsa çalışsın kayıt ortak — bir göç iki kez uygulanmıyor. Kendi koşucunu yazacaksan bu tabloyu değiştirme.

next build veritabanı istiyor

Bu, Next.js + ORM birleşiminin klasik tuzağı ve teşhisi zor.

Sorun şu: derleme sırasında Next.js modülleri değerlendiriyor. Veritabanı istemcisini modül seviyesinde kurmuşsan, o kurulum derleme anında çalışıyor ve DB_POSTGRES_URL yoksa next build kırılıyor. Oysa derlemenin veritabanına ihtiyacı yok.

Benim durumumda tetikleyici better-auth'un belirli bir sürümden sonra adaptöre kurulum anında dokunmasıydı — derleme sunucuda, veritabanı erişimi olmadan yapılıyordu ve her seferinde patlıyordu.

🧪Çözüm: bağlantıyı ilk sorguya ertele
TypeScript
function connect(): Database {
	if (globalForDb.__studentDb) return globalForDb.__studentDb;
 
	const derlemede = process.env.NEXT_PHASE === "phase-production-build";
	const url =
		process.env.DB_POSTGRES_URL ??
		(derlemede ? "postgresql://derleme@127.0.0.1:5432/derleme" : undefined);
 
	if (!url) {
		throw new Error("DB_POSTGRES_URL tanımlı değil.");
	}
 
	const sql = globalForDb.__studentSql ?? postgres(url, {
		max: 10,
		connect_timeout: 10,
	});
 
	const instance = drizzle(sql, { schema });
	globalForDb.__studentSql = sql;
	globalForDb.__studentDb = instance;
	return instance;
}
 
/** Bağlantı ilk sorguda kurulur; modül değerlendirmesinde değil. */
export const db = new Proxy({} as Database, {
	get: (_target, prop) => connect()[prop as keyof Database],
});

İki şey birden çalışıyor:

  • Yer tutucu adres yalnız derleme aşamasında devreye giriyor (NEXT_PHASE). Çalışma anında değişken hâlâ zorunlu ve hata mesajı aynı.
  • postgres.js soketi ilk sorguda açıyor, kurulumda değil. Yani yer tutucu adresle hiçbir bağlantı denemesi yapılmıyor; derleme boyunca tek bir paket bile gitmiyor.

Proxy de bunun için: db nesnesine gerçekten dokunulana kadar connect() çalışmıyor.

Bağlantı havuzu sızıntısı

İki ayrı sızıntı var ve ikisi de sessizce ilerliyor.

⚠️1. Geliştirmede modül yeniden yükleme

Next.js geliştirme sunucusu her değişiklikte modülleri yeniden yüklüyor. Her yükleme yeni bir postgres() havuzu açıyor, eskisi kapanmıyor. Yarım saat çalıştıktan sonra Postgres'in varsayılan 100 bağlantı sınırına dayanıyorsun ve "too many clients" hatası alıyorsun.

Çözüm havuzu globalThis üzerinde tutmak: modül yeniden yüklense de nesne kalıyor.

🚨2. Üretimde proxy

İkincisi daha sinsi ve bunu üretimde yakaladım. Havuzu yalnız geliştirmede önbelleğe alıyordum — yaygın tavsiye bu. Ama yukarıdaki proxy her özellik erişiminde connect() çağırıyor. Yani üretimde db.select, db.insert, db.query her erişimde yeni bir havuz kuruyordu.

Havuzlar hiç kapanmadığı için bağlantı sayısı istek başına artıyordu. Çözüm: üretimde de önbelleğe almak.

Havuz boyutu da bilinçli seçilmeli. Bu panelde max: 10: Postgres aynı makinede olduğu için bağlantı kurmak ucuz ve 10 soket bu eşzamanlılığa fazlasıyla yetiyor, Postgres'in varsayılan sınırını da zorlamıyor.

Yapılandırma

TypeScript
// drizzle.config.ts
export default defineConfig({
	schema: "./db/schema.ts",
	out: "./drizzle",
	dialect: "postgresql",
	dbCredentials: { url: databaseUrl },
	strict: true,
	verbose: true,
});

strict: true yıkıcı bir işlem üretileceğinde onay istiyor — sütun silen bir göçü fark etmeden üretmeni engelliyor. verbose: true de üretilen SQL'i gösteriyor. İkisini de açık tut; göç dosyasını uygulamadan önce okumak bu işin en ucuz güvenlik önlemi.

Özet

📋Üretim kontrol listesi
  • generate + migrate kullan; push prototip için
  • Göç dosyalarına anlamlı isim ver — altı ay sonra kendine teşekkür edersin
  • Göçleri kapsayıcı açılışında uygula, derlemede değil
  • Göç klasörünü __dirname'e göre çöz, çalışma dizinine göre değil
  • Kendi koşucunu yazsan bile __drizzle_migrations tablosunu paylaş
  • Bağlantıyı ilk sorguya ertele; next build veritabanı istememeli
  • Havuzu globalThis'te tut — geliştirmede de üretimde de
  • strict: true ile üretilen SQL'i uygulamadan önce oku

İlgili yazı: Next.js'i kendi sunucunda yayınlamak

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