Los tutoriales de Drizzle suelen terminar al definir un esquema y ejecutar una consulta. Pero en producción empiezan las preguntas de verdad: ¿cuándo aplicas las migraciones?, ¿qué pasa cuando next build no puede acceder a la base de datos? y ¿por qué tu servidor de desarrollo alcanza el límite de conexiones al cabo de un rato?
Este artículo se basa en el panel student.czay.dev: una configuración con un esquema de 1000 líneas y 17 migraciones, que corre en mi propio servidor con Docker.
Tres comandos, tres trabajos distintos
Esta es la parte más confusa de Drizzle Kit.
| Comando | Qué hace | Dónde se usa |
|---|---|---|
drizzle-kit generate | Genera un archivo SQL a partir de la diferencia entre el esquema y la base de datos | En cada cambio de esquema |
drizzle-kit migrate | Aplica en orden los archivos SQL generados | En local y en producción |
drizzle-kit push | Envía el esquema directamente a la base de datos sin generar archivos | Solo prototipos |
Precaución: no uses push en producción
push es rápido y tentador: cambias el esquema, haces push y listo. Pero no deja
ningún registro. No hay historial de los cambios aplicados, no hay forma de revertir
y no hay manera de reproducir los mismos pasos en otro entorno.
Usa generate + migrate en cualquier proyecto con una base de datos de producción. Reserva push
para prototipos personales o bases de datos desechables.
Ponerle nombres con sentido a las migraciones también compensa más adelante. La carpeta de este panel tiene este aspecto:
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.sqlDentro de seis meses, la respuesta a "¿cuándo se añadió este campo?" estará ahí mismo, en la carpeta.
¿Cuándo se deben aplicar las migraciones?
Respuesta: no durante la compilación, sino al arrancar el contenedor.
Explicación: ¿Por qué no durante la compilación?
Al construir la imagen no tienes acceso a la base de datos, ni deberías tenerlo: la misma imagen debe poder desplegarse sobre otra base de datos. Además, los reinicios, el escalado y los rollbacks de tu herramienta de despliegue usan la misma imagen; no puedes incrustar el estado del esquema en la imagen.
Una migración es el estado de la base de datos, no del código. La versión del código vive en la imagen, mientras que el estado del esquema vive en la base de datos.
El script de entrypoint ejecuta las migraciones antes de arrancar el servidor:
#!/bin/sh
set -e
echo "[entrypoint] applying schema migrations…"
node scripts/migrate.cjs
echo "[entrypoint] starting server…"
exec node apps/student/server.jsSi una migración falla, el contenedor no arranca en absoluto gracias a set -e. Es intencional: una versión que atiende solicitudes con un esquema roto es peor que una versión que no arranca, porque los errores solo aparecerían en silencio con la primera solicitud de un usuario.
Resuelve la carpeta de migraciones relativa al script
Un detalle pequeño, pero que duele en producción:
import path from "node:path";
// Relative to the script's location, NOT the working directory
const gocKlasoru = path.join(__dirname, "..", "drizzle");En el código fuente, el script vive bajo db/, mientras que en el contenedor la versión empaquetada está bajo scripts/. En ambos casos, el drizzle/ del directorio padre es el correcto; pero si dependes del directorio de trabajo, no encontrará la carpeta dentro del contenedor.
Dos ejecutores, un solo registro
En local se ejecuta drizzle-kit migrate, mientras que en producción se encarga mi script empaquetado. Como ambos usan la misma tabla drizzle.__drizzle_migrations, el seguimiento es compartido sin importar cuál se ejecute: una migración nunca se aplica dos veces. Si escribes tu propio ejecutor, no cambies esta tabla.
next build quiere una base de datos
Esta es la trampa clásica de combinar Next.js con un ORM, y es difícil de diagnosticar.
El problema es que Next.js evalúa los módulos durante la fase de compilación. Si inicializas el cliente de la base de datos a nivel de módulo, esa inicialización se ejecuta durante la compilación y, si falta DB_POSTGRES_URL, next build falla. Y sin embargo, el proceso de compilación no necesita realmente la base de datos.
En mi caso, el detonante fue que better-auth, a partir de cierta versión, tocaba el adaptador en el momento de la inicialización. La compilación corría en un servidor de CI sin acceso a la base de datos y no dejaba de reventar.
Ejemplo: Solución: aplaza la conexión hasta la primera consulta
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],
});Aquí intervienen dos cosas:
- La dirección de relleno solo se activa durante la fase de compilación (
NEXT_PHASE). En tiempo de ejecución la variable de entorno sigue siendo obligatoria y el mensaje de error no cambia. postgres.jsabre el socket con la primera consulta, no durante la inicialización. Esto significa que nunca se intenta una conexión con la dirección de relleno; no se envía ni un solo paquete durante toda la compilación.
Para eso sirve el Proxy: connect() no se ejecuta hasta que realmente se toca el objeto db.
Fuga del pool de conexiones
Hay dos fugas distintas y ambas avanzan en silencio.
Advertencia: 1. Recarga de módulos en desarrollo
El servidor de desarrollo de Next.js recarga los módulos con cada cambio de código. Cada recarga
abre un nuevo pool de postgres() y el anterior nunca se cierra. Después de trabajar
media hora, alcanzas el límite por defecto de Postgres de 100 conexiones
y aparece el error "too many clients".
La solución es mantener el pool en globalThis: aunque el módulo se recargue,
el objeto persiste.
Precaución: 2. El Proxy en producción
La segunda es más sutil y la detecté en producción. Estaba cacheando el pool
solo en desarrollo, que es el consejo habitual. Sin embargo, el proxy
de arriba llama a connect() en cada acceso a una propiedad. Esto significa que cada llamada a db.select,
db.insert o db.query en producción creaba un pool nuevo.
Como los pools nunca se cerraban, el número de conexiones crecía con cada solicitud. La solución: cachéalo también en producción.
El tamaño del pool también debe elegirse con criterio. En este panel uso max: 10: como Postgres está en la misma máquina, establecer conexiones es barato. 10 sockets son más que suficientes para este nivel de concurrencia y no fuerzan los límites por defecto de Postgres.
Configuración
// drizzle.config.ts
export default defineConfig({
schema: "./db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: { url: databaseUrl },
strict: true,
verbose: true,
});strict: true pide confirmación antes de generar una acción destructiva: evita que generes por accidente una migración que elimina una columna. verbose: true imprime el SQL generado. Mantén ambos activados; leer el archivo de migración antes de aplicarlo es la medida de seguridad más barata de todo este juego.
Resumen
Resumen: Lista de verificación para producción
- Usa
generate+migrate; reservapushpara prototipos - Dale nombres con sentido a los archivos de migración: dentro de seis meses te lo agradecerás
- Ejecuta las migraciones al arrancar el contenedor, no durante la compilación
- Resuelve la carpeta de migraciones con
__dirname, no con el directorio de trabajo - Comparte la tabla
__drizzle_migrationsaunque escribas tu propio ejecutor - Aplaza las conexiones hasta la primera consulta;
next buildno debería requerir una base de datos - Mantén el pool en
globalThis, tanto en desarrollo como en producción - Lee el SQL generado con
strict: trueantes de aplicarlo
Artículo relacionado: Desplegar Next.js en tu propio servidor
Hoja de ruta y herramienta interactivas
Desarrollador Frontend Junior 2026
Lo que realmente necesitas saber para conseguir un trabajo de frontend junior en 2026 y lo que puede esperar, a partir de ofertas reales.
¿SQL o NoSQL?
Responde seis preguntas y te diré qué tipo de base de datos encaja con tu proyecto, y por qué.