wandres.dev
CI/CD PARA FRONTEND · pipeline y deploy

Deploy: edge, CDN o contenedor

El destino de un despliegue se sigue de cómo y cuándo la aplicación produce su HTML, no al revés. Un sitio estático se sube a un CDN que sirve archivos inmutables desde el borde; una app con render en servidor puede correr al edge, en isolates que se ejecutan por petición cerca del usuario pero con un runtime restringido; y una app que exige un proceso vivo se empaqueta en un contenedor Docker sobre un VPS que controlas por completo a cambio de cargar con toda su operación. Elegir entre las tres es elegir dónde vive tu cómputo y quién paga su complejidad.

⏱ 17 min

El destino de un despliegue no es un detalle de infraestructura que se decide al final, sino la consecuencia directa de cómo y cuándo tu aplicación produce su HTML. Existen tres familias de destinos, y cada una encaja con una naturaleza de render distinta. Un sitio estático se sube a un CDN, que sirve archivos ya construidos desde el borde de la red sin ejecutar nada por petición. Una app con render en servidor puede desplegarse al edge, donde una función se ejecuta cerca del usuario en cada petición, pero dentro de un runtime restringido. Y una app que necesita un proceso vivo y persistente se empaqueta en un contenedor Docker y corre en un VPS que controlas de arriba abajo. Elegir entre las tres no es cuestión de moda ni de precio aislado: es responder con precisión una sola pregunta, dónde y cuándo se genera cada byte de tu respuesta, y aceptar quién paga la complejidad que esa respuesta arrastra.

🎯 Al terminar esta lección sabrás
  • Derivar el destino de despliegue de la naturaleza de render de la app, y no al contrario.
  • Desplegar un sitio estático a un CDN entendiendo su modelo de assets inmutables y su propagación.
  • Desplegar render en servidor al edge con Workers o funciones, respetando los límites de su runtime.
  • Contenerizar con Docker sobre un VPS, sopesando el control total contra el coste operativo.

El destino se sigue del render

Antes de mirar plataformas conviene fijar el eje que las ordena, porque sin él la elección parece un menú de marcas y en realidad es una consecuencia técnica. Dos preguntas bastan: cuándo se produce el HTML y dónde corre el código que lo produce. Un sitio generado estáticamente resuelve su HTML en build time: cuando el build termina, cada página ya existe como un archivo, y desplegar es solo mover archivos. Una app con render en servidor resuelve su HTML en request time: no hay archivo previo, hay un handler que se ejecuta en cada petición, y ese handler tiene que vivir en algún sitio capaz de ejecutar código. La segunda pregunta, la de dónde, parte ese “algún sitio” en dos: un runtime efímero y distribuido en el borde, o un proceso persistente en un servidor de origen.

De ese eje caen las tres familias sin ambigüedad, y también los híbridos. Frameworks como Astro permiten decidir el modo por ruta: una landing y un blog se prerenderizan a estático mientras un panel autenticado se sirve por SSR, de modo que la misma app reparte cada ruta al destino que le corresponde. La pregunta operativa deja de ser “estático o servidor” en bloque y pasa a ser, ruta por ruta, si su HTML puede fijarse en el build o si depende de datos que solo existen en el instante de la petición.

Ese eje también explica por qué el coste y la resiliencia cambian tanto entre destinos, y no por capricho de precios. Servir un archivo ya calculado cuesta lo mismo por petición pase lo que pase, así que el CDN absorbe picos sin inmutarse; ejecutar un handler es trabajo por petición, de modo que el edge y el contenedor escalan su cómputo con el tráfico y pueden llegar a saturarse. La naturaleza de render no solo dice dónde corre el código: predice cómo se degrada el sistema cuando llega una avalancha de tráfico.

📦

Estático a CDN

HTML resuelto en build time. Se despliega subiendo archivos al borde. Sin cómputo por petición: escala trivialmente y cuesta casi nada.

SSR al edge

HTML por petición en isolates cercanos al usuario. Arranque en frío casi nulo y alcance global, a cambio de un runtime restringido.

🐳

Contenedor en VPS

Proceso vivo y persistente. Control total sobre APIs, disco y conexiones largas, a cambio de cargar con toda la operación.

flowchart TD
SRC[codigo fuente] --> BUILD[build]
BUILD --> STATIC[HTML estatico en build time]
BUILD --> HANDLER[handler SSR por peticion]
STATIC --> CDN[CDN sirve archivos desde el borde]
HANDLER --> EDGE[edge ejecuta en isolates cerca del usuario]
HANDLER --> CONT[contenedor con proceso vivo en un VPS]
style STATIC fill:#a6e3a1,color:#11111b
style EDGE fill:#89b4fa,color:#11111b
style CONT fill:#f9e2af,color:#11111b
📝
El reparto por ruta es el default de 2026

En la práctica, la pregunta rara vez se responde una sola vez para toda la app. Lo normal en 2026 es una app híbrida donde conviven las tres físicas: las páginas de marketing y el blog se prerenderizan a estático y van al CDN, las rutas que dependen de la petición se sirven por SSR al edge, y solo lo que exige un proceso vivo cae en un contenedor. El adapter deja de ser una elección global que haces al principio y pasa a ser el mecanismo que reparte cada ruta a su destino. Diseñar el despliegue es, sobre todo, decidir ese reparto ruta por ruta.

Estático a un CDN

Cuando el build produce un directorio de archivos —HTML, CSS, JavaScript, imágenes—, desplegar es en esencia copiarlo al almacenamiento de un CDN que lo replicará por sus puntos de presencia. Lo que hace este destino tan barato y tan robusto es que no ejecuta nada por petición: el borde solo entrega bytes ya calculados, así que absorber un pico de tráfico es servir el mismo archivo más veces, no computar más. La pieza técnica que hay que dominar aquí no es el despliegue en sí, que es trivial, sino el cacheado, porque un CDN es antes que nada una caché geográfica y su corrección depende de las cabeceras que le mandas.

Hay dos regímenes de caché, y confundirlos rompe el sitio de una de dos formas. Los assets con nombre versionado por hashapp.9f3c2a.js— son inmutables por construcción: si el contenido cambia, cambia el nombre, así que se pueden cachear para siempre. El HTML, en cambio, conserva su URL estable y debe revalidarse; si lo cacheas eterno, servirías la página vieja apuntando a assets que ya no existen. La regla es exacta: cachea eterno lo que lleva hash y revalida lo que no.

/_astro/*
  Cache-Control: public, max-age=31536000, immutable
/*.html
  Cache-Control: public, max-age=0, must-revalidate

El otro rasgo que distingue a un CDN maduro es que el despliegue es atómico: la versión nueva de todos los archivos se publica de golpe y la invalidación del borde es casi instantánea, de modo que ningún usuario ve un estado a medias con HTML nuevo apuntando a assets viejos ni al revés. Subir archivos sueltos sin esa atomicidad abre una ventana de incoherencia visible; las plataformas serias la cierran publicando el conjunto entero como una unidad y purgando el borde en bloque.

💡
El hash es la clave de caché

Cachear un asset durante un año suena temerario hasta que ves por qué es seguro: el nombre del archivo es el hash de su contenido, así que dos versiones distintas nunca comparten URL y no hay nada que invalidar. Publicar una versión nueva no reescribe app.9f3c2a.js, sino que crea app.7b1e04.js y actualiza el HTML que lo referencia. Por eso los despliegues estáticos son atómicos y su rollback es instantáneo: el conjunto de archivos nuevo coexiste con el viejo y solo cambia qué HTML se sirve. La inmutabilidad que viste en las previews reaparece aquí, ahora a nivel de asset.

SSR al edge

Cuando la app renderiza en servidor pero no necesita un proceso persistente, el edge es el destino natural. Un adapter —@astrojs/cloudflare, @astrojs/vercel en su modo edge, o el equivalente de Netlify sobre Deno— transforma tu app en un handler que la plataforma ejecuta en isolates de V8 repartidos por todo el mundo. La diferencia con un servidor tradicional es doble y profunda: el arranque en frío es prácticamente nulo, porque un isolate no es un contenedor que haya que arrancar sino un contexto de V8 que se crea en microsegundos; y el código corre en el punto de presencia más cercano al usuario, no en una región única, así que la latencia de red se desploma para una audiencia global.

Ese poder viene con un runtime que no es Node completo, y ese es el error que más despliegues al edge rompe. El estándar es WinterCG: fetch, Request, Response, Web Crypto y los streams web están presentes; el sistema de archivos, los sockets crudos y muchos módulos nativos no. A lo largo de 2026 la bandera de compatibilidad con Node de Workers ha cerrado gran parte de esa brecha, pero la mentalidad correcta sigue siendo la inversa: el edge es un runtime de estándares web con extras, no un Node al que le faltan cosas. Hay además límites duros de tiempo de CPU por petición y de tamaño del bundle, pensados para que miles de isolates convivan en cada máquina.

Entre el edge y el contenedor hay un tercer punto que conviene conocer: las funciones serverless con Node completo, como las de Vercel, Netlify o AWS Lambda. Ejecutan tu handler bajo demanda igual que el edge, pero en un runtime de Node de verdad, así que aceptan el sistema de archivos y los módulos nativos que el isolate rechaza; a cambio pagan un arranque en frío real —cientos de milisegundos cuando la función llevaba dormida— y suelen correr en una única región, no en todas a la vez. Son el destino natural cuando necesitas Node de verdad pero no un proceso siempre encendido.

# desplegar una app SSR a Cloudflare Workers
pnpm astro build
pnpm wrangler deploy
flowchart LR
U[usuario] --> POP[PoP edge mas cercano]
POP --> ISO[isolate ejecuta el handler]
ISO --> R[HTML por peticion]
R --> U
ORIG[origen unico y lejano] -. mayor latencia .-> U
style POP fill:#89b4fa,color:#11111b
style ISO fill:#cba6f7,color:#11111b
style R fill:#a6e3a1,color:#11111b
⚠️
El edge no es un Node con todo

Asumir que el edge puede hacer lo que hace tu servidor local es la fuente número uno de sorpresas al desplegar. Código que lee un archivo con fs, que abre un socket TCP, que depende de un binario nativo o que guarda estado en memoria entre peticiones funcionará en tu máquina y fallará en el isolate, a veces solo bajo carga. Antes de comprometerte con el edge, audita tus dependencias: una sola que asuma Node completo te empuja a un VPS o a un runtime serverless con Node, no al edge. La regla operativa es diseñar para estándares web desde el principio, no migrar un servidor Node al edge y esperar que encaje.

En un contenedor sobre un VPS

Cuando la app necesita lo que el edge te niega —un proceso vivo entre peticiones, acceso al sistema de archivos, conexiones WebSocket largas, trabajos en segundo plano, cualquier módulo nativo—, el destino es un contenedor Docker corriendo en un VPS. Aquí el adapter es @astrojs/node en modo standalone, que emite un servidor que escucha en un puerto, y el contenedor lo empaqueta con todo lo que necesita para correr en cualquier máquina igual que corre en la tuya. El patrón canónico es un Dockerfile multi-etapa: una etapa instala y construye con todo el toolchain, y una etapa final ligera se queda solo con lo imprescindible para ejecutar.

# etapa de build: instala el toolchain y construye
FROM node:22-slim AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# etapa de runtime: solo lo necesario para ejecutar
FROM node:22-slim AS runtime
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV HOST=0.0.0.0 PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

Partir el Dockerfile en dos etapas no es cosmético: la imagen final no debe contener el toolchain de build, ni el código fuente, ni las dependencias de desarrollo, sino solo el runtime y lo que este ejecuta. Una imagen pequeña se despliega antes, arranca antes y ofrece menos superficie de ataque; por eso la etapa de runtime parte de una base ligera como slim o distroless y copia únicamente lo construido. Cada megabyte que no está en la imagen es uno que no hay que mover, escanear ni mantener.

Y como el contenedor es un proceso vivo, escalarlo horizontalmente exige que sea sin estado: si guardas sesiones o caché en la memoria de una réplica, dos réplicas detrás de un balanceador se contradicen. El estado compartido se externaliza a Redis, a la base de datos o a un almacén de sesiones, y el contenedor queda como una unidad intercambiable de la que puedes correr una o cincuenta copias idénticas. Esa disciplina sin estado es lo que convierte un servidor en una flota que escala.

El control total tiene un precio, y es todo lo que el CDN y el edge hacían por ti y ahora es tuyo. Tú terminas el TLS con un proxy inverso como Caddy o Traefik; tú supervisas el proceso para que reviva si cae; tú escalas poniendo réplicas detrás de un balanceador; tú parcheas el sistema operativo; tú cableas los logs y las métricas. Plataformas como Fly.io, Railway, Render o un PaaS autoalojado como Coolify recuperan buena parte de esa comodidad sobre contenedores, difuminando la frontera entre un VPS crudo y una plataforma gestionada. El eje real no es contenedor contra serverless, sino cuánta operación estás dispuesto a asumir a cambio de cuánto control necesitas.

Destino HTML Escala Estado y APIs Coste operativo
CDN estático build time trivial y global ninguno por petición casi nulo
Edge SSR por petición automática y global runtime web restringido bajo, sin ops de servidor
Contenedor en VPS por petición manual, por réplicas proceso vivo, Node completo alto, toda la operación
El destino de despliegue es una proyección del modelo de render

Merece la pena resistir la costumbre de pensar el despliegue como el último casillero de una lista —construyo y luego “lo subo a algún sitio”— porque esa forma de verlo invierte la causalidad real. El destino no es una decisión posterior e independiente sobre tu app: es una proyección de cómo tu app produce HTML, y cuando eliges destino no estás comprando una plataforma, estás aceptando una física. La física del CDN es la de un objeto inerte que se replica: no computa, así que no puede hacer nada dinámico, pero por lo mismo escala sin esfuerzo, no se cae bajo carga y cuesta casi nada, porque servir un archivo mil veces no es más difícil que servirlo una. La física del edge es la de un cómputo efímero y ubicuo: corre en todas partes a la vez y arranca en un parpadeo, y a cambio no puede recordar nada entre peticiones ni tocar el mundo fuera de los estándares web, porque esas dos renuncias son exactamente lo que le permite estar en todas partes. Y la física del contenedor es la de un proceso vivo y localizado: puede recordar, abrir conexiones largas, tocar el disco y usar cualquier API, y por eso mismo vive en un solo lugar, hay que mantenerlo despierto y escalarlo a mano, y cargas con toda su operación. Ninguna es mejor; cada una es la única coherente con una forma de generar la respuesta. El error que este marco te ahorra es el de pelearte con la física equivocada: intentar guardar estado de sesión en el edge, servir contenido personalizado desde un CDN, o pagar un VPS siempre encendido para un sitio que era estático desde el principio. Interiorizar esto cambia el orden de tus preguntas. Dejas de preguntar “dónde despliego” como si fuera un paso de logística, y empiezas por “cuándo y dónde se genera mi HTML”, que es una pregunta sobre la naturaleza de tu app; la respuesta a esa segunda pregunta ya contiene la respuesta a la primera. Y como Astro y sus pares te dejan decidir el render por ruta, el destino deja de ser una única apuesta global y se vuelve un reparto afinado: cada ruta va a la física que le corresponde, y la arquitectura de despliegue se vuelve el reflejo exacto de la arquitectura de render. Un despliegue bien elegido no es el que usa la plataforma de moda, sino el que no le pide a ninguna capa que haga algo contra su naturaleza.

⚔️ Despliega la misma app a los tres destinos
  1. Construye tu app como sitio estático y despliégala a un CDN; inspecciona las cabeceras Cache-Control y confirma que los assets con hash son inmutables y el HTML se revalida.
  2. Cambia al adapter de edge, despliega un Worker y comprueba desde dos continentes que el HTML se sirve cerca de ti y con arranque en frío casi nulo.
  3. Escribe un Dockerfile multi-etapa con @astrojs/node, levanta el contenedor en un VPS o en Fly.io y termina el TLS con un proxy inverso.
  4. Mide el TTFB de una misma ruta en los tres destinos desde varias regiones y explica las diferencias por la física de cada uno.
  5. Marca una ruta como SSR y otra como prerenderizada en la misma app, y verifica que cada una acaba en el destino que le corresponde.