Alojando un sitio estático en un contenedor

created: miércoles, dic 10, 2025

La mayoría de nuestros sitios alojados se renderizan de forma estática, construidos con herramientas como Hugo, Zola o Jekyll. En general, todos esos renderizadores de sitios toman una entrada simplificada (normalmente Markdown) y generan HTML bien definido como salida. Esto deja la pregunta: ¿cómo puedo alojar un sitio así?

Existen proveedores especializados que ofrecen hosting para sitios estáticos, pero como sabéis, tomamos un camino diferente al construir nuestra infraestructura central. Para nuestra nube, el denominador común para el despliegue es un contenedor. Y con eso surge la pregunta: ¿cómo puedo pasar de una compilación de sitio estático a un contenedor que aloje el sitio web?

Así que empecemos desde cero generando una página muy simple de “hello world” con Hugo.

hugo new site hello
cd hello
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
echo "theme = 'ananke'" >> hugo.toml

Para comprobar la compilación, podemos arrancar el servidor localmente:

hugo serve

Entonces nuestra propia página “Hello World” aparece en http://localhost:1313

Hasta aquí todo bien. Ahora consideremos poner esto en un contenedor.

Dos enfoques para la contenerización

Técnicamente, podemos ir por dos rutas diferentes aquí:

  1. Poner todo el código fuente en un contenedor y ejecutar el proceso de compilación dentro
  2. Construir localmente y copiar solo los resultados al contenedor

Para hacer nuestro ejemplo más reproducible y menos dependiente de los entornos locales, elegimos la opción 1 para este escenario. Esto también hace que las canalizaciones CI/CD sean más limpias, ya que el entorno de compilación está totalmente definido en el Dockerfile.

Construyendo la imagen del contenedor

Una imagen de contenedor siempre comienza con una imagen base. Para esto, usamos Alpine Linux ya que es pequeña (alrededor de 5MB) y proporciona suficiente tooling para nuestro proyecto.

Usaremos una construcción multi-etapa, una técnica soportada por las herramientas modernas de construcción de contenedores que nos permite usar una imagen para compilar y otra para ejecutar. Esto mantiene nuestra imagen final pequeña al excluir las herramientas de compilación que no necesitamos en tiempo de ejecución.

Etapa 1: Compilación

FROM alpine AS build
RUN apk add --no-cache hugo
WORKDIR /src
ADD . .
RUN hugo --minify

En esta etapa, nosotros:

El sitio compilado queda en /src/public/.

Etapa 2: Tiempo de ejecución

FROM alpine AS runner
RUN apk add --no-cache lighttpd
COPY --from=build /src/public /var/www/localhost/htdocs
EXPOSE 80
CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

En esta etapa, nosotros:

El Dockerfile completo

Aquí está el Dockerfile completo que combina ambas etapas:

# Stage 1: Build the static site
FROM alpine AS build
RUN apk add --no-cache hugo
WORKDIR /src
ADD . .
RUN hugo --minify

# Stage 2: Serve with lighttpd
FROM alpine AS runner
RUN apk add --no-cache lighttpd
COPY --from=build /src/public /var/www/localhost/htdocs
EXPOSE 80
CMD ["lighttpd", "-D", "-f", "/etc/lighttpd/lighttpd.conf"]

Guarda esto como Dockerfile en la raíz de tu proyecto Hugo.

Construir y ejecutar localmente

Puedes usar cualquier herramienta de contenedores compatible con OCI como Podman o Docker. Los ejemplos abajo usan docker, pero podman funciona como reemplazo directo.

Para construir la imagen:

docker build -t my-website .

Para ejecutarla localmente:

docker run -p 8080:80 --rm my-website

Tu sitio ahora está disponible en http://localhost:8080

La bandera -p 8080:80 mapea el puerto 8080 en tu máquina al puerto 80 dentro del contenedor. La bandera --rm elimina automáticamente el contenedor cuando se detiene.

Por qué este enfoque funciona bien

Esta configuración tiene varias ventajas:

  1. Tamaño de imagen pequeño: La imagen final contiene solo Alpine (~5MB) + lighttpd (~1MB) + tus archivos HTML. No hay Node.js, no hay Ruby, ni herramientas de compilación que inflen la imagen de producción.

  2. Compilaciones reproducibles: La misma versión exacta de Hugo se ejecuta en CI que localmente, eliminando problemas de “funciona en mi máquina”.

  3. Arranque rápido: lighttpd arranca en milisegundos, lo que lo hace perfecto para despliegues de escala a cero en DTZ.

  4. Seguridad: El contenedor de producción tiene una superficie de ataque mínima: solo un servidor de archivos estáticos sin runtime dinámico.

Consideraciones de arquitectura

Si estás construyendo tu imagen en un Mac con Apple Silicon (ARM64) u otra arquitectura no estándar, recuerda que los servidores típicamente corren en AMD64 (x86_64). Para asegurar que tu contenedor se ejecute correctamente en DTZ (y en la mayoría de proveedores cloud), deberías especificar explícitamente la plataforma objetivo durante la compilación.

Cambia tu comando de compilación a:

docker build --platform linux/amd64 -t my-website .

Esto le indica a Docker que cruce-compile la imagen para servidores Linux estándar, asegurando compatibilidad independientemente de la máquina donde la construyas.

Desplegando en DownToZero

Una vez que tu imagen esté construida, puedes subirla a un registro de contenedores y desplegarla en DTZ. Si estás usando nuestro registro de contenedores:

# Tag for DTZ registry
docker tag my-website YOUR_CONTEXT_ID.cr.dtz.dev/my-website:latest

# Login and push
docker login YOUR_CONTEXT_ID.cr.dtz.dev -u apikey
docker push YOUR_CONTEXT_ID.cr.dtz.dev/my-website:latest

Luego crea un servicio de contenedor en el panel de DTZ apuntando a tu imagen. El servicio manejará automáticamente certificados TLS, escalado y enrutamiento.

Para despliegues automatizados en cada commit, consulta nuestra GitHub Action para despliegues sin fricciones.

Adaptación para otros generadores de sitios estáticos

El mismo patrón funciona para otros generadores. Aquí están los cambios clave:

Para Zola:

FROM alpine AS build
RUN apk add --no-cache zola
WORKDIR /src
ADD . .
RUN zola build

Para Jekyll:

FROM ruby:alpine AS build
RUN apk add --no-cache build-base
RUN gem install bundler jekyll
WORKDIR /src
ADD . .
RUN bundle install
RUN bundle exec jekyll build

La etapa de runtime permanece igual: simplemente copia desde /src/public (Zola) o /src/_site (Jekyll) al directorio raíz de documentos de lighttpd.

Conclusión

Contenerizar sitios estáticos es sencillo una vez que entiendes el patrón: compilar en una etapa y servir desde otra. El resultado es un contenedor pequeño, rápido y seguro que es perfecto para despliegues en la nube modernos.

Este enfoque se alinea bien con nuestra filosofía en DTZ: uso mínimo de recursos, arranques en frio rápidos e infraestructura que escala a cero cuando no está en uso. Un sitio estático en un contenedor de ~10MB que arranca al instante es, en cuanto a eficiencia, de lo mejor que existe para hosting web.