Die Tutorials zu Drizzle enden meist damit, ein Schema zu definieren und eine Abfrage auszuführen. In Produktion fangen die eigentlichen Fragen aber erst an: Wann wendest du Migrationen an, was passiert, wenn next build nicht auf die Datenbank zugreifen kann, und warum stößt dein Entwicklungsserver nach einer Weile an das Verbindungslimit?
Dieser Beitrag basiert auf dem Panel student.czay.dev – einem Setup mit einem Schema von 1.000 Zeilen und 17 Migrationen, das auf meinem eigenen Server mit Docker läuft.
Drei Befehle, drei verschiedene Aufgaben
Das ist der verwirrendste Teil von Drizzle Kit.
| Befehl | Was er macht | Wo er eingesetzt wird |
|---|---|---|
drizzle-kit generate | Erzeugt aus dem Unterschied zwischen Schema und Datenbank eine SQL-Datei | Bei jeder Schema-Änderung |
drizzle-kit migrate | Wendet die erzeugten SQL-Dateien der Reihe nach an | Lokal und in Produktion |
drizzle-kit push | Schiebt das Schema direkt in die Datenbank, ohne Dateien zu erzeugen | Nur für Prototypen |
Achtung: push nicht in Produktion verwenden
push ist schnell und verlockend: Schema ändern, pushen, fertig. Aber es
hinterlässt keine Spuren. Es gibt keine Historie der angewendeten
Änderungen, keine Möglichkeit zum Rollback und keine Möglichkeit, genau diese
Schritte in einer anderen Umgebung zu wiederholen.
Nutze in jedem Projekt mit einer Produktionsdatenbank generate + migrate.
Heb dir push für Ein-Mann-Prototypen oder Wegwerf-Datenbanken auf.
Aussagekräftige Namen für Migrationen zahlen sich später ebenfalls aus. Der Ordner in diesem Panel sieht so aus:
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.sqlSechs Monate später steht die Antwort auf „Wann wurde dieses Feld hinzugefügt?" direkt im Ordner.
Wann solltest du Migrationen anwenden?
Antwort: nicht während des Builds, sondern beim Start des Containers.
Erklärung: Warum nicht während des Builds?
Beim Bauen des Images hast du keinen Zugriff auf die Datenbank, und den solltest du auch nicht haben – dasselbe Image muss sich auf eine andere Datenbank deployen lassen. Außerdem nutzen Neustarts, Skalierung und Rollbacks deines Deployment-Tools alle dasselbe Image; den Schema-Zustand kannst du nicht ins Image einbacken.
Eine Migration ist der Zustand der Datenbank, nicht des Codes. Die Code-Version steckt im Image, während der Schema-Zustand in der Datenbank liegt.
Das Entrypoint-Skript führt die Migrationen aus, bevor es den Server startet:
#!/bin/sh
set -e
echo "[entrypoint] applying schema migrations…"
node scripts/migrate.cjs
echo "[entrypoint] starting server…"
exec node apps/student/server.jsSchlägt eine Migration fehl, startet der Container dank set -e gar nicht erst. Das ist Absicht: Eine Version, die mit kaputtem Schema Anfragen bedient, ist schlimmer als eine, die nicht startet – Fehler würden sonst erst still bei der ersten Nutzeranfrage auftauchen.
Den Migrationsordner relativ zum Skript auflösen
Ein kleines Detail, das in Produktion aber wehtut:
import path from "node:path";
// Relative to the script's location, NOT the working directory
const gocKlasoru = path.join(__dirname, "..", "drizzle");Im Quellcode liegt das Skript unter db/, im Container liegt die gebündelte Version unter scripts/. In beiden Fällen ist das drizzle/ des übergeordneten Ordners korrekt; verlässt du dich aber auf das Arbeitsverzeichnis, findet das Skript den Ordner im Container nicht.
Zwei Runner, ein Protokoll
Lokal läuft drizzle-kit migrate, in Produktion übernimmt mein gebündeltes Skript. Da beide dieselbe Tabelle drizzle.__drizzle_migrations verwenden, ist die Nachverfolgung gemeinsam – egal, welcher von beiden läuft –, und eine Migration wird nie zweimal angewendet. Wenn du einen eigenen Runner schreibst, ändere diese Tabelle nicht.
next build will eine Datenbank
Das ist die klassische Falle bei der Kombination von Next.js mit einem ORM, und sie ist schwer zu diagnostizieren.
Das Problem: Next.js wertet Module in der Build-Phase aus. Wenn du den Datenbank-Client auf Modulebene initialisierst, wird dieses Setup zur Build-Zeit ausgeführt, und fehlt DB_POSTGRES_URL, bricht next build ab. Dabei braucht der Build-Prozess die Datenbank eigentlich gar nicht.
Bei mir war der Auslöser better-auth, das ab einer bestimmten Version bei der Initialisierung den Adapter anfasst. Der Build lief auf einem CI-Server ohne Datenbankzugriff und flog immer wieder auseinander.
Beispiel: Lösung: Die Verbindung bis zur ersten Abfrage aufschieben
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://[email protected]:5432/derleme" : undefined);
if (!url) {
throw new Error("DB_POSTGRES_URL is not defined.");
}
const sql = globalForDb.__studentSql ?? postgres(url, {
max: 10,
connect_timeout: 10,
});
const instance = drizzle(sql, { schema });
globalForDb.__studentSql = sql;
globalForDb.__studentDb = instance;
return instance;
}
/** Connection is established on the first query, not during module evaluation. */
export const db = new Proxy({} as Database, {
get: (_target, prop) => connect()[prop as keyof Database],
});Hier spielen zwei Dinge zusammen:
- Die Platzhalter-Adresse greift nur in der Build-Phase (
NEXT_PHASE). Zur Laufzeit ist die Umgebungsvariable weiterhin Pflicht, und die Fehlermeldung bleibt dieselbe. postgres.jsöffnet den Socket bei der ersten Abfrage, nicht bei der Initialisierung. Das heißt, mit der Platzhalter-Adresse wird nie ein Verbindungsversuch unternommen; während des gesamten Builds wird kein einziges Paket gesendet.
Dafür ist der Proxy da: connect() wird erst ausgeführt, wenn das db-Objekt tatsächlich angefasst wird.
Connection-Pool-Leck
Es gibt zwei getrennte Lecks, und beide entwickeln sich unbemerkt.
Warnung: 1. Neuladen von Modulen in der Entwicklung
Der Next.js-Entwicklungsserver lädt Module bei jeder Codeänderung neu. Jedes
Neuladen öffnet einen neuen postgres()-Pool, und der alte wird nie
geschlossen. Nach einer halben Stunde Arbeit erreichst du das
Standardlimit von Postgres mit 100 Verbindungen und bekommst den Fehler
„too many clients".
Die Lösung ist, den Pool auf globalThis zu halten: Selbst wenn das Modul neu
geladen wird, bleibt das Objekt bestehen.
Achtung: 2. Proxy in Produktion
Das zweite ist subtiler, und ich habe es in Produktion entdeckt. Ich habe den
Pool nur in der Entwicklung gecacht – was der verbreitete Rat ist. Der
Proxy oben ruft connect() jedoch bei jedem Property-Zugriff auf. Das heißt,
jeder Aufruf von db.select, db.insert oder db.query in Produktion hat
einen neuen Pool aufgebaut.
Da die Pools nie geschlossen wurden, wuchs die Zahl der Verbindungen mit jeder einzelnen Anfrage. Die Lösung: auch in Produktion cachen.
Auch die Pool-Größe solltest du bewusst wählen. In diesem Panel nutze ich max: 10: Da Postgres auf derselben Maschine läuft, ist der Verbindungsaufbau günstig. 10 Sockets reichen für diese Parallelität mehr als aus, und die Standardlimits von Postgres werden nicht belastet.
Konfiguration
// drizzle.config.ts
export default defineConfig({
schema: "./db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: { url: databaseUrl },
strict: true,
verbose: true,
});strict: true fragt vor dem Erzeugen einer destruktiven Aktion nach einer Bestätigung – es verhindert, dass du versehentlich eine Migration erzeugst, die eine Spalte löscht. verbose: true gibt das erzeugte SQL aus. Lass beides aktiviert; die Migrationsdatei vor dem Anwenden zu lesen ist die günstigste Sicherheitsmaßnahme in diesem Spiel.
Zusammenfassung
Zusammenfassung: Checkliste für Produktion
- Nutze
generate+migrate; heb dirpushfürs Prototyping auf - Gib Migrationsdateien aussagekräftige Namen – in sechs Monaten wirst du dir dafür danken
- Führe Migrationen beim Start des Containers aus, nicht während des Builds
- Löse den Migrationsordner über
__dirnameauf, nicht über das Arbeitsverzeichnis - Teile dir die Tabelle
__drizzle_migrations, auch wenn du einen eigenen Runner schreibst - Schiebe Verbindungen bis zur ersten Abfrage auf;
next buildsollte keine Datenbank brauchen - Halte den Pool auf
globalThis– sowohl in der Entwicklung als auch in Produktion - Lies das mit
strict: trueerzeugte SQL, bevor du es anwendest
Passender Beitrag: Next.js auf dem eigenen Server deployen
Interaktive Roadmap & Tool
Junior Frontend Developer 2026
Was du wirklich wissen musst, um 2026 einen Junior-Frontend-Job zu bekommen, und was warten kann – abgeleitet aus echten Stellenanzeigen.
SQL oder NoSQL?
Beantworte sechs Fragen, und ich sage dir, welche Art von Datenbank zu deinem Projekt passt und warum.