Respuesta corta#
Casi siempre es un problema de paridad de entornos: tu máquina y el servidor de build no son idénticos. Las causas más frecuentes son variables de entorno que existen en tu equipo pero no en el servidor, herramientas instaladas globalmente o como devDependencies, versiones distintas de Node o Python, archivos bloqueados por .gitignore y la sensibilidad a mayúsculas de Linux: macOS y Windows perdonan errores de mayúsculas en los imports; el servidor normalmente no.
La solución pasa por reproducir localmente el entorno de build de producción y eliminar las suposiciones implícitas. Abajo tienes las diez causas, cómo diagnosticar cada una y un checklist para recorrerlas en orden.
Documentación: The Twelve-Factor App · Dev/prod parity ↗ · npm · npm ci ↗
La causa raíz: «en mi máquina funciona»#
El build que pasa en tu laptop y falla en el servidor es uno de los problemas más viejos del desarrollo. La razón de fondo es siempre la misma: tu entorno local y el de build no son iguales. Tu máquina acumula configuración, variables exportadas en el shell, dependencias globales y rutas que diste por sentadas; el servidor arranca limpio, desde el repositorio y nada más.
Diagnosticarlo es un proceso de eliminar diferencias una por una. Estas diez explican la gran mayoría de los casos.
1. Variables de entorno que solo existen en tu máquina#
Tienes una variable en tu .env local o exportada en tu shell y el código la usa durante el build. En el servidor no está definida, y el build falla o genera un artefacto roto.
Cómo diagnosticarlo: busca toda lectura de process.env (Node) u os.environ (Python) que ocurra en tiempo de build, incluidos archivos de configuración del bundler.
Cómo solucionarlo: declara explícitamente cada variable en la configuración del entorno de deploy. Nunca subas el .env al repositorio; documenta las variables requeridas en un .env.example versionado.
# .env.example (este SÍ se versiona; el .env real no)
DATABASE_URL=
API_KEY=
NODE_ENV=productionDocumentación: The Twelve-Factor App · Dev/prod parity ↗
2. NODE_ENV=production cambia el comportamiento#
Muchas apps y herramientas de Node se comportan distinto según NODE_ENV. Un bug puede aparecer solo cuando vale production, por ejemplo al minificar o al optimizar assets, y no verse nunca en desarrollo. Además, npm cambia su propio comportamiento: con NODE_ENV=production, por defecto omite las devDependencies al instalar.
Cómo solucionarlo: reproduce el bug localmente forzando el mismo valor antes de compilar, con NODE_ENV=production npm run build. Si falla igual que en el servidor, ya tienes el caso aislado en tu máquina.
Documentación: Node.js · Development vs production ↗ · npm · config: omit / include ↗
3. La herramienta de build está en devDependencies#
Tu build necesita un paquete, como un bundler o el compilador de TypeScript, pero está en devDependencies. Si el servidor instala en modo producción (con NODE_ENV=production o --omit=dev), ese paquete nunca llega y el build falla con command not found o module not found.
Cómo solucionarlo: tienes dos caminos válidos. El más limpio es instalar también las devDependencies en la etapa de build y dejar fuera solo las de la imagen final. Si tu plataforma no te deja controlar la instalación, mueve a dependencies lo que el build necesita.
# Opción A: el servidor instala también devDependencies para el build
npm ci --include=dev
npm run build
# Opción B: la herramienta de build pasa a dependencies
npm install --save typescriptDocumentación: npm · package.json devDependencies ↗ · npm · config: omit / include ↗ · npm · npm ci ↗
4. Sensibilidad a mayúsculas: Linux no perdona#
Es el clásico silencioso. macOS y Windows usan por defecto sistemas de archivos que no distinguen mayúsculas, así que import Button from './components/button' funciona aunque el archivo se llame Button.tsx. El servidor de build suele correr Linux, que sí las distingue, y el mismo import falla con module not found.
Hay una variante más traicionera: renombraste button.tsx a Button.tsx en tu Mac, pero Git no registró el cambio porque core.ignoreCase está activo en ese sistema. En tu disco se llama de una forma y en el repositorio de otra.
# ¿Qué nombre tiene realmente el archivo en el repositorio?
git ls-files | grep -i 'components/button'
# Renombrar solo cambiando mayúsculas (en macOS/Windows hace falta git mv)
git mv src/components/button.tsx src/components/Button.tsxPara prevenirlo: corrige cada import para que coincida carácter por carácter con el archivo, activa forceConsistentCasingInFileNames en TypeScript y ejecuta el build en CI sobre Linux, donde el error aparece antes de llegar al deploy.
Documentación: Git · core.ignoreCase ↗ · TypeScript · forceConsistentCasingInFileNames ↗ · Git · git check-ignore ↗
5. Versión de Node, Python o del runtime distinta#
Tú corres una versión local y el servidor usa otra por defecto. Una API que existe en tu versión puede no existir en la del build, o al revés, y una dependencia puede exigir una versión mínima.
Cómo solucionarlo: fija la versión en el proyecto para que tu máquina, CI y el servidor lean la misma.
# .nvmrc (lo leen nvm y actions/setup-node)
22
// package.json
{
"engines": { "node": ">=22 <23" }
}
# .python-version (pyenv y varias plataformas)
3.12Verifica tu versión local con node --version o python --version y confirma en la documentación de tu plataforma cuál de estos archivos lee.
Documentación: npm · package.json engines ↗ · nvm · .nvmrc ↗ · GitHub · actions/setup-node ↗
6. Un archivo necesario está bloqueado por .gitignore#
El archivo existe en tu disco, pero una regla demasiado amplia de .gitignore impide que entre al repositorio. Como el build parte del repositorio, en el servidor ese archivo simplemente no existe.
Ejemplo clásico: una regla lib, pensada para ignorar la carpeta de salida de la raíz, ignora también cualquier carpeta llamada lib en cualquier nivel, como src/lib con código que tu app necesita. Un patrón sin barra coincide en cualquier nivel; con barra inicial, solo en el directorio del .gitignore.
# Antes: ignora cualquier carpeta "lib" en cualquier nivel
lib
# Después: ignora solo /lib en la raíz del proyecto
/lib
# ¿Qué regla está ignorando este archivo?
git check-ignore -v src/lib/format.tsConfirma qué se está versionando realmente con git ls-files.
Documentación: Git · gitignore ↗ · Git · git check-ignore ↗
7. Lockfile desactualizado o sin commitear#
Si tu package-lock.json, yarn.lock o poetry.lock no está en el repositorio o está desincronizado con package.json, el servidor puede resolver versiones distintas a las tuyas y romper el build.
Cómo solucionarlo: versiona siempre el lockfile e instala con el comando que respeta el lock exacto.
npm ci # instala exactamente lo del lockfile y falla si no coincide con package.json
# en lugar de:
npm install # puede resolver versiones nuevas y reescribir el lockfileDocumentación: npm · npm ci ↗ · npm · package-lock.json ↗
8. Dependencias nativas o herramientas de compilación ausentes#
Algunos paquetes compilan código nativo (C o C++) durante la instalación y necesitan herramientas como gcc, make o python, que tu máquina tiene y la imagen del servidor no. Otros descargan binarios precompilados para un sistema operativo y una arquitectura concretos: el binario de tu Mac con Apple Silicon no sirve en un servidor Linux x86_64.
Cómo solucionarlo: lee los logs del build para identificar el paquete que falla, asegúrate de que la imagen base incluya las herramientas de compilación necesarias o usa una versión precompilada para la plataforma del servidor, y nunca copies node_modules desde tu máquina: instala siempre en el servidor.
Documentación: npm · npm ci ↗
9. El build se queda sin memoria#
Tu laptop suele tener bastante más RAM que el entorno de build. Los builds grandes, sobre todo de frontend, pueden morir con un error de memoria que nunca verías en local.
Cómo diagnosticarlo: busca en los logs JavaScript heap out of memory o un proceso que termina sin explicación, típico de un OOM kill.
Cómo solucionarlo: reduce el consumo del build (divide bundles, limita la concurrencia) o sube el límite del heap de Node con NODE_OPTIONS=--max-old-space-size=4096 npm run build. El valor está en MiB y debe caber en la memoria real del entorno de build; si lo subes por encima, el sistema matará el proceso de todos modos.
Documentación: Node.js · --max-old-space-size ↗
10. Variables de build-time que no llegan al build#
Es distinto del punto 1: algunas plataformas inyectan las variables solo en runtime, cuando la app corre, y no durante el build. Si la compilación necesita una variable, por ejemplo la URL pública de una API en un frontend, y solo existe en runtime, el build usa un valor vacío sin avisar.
En Next.js, las variables NEXT_PUBLIC_ se incrustan en el código del navegador al compilar; en Vite ocurre lo mismo con las VITE_. Cambiarlas después del build no tiene efecto: hay que volver a compilar. Confirma en la documentación de tu plataforma qué variables están disponibles en build-time y declara ahí las que necesita la compilación.
Documentación: Next.js · Environment variables ↗ · Vite · Env variables and modes ↗
Reproduce el build en limpio y en CI#
La regla práctica: si no puedes reproducir el build en un directorio nuevo, sin node_modules previos, sin variables exportadas en tu shell y con la misma versión de runtime, el problema no es el servidor; es tu configuración local.
# Reproduce el build como lo haría el servidor: clon limpio, sin node_modules ni .env
git clone --depth 1 <url-del-repo> /ruta/limpia && cd /ruta/limpia
node --version # compárala con la del servidor
env -i PATH="$PATH" HOME="$HOME" NODE_ENV=production \
sh -c 'npm ci --include=dev && npm run build' Mejor todavía: ejecuta el mismo build en CI sobre Linux en cada push. Así los errores de mayúsculas, versiones y lockfile aparecen en el pull request y no en el deploy.
# .github/workflows/build.yml
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build
env:
NODE_ENV: productionDocumentación: GitHub · actions/setup-node ↗ · npm · npm ci ↗
Checklist de diagnóstico rápido#
Cuando un build pase en local y falle en el deploy, recórrelo en este orden:
- ¿Reproduces el fallo con NODE_ENV=production npm run build en un clon limpio?
- ¿Todas las variables que el build necesita están declaradas en el servidor, en la fase correcta?
- ¿Las herramientas de build se instalan en el servidor (dependencies o --include=dev)?
- ¿Los imports coinciden exactamente en mayúsculas con los nombres de archivo del repositorio?
- ¿La versión de Node o Python está fijada y coincide con la local?
- ¿Hay archivos necesarios bloqueados por .gitignore?
- ¿El lockfile está commiteado y usas npm ci?
- ¿Los logs mencionan memoria, herramientas de compilación o un paquete nativo?
Si llegas al final sin encontrar la causa, lo más probable es una dependencia global implícita o un script de build distinto al que usas en local.
Preguntas frecuentes#
¿Por qué mi import funciona en Mac pero falla en el servidor? Porque macOS y Windows ignoran por defecto las mayúsculas en los nombres de archivo y Linux no. Un import de ./Button no encuentra un archivo llamado button.tsx en Linux. Corrige la ruta y, si renombraste el archivo, hazlo con git mv.
¿Cuál es la diferencia entre npm install y npm ci en un deploy? npm install puede actualizar versiones y modificar el lockfile; npm ci instala exactamente lo que dice el lockfile, borra node_modules antes de empezar y falla si el lock no coincide con package.json. Para builds reproducibles, usa npm ci.
¿Por qué el build se queda sin memoria solo en el servidor? Porque el entorno de build suele tener menos RAM que tu máquina. Reduce el tamaño del build o ajusta el límite del heap dentro de la memoria disponible.
¿Debo subir mi .env al repositorio para que el deploy funcione? No. Declara las variables en la configuración de tu plataforma y versiona solo un .env.example sin valores.
Cierra la brecha entre local y producción#
Cada build que falla solo en el servidor es el costo de una suposición no documentada: una variable que solo existía en tu shell, una dependencia global que instalaste hace meses o un import que tu sistema de archivos perdonó. La paridad de entornos no es un lujo; es la base para que los deploys sean predecibles.
El objetivo no es que el servidor se parezca a tu máquina, sino que ambos se comporten igual a partir de la misma definición explícita: versiones fijadas, variables declaradas, lockfile versionado y un build que corre en CI.
Documentación: The Twelve-Factor App · Dev/prod parity ↗
Fuentes y alcance
Documentación consultada el 25 de septiembre de 2026. Los ejemplos y criterios de decisión son propuestas editoriales; adáptalos al contrato de tu aplicación y valídalos en tu entorno de pruebas autorizado.
- The Twelve-Factor App · Dev/prod parity ↗
- npm · npm ci ↗
- Node.js · Development vs production ↗
- npm · config: omit / include ↗
- npm · package.json devDependencies ↗
- Git · core.ignoreCase ↗
- TypeScript · forceConsistentCasingInFileNames ↗
- Git · git check-ignore ↗
- npm · package.json engines ↗
- nvm · .nvmrc ↗
- GitHub · actions/setup-node ↗
- Git · gitignore ↗
- npm · package-lock.json ↗
- Node.js · --max-old-space-size ↗
- Next.js · Environment variables ↗
- Vite · Env variables and modes ↗
Compara soluciones cloud
Revisa tarifas, límites, condiciones y fuentes de cada opción.
Abrir comparador