Hosting a static site in a container

created: miércoles, dic. 10, 2025

La mayoría de nuestros sitios alojados son renderizados de forma estática, construidos con herramientas como Hugo, Zola o Jekyll. En general, todos esos generadores de sitios toman una entrada simplificada (usualmente 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 saben, 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 viene la pregunta: ¿cómo puedo ir de una compilación de sitio estático a un contenedor que aloje el sitio web?

Así que comencemos desde cero generando una página muy simple de “hola mundo” 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 verificar la compilación, podemos iniciar el servidor localmente:

hugo serve

Entonces aparece nuestra propia página de “Hola Mundo” en http://localhost:1313

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

Two Approaches to Containerization

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

  1. Poner todo el código fuente dentro de un contenedor y ejecutar el proceso de compilación dentro
  2. Compilar localmente y copiar solo las salidas dentro del contenedor

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

Building the Container Image

Una imagen de contenedor siempre empieza con una imagen base. Para esto, usamos Alpine Linux ya que es pequeña (alrededor de 5MB) y provee las herramientas suficientes para nuestro proyecto.

Usaremos una compilación multi-etapa, una técnica soportada por herramientas modernas de construcción de contenedores que nos permite usar una imagen para construir 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: Construcción

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

En esta etapa, nosotros:

El sitio construido termina 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:

The Complete Dockerfile

Aquí está el Dockerfile completo combinando 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.

Building and Running Locally

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

Para construir la imagen:

docker build -t my-website .

Para ejecutarlo 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 el contenedor automáticamente cuando se detiene.

Why This Approach Works Well

Esta configuración tiene varias ventajas:

  1. Tamaño pequeño de la imagen: La imagen final contiene solo Alpine (~5MB) + lighttpd (~1MB) + tus archivos HTML. No Node.js, ni Ruby, ni herramientas de compilación que hinchan tu 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. Inicio rápido: lighttpd inicia en milisegundos, ideal para despliegues que escalan 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.

Architecture Considerations

Si estás construyendo tu imagen en un Mac 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 funcione correctamente en DTZ (y la mayoría de otros proveedores de nube), deberías especificar explícitamente la plataforma destino durante la compilación.

Cambia tu comando de construcción a:

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

Esto le dice a Docker que compile la imagen para servidores Linux estándar, asegurando compatibilidad sin importar la máquina donde construyas.

Deploying to DownToZero

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

# Etiquetar para el registro DTZ
docker tag my-website YOUR_CONTEXT_ID.cr.dtz.dev/my-website:latest

# Login y 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 automáticos en cada commit, consulta nuestra acción de GitHub para despliegues sin interrupciones.

Adapting for Other Static Site Generators

El mismo patrón funciona para otros generadores. Aquí 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 tiempo de ejecución queda igual - solo copia desde /src/public (Zola) o /src/_site (Jekyll) al directorio raíz de documentos de lighttpd.

Wrapping Up

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

Este enfoque coincide bien con nuestra filosofía en DTZ - uso mínimo de recursos, inicio rápido en frío y una infraestructura que escala a cero cuando no se usa. Un sitio estático en un contenedor de ~10MB que inicia al instante es tan eficiente como puede ser el hosting web.