Arquitectura · 28 de octubre de 2026 · Daniel Murillo
Contenedores y Twelve-Factor: de tu máquina a producción
Un contenedor hace más reproducible el entorno; no convierte solo la app en un servicio seguro, escalable y fácil de operar.
Un contenedor hace más reproducible el entorno; no convierte por sí solo una aplicación en un servicio seguro, escalable o fácil de operar.
Introducción
«En mi máquina funciona» suele señalar diferencias entre entornos: dependencias, configuración, permisos, secretos o servicios externos.
Docker empaqueta la app y dependencias de sistema. Twelve-Factor aporta principios para configurar y operar servicios. Ninguno reemplaza diseño, seguridad ni operación.
1. Imagen, contenedor y configuración
| Concepto | Qué es |
|---|---|
| Imagen | Plantilla inmutable (capas, files, deps) |
| Contenedor | Proceso aislado que ejecuta una imagen |
| Volumen | Persistencia o datos compartidos cuando hace falta |
| Config | Valores por entorno (URLs, límites, flags) |
| Secreto | Credencial con acceso controlado y rotación |
No metas secretos en la imagen ni en capas del build. Las variables de entorno inyectan config, pero no son un vault: controla quién puede leerlas en la plataforma.
2. Dockerfile multi-stage para Node.js
Usa una rama LTS deliberada. A finales de octubre de 2026, Node.js 26 entra en Active LTS; 24 está en mantenimiento. El ejemplo fija node:24-bookworm-slim como base conservadora; actualiza cuando tu matriz de dependencias lo permita.
FROM node:24-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:24-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json /app/package-lock.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Buenas prácticas: pin de versión (o digest), .dockerignore, escanear vulnerabilidades, poca superficie en runtime, no copiar .env.
node_modules
dist
.git
.env
.env.*
coverage
Si usas Bun en desarrollo, el runtime de producción puede seguir siendo Node o Bun según lo que hayas validado; no mezcles lockfiles y comandos sin intención.
3. Twelve-Factor (lo que más pesa)
Consulta la lista completa. En la práctica, estos factores evitan dolores:
- Config fuera del código
- Dependencies explícitas
- Build / release / run separados
- Procesos sin estado local persistente
- Logs como stream (stdout/stderr)
- Disposability: arranque rápido y apagado ordenado
- Dev/prod parity razonable
No es dogma religioso: es una checklist de diseño operativo.
4. Configuración que falla rápido
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
export const config = {
port: Number(process.env.PORT ?? 3000),
databaseUrl: requiredEnv("DATABASE_URL"),
};
Valida al inicio (tipos, URLs, rangos). No imprimas connection strings con credenciales.
5. Liveness vs readiness
| Probe | Pregunta | Error típico |
|---|---|---|
| Liveness | ¿El proceso está vivo? | Reiniciar en bucle por un fallo externo |
| Readiness | ¿Puede recibir tráfico? | Seguir enviando requests a una instancia rota |
app.get("/health/live", (_req, res) => {
res.status(200).json({ status: "alive" });
});
app.get("/health/ready", async (_req, res) => {
try {
await database.query("SELECT 1");
res.status(200).json({ status: "ready" });
} catch {
res.status(503).json({ status: "not_ready" });
}
});
No hagas que liveness dependa de cada dependencia: a veces quieres marcar not ready sin matar el proceso.
6. Apagado graceful
const server = app.listen(config.port);
process.on("SIGTERM", () => {
server.close(async () => {
await database.end();
process.exit(0);
});
});
En producción: timeouts de drenaje, keep-alive, colas y trabajo en curso. Ni apagado inmediato ciego ni espera infinita.
7. Seguridad básica
- Usuario no root (
USER node) - Imagen base mantenida y actualizada
- Superficie mínima en runtime
- Escaneo de imagen y dependencias
- Límites de CPU/memoria
- Sin secretos en capas, logs ni repo
- No montar el socket Docker en apps sin necesidad extrema
Un contenedor no es sandbox absoluto: importa el host, capacidades y políticas.
8. ¿Compose, PaaS o Kubernetes?
| Situación | Dirección razonable |
|---|---|
| App pequeña / equipo chico | Compose o PaaS administrado |
| Varios servicios con ops maduro | Plataforma según redes, deploys y costo |
| Kubernetes | Solo si la complejidad operativa se justifica |
Elige lo que el equipo puede operar, no solo lo que brilla en un diagrama.
9. Errores frecuentes
- Persistencia solo en el filesystem del contenedor
- Una imagen distinta por entorno solo por config
- Root innecesario
.envdentro de la imagen- Ignorar
SIGTERM - Un solo health check para todo
- Sin límites de recursos
- Kubernetes «por si acaso»
10. Checklist de producción
- Imagen reproducible
- Secretos fuera de la imagen
- Config validada al arrancar
- Permisos mínimos
- Liveness ≠ readiness
- Apagado graceful
- Límites de recursos
- Logs observables
- Contenedor reemplazable sin perder datos críticos
- Deploy + rollback ensayados
Conclusión
Los contenedores estandarizan build y ejecución. Twelve-Factor ordena config, procesos y logs. La confiabilidad aparece al combinarlos con seguridad, observabilidad, pruebas y una plataforma acorde al tamaño del problema.
No elijas la infraestructura más compleja por anticipado. Elige la que puedas operar bien hoy.
En esta serie
- Autenticación
- Observabilidad
- SQL, query builders y ORMs
- Cómo probar una API
- Contenedores y Twelve-Factor (esta nota)