Despliega una app generada por IA con Docker: compilaciones seguras y repetibles
Despliega una app generada por IA con Docker de forma segura: compilaciones repetibles, versiones fijadas, manejo de secretos y comprobaciones para evitar imágenes que solo funcionan en el builder.

Por qué las apps generadas por IA suelen fallar al desplegar
"Funciona solo en el builder" significa que la app se ejecuta en la máquina que construyó la imagen, pero falla en cuanto ejecutas esa misma imagen en CI, staging o producción. La build parece correcta, pero el contenedor depende de algo que en realidad no está dentro de la imagen.
Las apps generadas por IA se topan con esto con más frecuencia porque el código se arma a partir de patrones que asumen un entorno permisivo. Puede depender de herramientas instaladas globalmente en el ordenador del autor, de una base de datos local en el host o de un archivo que nunca se comprometió. A veces el Dockerfile "aprueba" porque copia demasiado del workspace, incluyendo caches, artefactos compilados o incluso secretos.
Normalmente verás los síntomas en cuanto intentes desplegar en un entorno limpio:
- Las builds pasan localmente pero fallan en CI con "command not found" o faltan librerías del sistema
- La app arranca y luego se cae porque falta una variable de entorno (o porque un secreto quedó incrustado en la imagen)
- La autenticación funciona en localhost pero falla en staging por URLs de callback o ajustes de cookies
- Faltan assets estáticos o migraciones porque el paso de build nunca se ejecutó dentro del contenedor
- Comportamiento aleatorio de "a veces funciona" causado por versiones sin fijar y dependencias cambiantes
El objetivo es simple: una imagen, mismo comportamiento, en cualquier lugar. Si el contenedor la necesita para ejecutarse, debe declararse, instalarse y configurarse dentro de los pasos de build y runtime, no tomarse prestado de la máquina del builder.
Elige la imagen base y fija versiones primero
Empieza por bloquear sobre qué se construye tu contenedor. La mayoría de los fallos de "funcionaba en mi laptop" ocurren porque la imagen base o el runtime cambiaron por debajo.
El mayor riesgo es usar :latest. Es cómodo, pero es un blanco en movimiento. Una pequeña actualización de la imagen base puede cambiar OpenSSL, libc, Python, Node o incluso el comportamiento del shell por defecto. Tu build puede pasar hoy y fallar la semana que viene, o peor, comportarse distinto en producción.
Elige una imagen base estable (y evita cambios ocultos)
Escoge una imagen base que puedas mantener estable durante meses. Fíjala a una versión específica y, cuando sea posible, a un digest. Fijar por digest te da exactamente los mismos bytes de imagen cada vez, no solo "Node 20" de hoy.
También fija la versión de tu runtime de lenguaje. "Node 20" no es lo mismo que "Node 20.11.1". Incluso cambios menores pueden romper módulos nativos, criptografía o scripts de build.
Decide la plataforma objetivo temprano (amd64 vs arm64)
Sé explícito sobre dónde correrá la imagen. Muchos builders usan Apple Silicon (arm64), mientras que muchos servidores usan amd64. Las dependencias nativas pueden compilarse de forma distinta y algunos paquetes no ofrecen binarios para arm64.
Ejemplo: una app Node instala una librería de procesamiento de imágenes. En arm64 se compila desde la fuente y pasa; en amd64 descarga un binario precompilado de distinta versión y falla en tiempo de ejecución.
Antes de escribir el resto del Dockerfile, fija esto:
- Versión de la imagen base (y digest si puedes)
- Versión del runtime (Node, Python, Java)
- Plataforma objetivo (amd64 o arm64)
Si heredaste un repo generado por IA y las versiones están cambiando, empieza por fijar la imagen base y el runtime. Eso elimina toda una categoría de fallos de despliegue "misteriosos".
Paso a paso: un Dockerfile que construye igual siempre
Las imágenes repetibles vienen de decisiones aburridas: fijar la etiqueta de la base, apoyarse en un lockfile y mantener pasos de build predecibles.
Aquí tienes un esqueleto simple de Dockerfile para una app Node típica (API o full-stack) que mantiene el cache efectivo e instala de forma determinista:
# Pin the exact base image version
FROM node:20.11.1-alpine3.19
WORKDIR /app
# Copy only dependency files first (better caching)
COPY package.json package-lock.json ./
# Deterministic install based on the lockfile
RUN npm ci --omit=dev
# Now copy the rest of the source
COPY . .
# Build only if your app has a build step
RUN npm run build
EXPOSE 3000
# Clear, explicit start command
CMD ["node", "server.js"]
# Add a simple healthcheck only if you can keep it stable
# HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
Tres detalles importan más de lo que la gente espera:
- Copia primero los archivos de dependencias, instala y luego copia el resto. Evitas reinstalar paquetes cada vez que cambias un archivo de código.
- Usa el comando de instalación del lockfile (como
npm ci) para obtener las mismas versiones de dependencias en cada build. - Mantén el comando de inicio directo. Evita scripts "mágicos" que se comportan distinto entre entornos.
Un caso común de fallo es una app que funciona localmente porque lee silenciosamente un archivo .env y depende de herramientas instaladas globalmente. En Docker, ambos faltan, así que la build falla o el contenedor arranca y se cae de inmediato. Este patrón te fuerza a declarar lo que necesitas, que es la idea central.
Usa builds multi-etapa para evitar imágenes de runtime hinchadas
Los builds multi-etapa separan "build" de "runtime". Compilas la app en una imagen (con herramientas pesadas) y luego copias solo el resultado final a una segunda imagen más limpia que es la que realmente ejecutas.
Esto importa con apps generadas por IA porque a menudo incorporan compiladores, CLIs y caches sin que te des cuenta. Si eso llega a producción, la imagen se vuelve enorme, más lenta de distribuir y más difícil de razonar.
Etapa de build vs etapa de runtime (en lenguaje sencillo)
En la etapa de build instalas herramientas de compilación y dependencias de desarrollo: compiladores TypeScript, bundlers y cualquier cosa necesaria solo para crear la app final.
En la etapa de runtime conservas solo lo que la app necesita para ejecutarse. Eso hace la imagen final más pequeña y predecible. También evita que una dependencia "funcione" solo porque una herramienta de build estaba accidentalmente presente.
Una pregunta útil: si la app ya está compilada, ¿sigo necesitando este paquete? Si no, pertenece a la etapa de build.
El fallo más habitual
La gente construye con éxito y luego olvida copiar algo en la etapa de runtime. Lo que falta suele ser assets construidos (como dist/), plantillas/configs de runtime o módulos instalados. Los permisos también pueden dar problemas: la app funciona localmente y luego falla en el contenedor porque no puede leer o escribir un directorio.
Después de construir la imagen, ejecútala como si fuera producción. Confirma que puede arrancar solo con variables de entorno y una conexión real a la base de datos. Si necesita algo más, probablemente se quedó en la etapa de build.
Haz las instalaciones de dependencias deterministas
Las instalaciones de dependencias son donde las builds se desvían primero. Los proyectos generados por IA suelen funcionar en la máquina del creador porque accidentalmente instalaron paquetes más nuevos, reutilizaron caches o tiraron de una rama Git móvil.
Los lockfiles son tu red de seguridad. Compromételos y haz que tu build Docker los use a propósito. Para Node, eso suele significar package-lock.json con npm ci, o pnpm-lock.yaml con una instalación congelada. Para Python, usa poetry.lock (Poetry) o requirements completamente fijados y evita "latest".
Una regla ahorra mucho dolor: nunca instales desde referencias flotantes como main, master o una etiqueta sin fijar. Si debes tirar de Git, fíjalo a un SHA de commit específico.
Los paquetes privados son otra trampa. No incrustes tokens en la imagen. Usa secretos de build para que el build pueda acceder a registros privados sin dejar credenciales en las capas. Después del paso de instalación, la imagen final no debe contener .npmrc, configuración de pip ni archivos de autenticación.
También haz explícitos los scripts de build. Algunos proyectos generados dependen de comportamientos ocultos postinstall que descargan binarios o ejecutan generación de código. Si algo debe ejecutarse, hazlo como un paso de build claro y que falle abiertamente.
Maneja secretos de forma segura (sin romper el desarrollo local)
Si una app funciona localmente pero falla o se expone, los secretos suelen ser la causa. Una imagen Docker se comparte, cachea y almacena en registros. Cualquier cosa dentro puede filtrarse.
Nunca incrustes valores sensibles en la imagen, ni "solo para pruebas". Eso incluye claves de API, secretos de clientes OAuth, contraseñas de bases de datos, claves JWT y certificados privados.
Una regla simple: pasa los secretos en tiempo de ejecución, no durante docker build. Los valores de build pueden quedarse en capas de imagen y registros, especialmente si un Dockerfile generado usa ARG y luego lo imprime, lo escribe en un archivo de configuración o lo incrusta en código frontend empaquetado.
En su lugar, deja que la plataforma de despliegue inyecte secretos cuando el contenedor arranque (su gestor de secretos, ajustes de variables de entorno o un almacén cifrado). Mantén el Dockerfile enfocado en instalar dependencias y copiar código, no en cablear credenciales.
Para desarrollo local, mantenlo conveniente sin comprometer secretos:
- Usa un archivo
.envlocal y añádelo a.gitignore - Compromete un
.env.examplecon nombres de variables de ejemplo (sin valores reales) - Falla rápido con un error claro cuando falta una variable de entorno requerida
Ejemplo: localmente usas DATABASE_URL desde .env. En producción pones el mismo DATABASE_URL en los ajustes secretos del host. Mismo flujo de código, valores distintos, nada sensible dentro de la imagen.
Builds repetibles: etiquetado y seguimiento de cambios
Trata cada build de imagen como un recibo fechado. El mayor error es reutilizar una etiqueta como latest (o incluso v1) para contenidos distintos. Ahí es donde aparecen los despliegues de "ayer funcionaba".
Una etiqueta debe significar un conjunto exacto de bits. Usa etiquetas que te digan qué código enviaste y que sean difíciles de cambiar en secreto.
Un patrón práctico es una versión de release para humanos (como v1.4.2) más un SHA de commit para precisión (como sha-3f2c1a9). Si usas una etiqueta de entorno como prod, que sea un puntero a una versión inmutable.
Para hacer las builds auditables, registra las entradas que afectan la imagen final. Guárdalo en un lugar que tu equipo lea (un archivo corto BUILDINFO, notas de release o labels de imagen):
- Digest de la imagen base (no solo
node:20, el digest exacto) - Hash del lockfile (
package-lock.json,yarn.lock,poetry.lock, etc.) - Comando de build y args clave (por ejemplo
NODE_ENV=production) - Versión de migraciones (si la app cambia la base de datos)
Una guía útil para decidir cuándo reconstruir:
- Solo cambio de código: reconstruir la capa de app, dependencias sin cambiar si el lockfile no cambió
- Cambio en el lockfile: reconstruir dependencias y app, volver a ejecutar tests
- Cambio de digest de la imagen base: reconstruir todo y volver a probar
- Cambio de secretos: rotar en tiempo de despliegue (no reconstruir la imagen)
Endurecimiento básico de contenedores para no expertos en seguridad
Si puedes empaquetar una app en un contenedor, también puedes hacer que sea más difícil de comprometer. No necesitas habilidades avanzadas de seguridad. Unos valores por defecto cubren la mayoría de los problemas reales en imágenes apresuradas.
Empieza evitando root cuando puedas. Muchos Dockerfiles generados ejecutan todo como root porque "simplemente funciona". En producción, eso convierte un pequeño bug en un incidente mayor. Crea un usuario, asigna la propiedad de la carpeta de la app y ejecuta el proceso como ese usuario.
Los permisos importan también. Cuando un contenedor no puede escribir un archivo, la solución rápida común es chmod -R 777. Eso suele crear un problema mayor. Decide qué carpetas deben ser escribibles (logs, uploads, archivos temporales) y da solo esos permisos de escritura.
Si conservas herramientas de build en la imagen final, también estás dando más herramientas a un atacante. Los builds multi-etapa ayudan porque compiladores y gestores de paquetes quedan en la etapa de build.
Trampas comunes que causan imágenes que solo funcionan en el builder
Una imagen que solo funciona para quien la construyó y luego falla en CI o producción suele deberse a que el Dockerfile depende de la configuración de tu laptop.
Los culpables habituales:
- Un
.dockerignoreque oculta algo que en realidad necesitas. A veces se ignoradist/,prisma/,migrations/o incluso un lockfile. - Herramientas globales requeridas accidentalmente. Si el build asume que
tsc,vite,pnpmo Poetry están instalados globalmente, puede "funcionar por accidente" en una máquina y fallar en todas las demás. - Módulos nativos que se comportan distinto entre plataformas. Cualquier cosa que compile código nativo puede romperse si el builder usa macOS/Windows y producción usa Linux.
- Variables de entorno de build usadas como configuración de runtime. Incrustar
API_URL, ajustes de autenticación o flags de feature en el build puede hacer que la imagen funcione en un entorno y falle en otro.
Una comprobación rápida: reconstruye sin cache y con un contexto limpio, luego ejecuta el contenedor con solo las variables de entorno que piensas poner en producción.
Chequeos rápidos antes de desplegar que puedes hacer en 10 minutos
Antes de enviar, haz una pasada que imite producción. Estos chequeos atrapan las sorpresas más comunes.
- Reconstruye desde cero una vez (cache deshabilitado) para no depender de capas antiguas.
- Arranca el contenedor con solo las variables de entorno requeridas y confirma que la falta de config falle con claridad.
- Ejecuta sin bind mounts. La imagen debe contener todo lo que necesita.
- Verifica el orden de arranque: migraciones antes del proceso web, y scripts de seed (si hay) son seguros.
- Escanea los logs en busca de config faltante, errores de permisos y errores de conexión a la base de datos.
Un fallo típico: localmente la app parece bien porque tenías claves extras en .env y un bind mount que ocultaba artefactos de build faltantes. En producción arranca, intenta escribir uploads en una carpeta que no existe, las migraciones no se ejecutan y solo ves un vago "500". Una build sin cache más una ejecución con variables mínimas normalmente lo deja ver en minutos.
Ejemplo: de éxito local a imagen Docker segura para producción
Una historia común es generar una pequeña web con una herramienta de IA, ejecutarla bien en tu laptop y luego verla fallar en un VPS o servicio gestionado. Localmente tienes la versión correcta de Node, un .env completo y un cache caliente. En producción, el contenedor arranca en frío, sin archivos ocultos y sin configuración interactiva.
Para diagnosticar rápido, compara las versiones de runtime primero. Si tu imagen usa node:latest o python:3, estás aceptando cambios silenciosos. Luego, revisa variables de entorno faltantes: claves de autenticación, URLs de base de datos y callbacks OAuth suelen existir en tu máquina pero no en el entorno de despliegue. Finalmente, confirma que exista el output del build. Muchos proyectos dependen de un paso de build local, pero la imagen Docker solo copia el código, así que no hay nada que servir.
Un camino práctico de arreglo suele ser:
- Fijar la imagen base y las versiones del runtime.
- Comprometer y exigir un lockfile para la instalación.
- Mover secretos a variables de entorno en runtime (no incrustados en la imagen ni copiados desde
.env). - Usar builds multi-etapa para que la imagen de runtime contenga solo el output y dependencias de producción.
El éxito es ver la misma etiqueta de imagen ejecutándose localmente, en CI y en producción sin pasos de "solo pon este archivo".
Próximos pasos si tu app generada por IA aún no despliega
Si ya probaste lo básico y sigue fallando, deja de adivinar y escribe una definición simple de "deployment done". Manténlo corto:
- Versiones exactas de runtime (imagen base, runtime, gestor de paquetes)
- Cómo se proveen los secretos en runtime (y qué nunca debe incrustarse en la imagen)
- Una prueba de humo y un health check que puedas ejecutar después del deploy
- Reglas de etiquetado de releases (una etiqueta por build, ligada a un commit)
- Dónde mirar logs primero
Luego decide si arreglas lo que tienes o reconstruyes desde cero. Arregla la base cuando el flujo principal funciona y los fallos son mayormente empaquetado y config. Reconstruye cuando los flujos básicos siguen rompiendo, los modelos de datos no están claros o cada cambio causa nuevos errores.
Si ves fallos solo en producción como autenticación rota, secretos expuestos, arquitectura enmarañada o problemas de seguridad obvios (incluida inyección SQL), suele ser más rápido pedir una segunda opinión pronto. FixMyMess (fixmymess.ai) se enfoca en diagnosticar y reparar codebases generadas por IA para que se comporten de forma consistente en producción, empezando con una auditoría de código gratuita que señala qué está realmente fallando.