Daniel Murillo
Índice

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

ConceptoQué es
ImagenPlantilla inmutable (capas, files, deps)
ContenedorProceso aislado que ejecuta una imagen
VolumenPersistencia o datos compartidos cuando hace falta
ConfigValores por entorno (URLs, límites, flags)
SecretoCredencial 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

ProbePreguntaError 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ónDirección razonable
App pequeña / equipo chicoCompose o PaaS administrado
Varios servicios con ops maduroPlataforma según redes, deploys y costo
KubernetesSolo 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
  • .env dentro 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

  1. Autenticación
  2. Observabilidad
  3. SQL, query builders y ORMs
  4. Cómo probar una API
  5. Contenedores y Twelve-Factor (esta nota)

Lecturas relacionadas

Referencias