Introducción: por qué necesitaba algo más que Cloudflare
Quería responder dos preguntas sencillas sobre mi blog: qué posts se leen más y de dónde llegan los lectores. Cloudflare ya me daba estadísticas básicas (visitas, páginas, países), pero se quedaba corto para entender el contenido y el origen del tráfico. Además, quería conservar el histórico en mi propia base de datos.
Por eso probé Umami. Este artículo tiene dos partes:
- Parte 1 (este artículo): qué es Umami, cómo lo instalé con Docker, cómo lo publiqué y cómo lo integré con Hugo y PaperMod.
- Parte 2: los problemas que encontré (una política CSP que bloqueaba el script y un puerto expuesto sin querer), el endurecimiento de seguridad y los siguientes pasos.
Instalar un servicio nuevo terminó siendo una excusa para auditar lo que ya tenía expuesto, y eso lo cuento en la Parte 2.
¿Qué es Umami y cómo funciona?
Umami es una plataforma de analítica web de código abierto, pensada como alternativa ligera y centrada en la privacidad frente a Google Analytics.
Visitante → tu blog (Hugo + PaperMod)
│
└── script de Umami
│
▼
Servidor Umami → PostgreSQL → Panel de estadísticas
El navegador del visitante ejecuta un script pequeño que envía cada visita a tu servidor de Umami. Tú consultas los resultados en un panel web.
Ventajas que me convencieron:
- Código abierto (licencia MIT) y autoalojable.
- Sin cookies de tracking: el script de seguimiento no usa cookies. Esto simplifica las consideraciones de privacidad, aunque los requisitos legales concretos dependen de tu jurisdicción y del resto de servicios que uses en el sitio.
- Control de los datos: viven en mi propia base de datos.
- Ligero: corre bien en una VM pequeña.
- Más detalle: páginas, referentes, campañas UTM, eventos personalizados, sesiones y más.
Comparación para mi escenario (un blog personal estático):
| Umami (self-hosted) | Cloudflare Web Analytics | Google Analytics | |
|---|---|---|---|
| Dónde viven los datos | En mi propio servidor | En la plataforma del proveedor | En la plataforma del proveedor |
| Cookies de tracking | No | No | Sí |
| Eventos y UTM | Sí | No | Sí |
| Mantenimiento | Lo hago yo | Ninguno | Ninguno |
Es una comparación orientada a mi caso, no una evaluación absoluta de privacidad. Revisa las funciones actuales de cada plataforma antes de decidir.
¿Qué información guarda Umami?
“Amigable con la privacidad” no significa “no recopila ningún dato”. De acuerdo con la documentación de Umami, registra cosas como:
- Páginas vistas y página de origen (referente).
- Navegador, sistema operativo y tipo de dispositivo.
- País del visitante.
- Sesiones, identificadas mediante un hash y no con cookies.
Conviene revisar en la documentación oficial el detalle vigente de qué se almacena, sobre todo si tu sitio tiene audiencia en jurisdicciones con regulación estricta.
Mi escenario y requisitos
- Blog en Hugo con el tema PaperMod, servido con nginx en Docker.
- VM ARM64 en Oracle Cloud.
- Nginx Proxy Manager (NPM) como proxy inverso y Cloudflare delante.
Umami necesita una base de datos PostgreSQL (con Docker se levanta junto a Umami). Mi duda era si existía imagen para ARM64. La verifiqué en el registro oficial: las imágenes son multi-arquitectura. También se puede comprobar en la VM:
docker pull docker.umami.is/umami-software/umami:latest
docker image inspect docker.umami.is/umami-software/umami:latest --format '{{.Architecture}}'
# Esperado: arm64
Instalación con Docker Compose
Partí del docker-compose.yml oficial y lo ajusté: secretos en un .env, límite de logs y, lo más importante, el puerto publicado solo en la interfaz del bridge de Docker.
services:
umami:
image: docker.umami.is/umami-software/umami:latest
container_name: umami
ports:
- "172.17.0.1:3000:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
APP_SECRET: ${APP_SECRET}
TWO_FACTOR_ENCRYPTION_KEY: ${TWO_FACTOR_ENCRYPTION_KEY}
DISABLE_TELEMETRY: 1
depends_on:
db:
condition: service_healthy
init: true
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:15-alpine
container_name: umami-db
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
umami-db-data:
Y el .env (con valores de ejemplo, nunca los reales):
POSTGRES_DB=umami
POSTGRES_USER=umami
POSTGRES_PASSWORD=cambiar-por-password-segura # openssl rand -base64 32
APP_SECRET=cambiar-por-string-aleatorio # openssl rand -hex 32
TWO_FACTOR_ENCRYPTION_KEY=cambiar-por-64-hex # openssl rand -hex 32
Guarda una copia del .env fuera de la VM: si pierdes TWO_FACTOR_ENCRYPTION_KEY, tendrás que volver a configurar el 2FA.
Dos notas sobre este compose:
- Imagen y versión: usé
latestporque es un laboratorio. Para una instalación más reproducible conviene fijar una etiqueta de versión concreta (revisa las disponibles en el registro). El proyecto también publica la imagen enghcr.io/umami-software/umami, que es la que aparece en el compose del repositorio oficial; ambas sirven. - La IP
172.17.0.1: en mi servidor es la dirección del bridgedocker0, pero puede variar según la configuración de Docker. Compruébala antes de copiarla (por ejemplo, conip addr show docker0).
Decisión de seguridad: publicar el puerto como 172.17.0.1:3000 hace que Umami sea accesible solo desde la propia VM (donde vive NPM), no desde Internet. Por eso no abrí el puerto 3000 en Oracle Cloud.
Levanté todo y verifiqué:
chmod 600 .env
docker compose up -d
docker compose ps # ambos "Up (healthy)"
curl -s http://172.17.0.1:3000/api/heartbeat # {"ok":true}
Publicación del servicio
- DNS en Cloudflare: registro
Aparastatsapuntando a la VM, con el proxy activado. - Nginx Proxy Manager: Proxy Host para
stats.tudominio.com→http://172.17.0.1:3000, con certificado de Let’s Encrypt, Force SSL y Block Common Exploits. - Primer ingreso: el usuario por defecto es
admincon contraseñaumami. Cámbiala de inmediato y activa 2FA. - Registrar el sitio en el panel y copiar el snippet del script.
Otra opción para lectores más avanzados: si NPM y Umami corren ambos en Docker, puedes ponerlos en una red Docker compartida y que NPM apunte a
http://umami:3000. Así no dependes de172.17.0.1ni publicas el puerto en el host. Yo conté lo que hice en mi servidor, pero es una mejora que vale la pena considerar.
Integración con Hugo y PaperMod
En hugo.yml, dentro de params::
params:
umami:
src: "https://stats.tudominio.com/script.js"
websiteId: "TU-WEBSITE-ID"
domains: "tudominio.com,www.tudominio.com"
Y un partial en layouts/partials/extend_head.html (en la raíz del sitio, no dentro de themes/PaperMod):
{{- if and hugo.IsProduction site.Params.umami.websiteId }}
<script defer
src="{{ site.Params.umami.src }}"
data-website-id="{{ site.Params.umami.websiteId }}"
data-domains="{{ site.Params.umami.domains }}"></script>
{{- end }}
De esta forma la integración queda en la configuración y los layouts del sitio, sin modificar los archivos del tema PaperMod.
hugo.IsProduction: conhugo serverel script no se inserta; conhugo --minifysí. Así no contamino las métricas mientras desarrollo.data-domains: limita la ejecución del tracker a los dominios indicados.- El
website-idno es un secreto: queda visible en el HTML.
Regeneré el sitio y comprobé que el script quedara incluido:
hugo --minify
curl -s https://tudominio.com/ | grep -c "stats.tudominio.com" # 1
Hasta aquí la Parte 1
Con esto Umami queda instalado, publicado y enlazado con el blog. En mi caso, sin embargo, el panel mostraba todo en cero. En la Parte 2 cuento por qué, cómo lo resolví, qué exposiciones descubrí en mi propia infraestructura y cómo la endurecí.
