El espectro del despliegue
No existe una única “nube”. Existe un espectro basado en cuánto control tienes sobre la máquina, y cada producto se ubica en algún punto de ese espectro.
Un servidor privado virtual (VPS) es una máquina que alquilas y administras. Tú instalas el runtime, configuras un reverse proxy, gestionas los certificados TLS y mantienes el OS actualizado. Es barato, flexible y es completamente tuyo para romperlo. Es la opción correcta cuando necesitas un runtime específico o quieres entender cada capa.
Una plataforma como servicio (PaaS) como Railway, Render o Fly.io toma tu código o imagen y lo ejecuta. Tú declaras un puerto y un health check; la plataforma se encarga del TLS, el enrutamiento, los reinicios y el escalado. Aquí es donde la mayoría de los equipos pequeños deberían empezar.
Contenedores gestionados como AWS ECS, Google Cloud Run o Azure Container Apps ejecutan una imagen de contenedor con más opciones de configuración que un PaaS: networking, roles de IAM, reglas de autoscaling. En cuanto a carga operativa, se sitúan entre un PaaS y un clúster.
Funciones serverless como AWS Lambda o Cloudflare Workers ejecutan una función por solicitud, escalan a cero y facturan por invocación. Son excelentes para tareas cortas con picos de tráfico, pero incómodas para conexiones prolongadas y uso intensivo de CPU.
Kubernetes es un orquestador de propósito general. Ofrece el mayor control y la mayor superficie de gestión. Está justificado cuando ejecutas muchos servicios con un equipo de plataforma; es excesivo para una sola API.
La forma de elegir es comenzar por el extremo gestionado y avanzar hacia el control solo cuando aparezca una limitación concreta. El error más común es adoptar Kubernetes para un solo servicio porque parece la opción más “seria”.
| Opción | Tú gestionas | Ideal para | Ten cuidado con |
|---|---|---|---|
| VPS | OS, runtime, proxy, TLS | Control total, runtimes personalizados | Parches, failover, guardias (on-call) |
| PaaS | Solo la aplicación | Equipos pequeños, iteración rápida | Menos control, coste por unidad |
| Contenedores gestionados | Imagen, IAM, reglas de escalado | Servicios que requieren ajustes finos | Más configuración que un PaaS |
| Serverless | Una función a la vez | Tareas cortas con picos de tráfico | Cold starts y límites de tiempo |
| Kubernetes | Todo por encima del kernel | Muchos servicios, equipos de plataforma | Carga operativa real |
Lo que realmente compras con un servicio “managed”
La palabra managed oculta una lista de tareas que dejan de ser tu responsabilidad.
- Parches. El kernel, el runtime y la imagen base se actualizan sin necesidad de que programes una ventana de mantenimiento.
- TLS. Los certificados se emiten y renuevan automáticamente, y el tráfico HTTP se redirige a HTTPS.
- Escalado. Se añaden instancias cuando el CPU o el número de peticiones superan un umbral, y se eliminan cuando este desciende.
- Salud y reinicios. Un proceso que falla o no pasa su health check es reemplazado automáticamente sin intervención humana.
- Logs y métricas. La salida se recolecta de forma centralizada y es consultable, en lugar de vivir en un archivo dentro de una máquina a la que debes entrar por SSH.
- Backups. Las bases de datos managed realizan snapshots y soportan point-in-time recovery.
Lo que sacrificas es el control y cierta eficiencia de costes. No puedes tunear el kernel, instalar paquetes del sistema arbitrarios ni exprimir hasta el último dólar de una máquina. Para la mayoría de los equipos, es un intercambio justo: la alternativa es una rotación de guardias (on-call) para una infraestructura en la que no eres especialista.
La excepción son los datos. PostgreSQL, Redis y el almacenamiento de objetos managed casi siempre valen la pena. Construir tú mismo sistemas fiables de backups, failover y replicación es un proyecto en sí mismo, y la versión managed suele ser más barata que las horas de ingeniería que ahorra.
Pensamiento de los doce factores
La aplicación de los doce factores es una lista de verificación antigua que aún describe con precisión el despliegue cloud-native. Tres de sus factores son los más importantes en el día a día.
Configuración en el entorno. Cualquier cosa que varíe entre despliegues —URLs de bases de datos, API keys, feature flags— proviene de variables de entorno, no de archivos committeados en el repositorio. Esto es lo que permite que una sola imagen se ejecute en cualquier entorno.
DATABASE_URL=postgres://user:pass@host:5432/app
REDIS_URL=redis://host:6379
PORT=8080
Procesos sin estado. Una instancia no mantiene ningún estado duradero. Puede iniciarse, detenerse, duplicarse y destruirse libremente. Las sesiones, las subidas de archivos y las cachés residen en servicios externos.
Logs como flujos de eventos. La aplicación escribe líneas estructuradas en stdout y no hace nada más con ellas. La plataforma se encarga de recolectarlas, almacenarlas y buscarlas. Sin archivos de log que rotar, sin discos que llenar.
Vale la pena mencionar un cuarto factor: desechabilidad. Iniciar rápido y apagarse elegantemente. Un proceso que tarda un minuto en arrancar hace que el escalado y los despliegues sean lentos; uno que ignora SIGTERM pierde las solicitudes que están en curso.
Creando una imagen de contenedor ligera
Una buena imagen de producción es pequeña, reproducible y se ejecuta con un usuario que no sea root. Un build multi-etapa (multi-stage build) logra las tres cosas.
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
La etapa de build instala todo, compila y luego elimina las dependencias de desarrollo. La etapa de runtime copia únicamente node_modules, dist y el manifiesto. TypeScript, los frameworks de testing y los archivos fuente nunca llegan a producción.
USER node elimina los privilegios de root, evitando que un proceso comprometido pueda escalar privilegios fácilmente. NODE_ENV=production desactiva el comportamiento de desarrollo y habilita las optimizaciones del framework. Fija la imagen base a una variante específica y prefiere bookworm-slim o una imagen distroless sobre una instalación completa de Debian para reducir la superficie de ataque.
Añade un .dockerignore para que el contexto del build se mantenga pequeño:
node_modules
.git
dist
.env
Una imagen más pequeña se descarga más rápido, lo que reduce directamente los cold starts y los tiempos de despliegue.
Configuración del entorno y secretos
La configuración debe ser inyectada, nunca integrada en el código. La misma imagen debe ejecutarse tanto en staging como en producción, cambiando únicamente el entorno.
Las plataformas difieren en el mecanismo, pero la estructura es la misma. Un archivo de configuración declara los valores que no son secretos y un almacén de secretos guarda los datos sensibles.
[env]
NODE_ENV = "production"
LOG_LEVEL = "info"
[[services]]
internal_port = 8080
Los secretos se configuran por separado y nunca se suben al repositorio:
fly secrets set DATABASE_URL=postgres://...
fly secrets set STRIPE_SECRET_KEY=sk_live_...
En Kubernetes, los secretos llegan como variables de entorno o archivos montados:
envFrom:
- secretRef:
name: api-secrets
Dos hábitos mantienen esto seguro. Primero, valida la configuración al iniciar y falla rotundamente si falta algo, en lugar de lanzar un error en la primera solicitud que lo necesite. Segundo, prefiere el acceso basado en identidad: una identidad de carga de trabajo o un rol de instancia permite que la aplicación obtenga credenciales de corta duración sin necesidad de ningún secreto estático.
import { z } from "zod";
const Env = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
});
const parsed = Env.safeParse(process.env);
if (!parsed.success) {
console.error("invalid configuration", parsed.error.flatten().fieldErrors);
process.exit(1);
}
export const config = parsed.data;
El proceso se niega a iniciar con un entorno incompleto, lo que convierte una categoría de sorpresas en tiempo de ejecución en un fallo de despliegue inmediato y obvio. Nunca registres el entorno en sí mismo: una sola línea de depuración que vuelque process.env puede filtrar cada secreto que posea el servicio.
Lanzamientos sin tiempo de inactividad y health checks
Un despliegue que pierde solicitudes es un despliegue que los usuarios notan. Los lanzamientos sin tiempo de inactividad (zero-downtime releases) dependen de dos cosas: iniciar nuevas instancias antes de detener las antiguas y saber cuándo una nueva instancia está realmente lista.
Readiness significa que la instancia puede atender tráfico. Se ha conectado a la base de datos, ha calentado sus cachés y ha terminado de iniciar. Solo entonces el load balancer debería redirigir tráfico hacia ella.
Liveness significa que la instancia sigue estando saludable. Si falla repetidamente, la plataforma la mata y la reemplaza.
app.get("/healthz", async (req, res) => {
try {
await db.query("select 1");
res.status(200).json({ status: "ok" });
} catch {
res.status(503).json({ status: "unhealthy" });
}
});
Mantén el health check ligero y honesto. Verificar la base de datos es razonable; llamar a cinco servicios downstream no lo es, porque convierte un pequeño fallo en otro lugar en un bucle de reinicios. Si una dependencia es opcional, reporta que está saludable y degrada el servicio elegantemente.
El apagado gradual (graceful shutdown) es la otra mitad. Al recibir SIGTERM, deja de aceptar nuevas conexiones, finaliza las solicitudes en curso, cierra el pool de la base de datos y sal. Dale a la plataforma un periodo de gracia ligeramente superior al de la solicitud más lenta.
process.on("SIGTERM", () => {
server.close(async () => {
await db.end();
await redis.quit();
process.exit(0);
});
setTimeout(() => process.exit(1), 15_000).unref();
});
El timeout es un respaldo: si algo se queda colgado, el proceso sigue saliendo antes de que expire el periodo de gracia de la plataforma y esta recurra a SIGKILL.
Escalado horizontal y statelessness
Escalar horizontalmente significa ejecutar más copias de la misma instancia. Esto solo funciona si las instancias son intercambiables, lo que requiere que ninguna de ellas mantenga un estado único.
El estado debe residir en servicios diseñados para ello:
- Sesiones en Redis, no en memoria ni únicamente en una cookie firmada.
- Cargas de archivos en almacenamiento de objetos como S3 o R2, no en el disco local.
- Cachés en Redis o un CDN, no en un mapa local del proceso.
- Tareas en segundo plano en una cola con workers dedicados, no en un temporizador dentro del proceso web.
Una vez que el estado es externo, escalar es cuestión de números. La plataforma añade instancias bajo carga y las elimina cuando esta disminuye, y cualquier instancia puede atender cualquier solicitud.
El autoscaling necesita una señal. La CPU es el valor predeterminado común, pero para servicios de Node.js limitados por I/O, la concurrencia de solicitudes o la profundidad de la cola suelen reflejar mejor la carga. Escala basándote en la métrica que realmente prediga la saturación, y establece siempre un número mínimo de instancias para que el servicio sobreviva a un pico de tráfico mientras arrancan las nuevas instancias.
Recuerda el problema de las conexiones: cada instancia abre su propio pool de base de datos. Veinte instancias con un pool de veinte necesitan cuatrocientas conexiones, algo que PostgreSQL no permitirá. Limita el pool por instancia y coloca un pooler frente a la base de datos.
Postgres y Redis gestionados
La base de datos es la parte del stack menos apta para gestionarla por cuenta propia y, aun así, la que más tienta a hacerlo. Un Postgres gestionado te ofrece backups automatizados, recuperación en un punto temporal (point-in-time recovery), un standby de failover y, a menudo, réplicas de lectura, sin ninguna de las tareas operativas.
Dos reglas mantienen el sistema saludable. Primero, dimensiona las conexiones deliberadamente. Configura max en el pool con un número pequeño por instancia y utiliza un pooler como PgBouncer para despliegues de múltiples instancias. Segundo, ejecuta las migraciones como un paso del release, no al iniciar la aplicación. Si cada instancia migra al arrancar, un despliegue progresivo (rolling deploy) ejecutará la misma migración cinco veces de forma concurrente.
# run once, before the new version starts
npm run db:migrate
Un Redis gestionado es el hogar natural para las sesiones, los contadores de rate-limit y las colas de trabajos. Activa la persistencia si los datos son importantes y trata la instancia como una dependencia compartida cuya latencia afecta a cada solicitud. Mantén la instancia en la misma región que la aplicación; un salto entre regiones en cada lectura de caché es un impuesto que notarás.
DNS, TLS y dominios personalizados
El DNS mapea un nombre a la plataforma y el TLS hace que la conexión sea confiable. Ambos están ampliamente automatizados hoy en día, pero los detalles siguen siendo importantes.
Apunta un registro A o ALIAS a la dirección de la plataforma, o un CNAME a su hostname. Utiliza un TTL corto durante la migración para que cualquier error se pueda corregir rápidamente y luego auméntalo. Mantén el dominio apex y el host www consistentes, redireccionando uno al otro para que los enlaces y las cookies no se fragmenten entre dos orígenes.
El TLS es emitido y renovado automáticamente por la mayoría de las plataformas. Fuerza el uso de HTTPS, habilita HSTS una vez que estés seguro y asegúrate de que la aplicación confíe en los headers de forwarding del proxy para que genere URLs https y detecte la IP real del cliente.
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
Cuando ejecutes tu propio reverse proxy, la guía de Nginx cubre estos headers, la terminación de TLS y la configuración de upstream en detalle. Configurarlos incorrectamente provoca bucles de redireccionamiento y que los rate limiters detecten que cada solicitud proviene de la IP del proxy.
Observabilidad y alertas
No puedes operar aquello que no puedes ver. Tres tipos de señales cubren la mayoría de las necesidades.
Los Logs son eventos estructurados. Emite JSON con un nivel, un mensaje y un request id para que puedan ser filtrados y correlacionados. Escribe en stdout y deja que la plataforma los recolecte.
console.log(JSON.stringify({ level: "info", msg: "request", id, path, ms }));
Las Metrics son números a lo largo del tiempo: tasa de solicitudes, tasa de errores, percentiles de latencia, CPU, memoria y profundidad de la cola. Rastrea las cuatro señales doradas —latencia, tráfico, errores y saturación— para cada servicio.
Los Traces siguen una solicitud a través de los servicios y hacen visible un fan-out lento. Son más importantes a medida que crece el número de servicios; para una sola API, buenos logs y metrics suelen ser suficientes.
Configura alertas basadas en síntomas que el usuario perciba, no en las causas. Un aumento en la tasa de errores o una latencia p95 por encima de un umbral es motivo suficiente para despertar a alguien. Un uso elevado de CPU que no ha afectado las solicitudes es una nota para el dashboard, no una alerta urgente. Cada alerta debe ser accionable, o de lo contrario, acostumbrarás a la gente a ignorarlas.
Conciencia de los costes
Las facturas de la nube crecen en los huecos entre decisiones. Los culpables habituales son predecibles.
El Egress suele ser la mayor sorpresa. Los datos que salen del proveedor se facturan, a veces considerablemente, mientras que los datos que entran suelen ser gratuitos. Sirve los assets pesados a través de un CDN, comprime las respuestas y mantén los servicios que se comunican frecuentemente en la misma región.
Los recursos inactivos son fáciles de olvidar: una base de datos de staging que se quedó encendida, volúmenes sin asignar, snapshots antiguos o instancias sobredimensionadas mantenidas “por si acaso”. Reduce la escala de los entornos que no sean de producción fuera del horario laboral.
El over-provisioning desperdicia dinero en la dirección opuesta. Ajusta el tamaño de las instancias midiendo el uso real y deja que el autoscaling gestione los picos en lugar de pagar por el peor escenario las 24 horas del día.
Compromisos de Serverless. Las funciones que escalan a cero son baratas cuando están inactivas, pero pueden resultar costosas bajo una carga constante en comparación con un contenedor pequeño siempre activo. Modela ambas opciones si el tráfico es predecible.
Configura una alerta de presupuesto en la cuenta. Una factura que se duplica silenciosamente es mucho peor que una notificación de que se ha superado un umbral.
Infraestructura como Código
Hacer clic en una consola está bien para el primer despliegue, pero se convierte en un riesgo después. La infraestructura como código registra el estado deseado en archivos, permitiendo que los entornos sean reproducibles, revisables y recuperables.
Terraform y OpenTofu describen los recursos de forma declarativa. Pulumi utiliza lenguajes de programación reales. Los manifiestos de Kubernetes y los archivos de configuración de PaaS son formas más simples de la misma idea. La herramienta importa menos que la práctica: la definición reside en el control de versiones y es aplicada por un pipeline.
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = "db.t4g.small"
allocated_storage = 50
backup_retention_period = 7
}
Dos reglas hacen que la IaC sea segura. Mantén el estado remoto y bloqueado para que dos personas no puedan aplicar cambios conflictivos. Revisa los planes antes de aplicarlos, porque un plan que pretenda destruir una base de datos debería detener a un humano, no ejecutarse silenciosamente.
Para un único servicio de PaaS, un fly.toml o render.yaml commitado en el repositorio ya es infraestructura como código. Empieza por ahí y adopta una herramienta completa a medida que la superficie crezca.
Rollbacks y forward fixes
Cada despliegue necesita una vía de retorno. Debido a que el artefacto es inmutable y está etiquetado por commit, hacer un rollback consiste en volver a desplegar el digest anterior.
fly releases
fly deploy --image ghcr.io/me/app@sha256:previous
En Kubernetes, un rollout undo regresa a la revisión previa. En un PaaS, la mayoría de las plataformas mantienen una lista de releases y permiten volver a desplegar una con un solo clic.
Los rollbacks son rápidos, pero no siempre son suficientes. Si una migración de base de datos ya se ejecutó y cambió el esquema, el código antiguo aún debe ser capaz de leerlo. Es por eso que las migraciones deben ser compatibles hacia atrás durante al menos una release: añade columnas antes de usarlas, deja de usar columnas antes de eliminarlas y nunca combines un cambio destructivo con el código que depende de él en el mismo despliegue.
Cuando un rollback es imposible —como una migración de datos que no se puede revertir— la solución es un forward fix: lanzar la corrección rápidamente, siguiendo la misma disciplina del pipeline, en lugar de editar manualmente el entorno de producción.
Redes, regiones y latencia
El lugar donde residen tus instancias y datos determina qué tan rápida se siente la aplicación. Una solicitud que debe cruzar un océano para llegar a la base de datos conlleva al menos cien milisegundos de latencia antes de que se realice cualquier trabajo, y no hay optimización de la aplicación que pueda eliminarlo.
Mantén la aplicación, la base de datos y la caché en la misma región. Esta es la decisión con mayor impacto en la latencia y es gratuita. Coloca los activos estáticos detrás de un CDN para que se sirvan desde un edge cercano al usuario, y permite que solo las solicitudes dinámicas viajen al origen.
No expongas la base de datos a la internet pública. Utiliza la red privada de la plataforma para que solo los servicios en la misma red puedan acceder a ella, y permite el acceso mediante grupos de seguridad o reglas de firewall en lugar de un puerto abierto. Esto elimina toda una categoría de ataques y, por lo general, mejora la latencia al mismo tiempo.
El despliegue multi-región es un paso que la mayoría de los productos no necesitan. Multiplica los costos, complica la gestión de la base de datos e introduce el lag de replicación, donde un usuario que escribe en una región y lee en otra ve datos obsoletos. Comienza con una sola región y un CDN, mide la latencia que experimentan los usuarios reales y expande la infraestructura solo cuando una audiencia específica lo requiera.
Probar un despliegue antes de que los usuarios lo vean
Producción no debería ser el primer lugar donde se ejecute un cambio. Algunas capas de pruebas adicionales permiten detectar fallos que las unit tests no pueden capturar.
Los Preview environments levantan un despliegue completo por cada pull request, permitiendo que el revisor interactúe directamente con el cambio real. Las plataformas que los soportan transforman la revisión de código: ya no se trata solo de leer un diff, sino de usar la funcionalidad.
El entorno de Staging refleja la configuración de producción —mismo motor de base de datos, misma estructura de variables de entorno, mismo proxy— pero sin los datos de producción. Su objetivo es detectar aquel tipo de errores que solo aparecen cuando los componentes reales están conectados entre sí.
Los Smoke tests se ejecutan inmediatamente después de cada deploy y ponen a prueba el camino crítico: el endpoint de salud, un login, una lectura y una escritura. No son una suite de pruebas completa; son una confirmación rápida de que el release está operativo.
#!/usr/bin/env bash
set -euo pipefail
BASE="${1:-https://app.example.com}"
curl -fsS "$BASE/healthz" >/dev/null
curl -fsS "$BASE/api/version" | grep -q '"sha"'
echo "smoke test passed"
Los Feature flags desacoplan el despliegue del lanzamiento. El código se despliega “en la sombra” (dark launch), luego un flag lo activa para un solo usuario, después para un porcentaje y finalmente para todos. Si algo falla, el flag se desactiva en segundos sin necesidad de realizar un nuevo deploy.
El Load testing antes de un lanzamiento te indica dónde se encuentra el primer cuello de botella: conexiones, CPU o una consulta lenta. Realiza las pruebas con una concurrencia realista y monitorea la base de datos, no solo la API.
Mejores prácticas
- Comienza con servicios gestionados y muévete hacia el control total solo cuando aparezca una limitación.
- Construye una imagen multi-stage pequeña y ejecútala como un usuario no root.
- Inyecta toda la configuración desde el entorno; nunca la incluyas directamente en la imagen.
- Mantén cada instancia stateless; coloca las sesiones, subidas y cachés en almacenes gestionados.
- Expón un endpoint de readiness y liveness ligero.
- Gestiona
SIGTERMy cierra las conexiones antes de salir. - Ejecuta las migraciones como un paso del release y mantenlas compatibles hacia atrás.
- Limita las conexiones a la base de datos por instancia y usa un pool frente a Postgres.
- Etiqueta los releases por digest y mantén la versión anterior lista para un rollback.
- Escribe logs estructurados en stdout y genera alertas basadas en síntomas visibles para el usuario.
- Define la infraestructura en control de versiones y revisa los planes antes de aplicarlos.
- Configura una alerta de presupuesto y monitorea el egress.
Errores comunes
- Adoptar Kubernetes para un único servicio y dedicar todo el roadmap al clúster.
- Almacenar sesiones en memoria y luego preguntarse por qué los usuarios cierran sesión en cada deploy.
- Escribir archivos subidos en el disco local y perderlos cuando se reemplaza la instancia.
- Incluir la configuración del entorno dentro de la imagen y reconstruirla para cada entorno.
- Ejecutar migraciones al iniciar la aplicación, provocando que un rolling deploy las ejecute varias veces.
- Escalar instancias sin ajustar el límite de conexiones de la base de datos.
- Confiar ciegamente en los proxy headers, o no reenviarlos en absoluto.
- Dejar el tamaño predeterminado del connection pool de la base de datos en una flota de instancias.
- Configurar alertas basadas en la CPU en lugar de en los errores y la latencia que perciben los usuarios.
- Olvidar el egress y los recursos inactivos hasta recibir la primera factura sorprendente.
- No tener una ruta de rollback, o tener una que falla porque una migración no era compatible.
- Gestionar producción manualmente en una consola sin dejar registro de qué ha cambiado.
Próximos pasos
Esta guía es el destino del artefacto que CI/CD construye y promueve. La imagen en sí proviene de la guía de Docker, que cubre en profundidad los multi-stage builds y el almacenamiento en caché de capas. Cuando gestionas la terminación de TLS o el enrutamiento de tráfico por tu cuenta, la guía de Nginx muestra la configuración del proxy, y la guía de Linux explica el host y la shell que estás automatizando. En conjunto, cubren todo el camino desde un cambio committeado hasta un release en ejecución y observable.