SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-25

Cómo alojar OpenAnalytics en un VPS

Comprueba los requisitos reales de OpenAnalytics: ClickHouse, Postgres, Valkey, 4 GB de RAM, 25 GB libres y cuatro registros DNS antes de instalarlo.

La huella, antes del primer paso

Para alojar OpenAnalytics por su cuenta necesita un VPS Linux con unos 4 GB de RAM, 25 GB de espacio libre en disco, Docker con el complemento Compose y cuatro registros DNS que ya apunten al servidor. Ese es el requisito real y debe aparecer antes del primer comando, no después.

La pila consta de seis servicios de aplicación y tres almacenes de datos. Postgres almacena el plano de control: cuentas, sitios, claves API y enlaces para compartir. ClickHouse almacena los eventos sin procesar y las agregaciones que lee el panel. Valkey se ejecuta dos veces: una como cola de eventos persistente y otra como caché que se puede perder, porque esas dos funciones necesitan políticas de expulsión opuestas. Sólo un proceso, la puerta de enlace de consultas, puede leer ClickHouse, y verifica una firma Ed25519 en cada sobre de consulta antes de ejecutarlo.

Si buscaba un único binario y un único archivo de configuración, esta no es la opción adecuada. GoatCounter es la alternativa de un solo binario en esta categoría: un ejecutable de Go, SQLite de forma predeterminada y ninguna base de datos externa. La pila más compleja ofrece embudos, métricas web esenciales, atribución de ingresos desde su propia cuenta de Stripe y un servidor MCP (protocolo de contexto de modelos). Elegir entre herramientas de analítica autoalojadas es el artículo que analiza esa decisión. Esta guía da por hecho que ya la tomó.

Apunte primero cuatro registros DNS al servidor

Los cuatro subdominios deben resolver a la IP pública del servidor antes de iniciar cualquier otra tarea, porque Caddy solicita certificados de Let's Encrypt en el primer arranque y el desafío falla si el nombre todavía no resuelve.

  • app.example.com sirve el panel.
  • api.example.com sirve la API y las devoluciones de OAuth.
  • c.example.com sirve el recopilador y el script de seguimiento.
  • rt.example.com sirve el flujo en tiempo real.

Use cuatro registros A, o un registro A y tres CNAME que apunten a él. Confirme la resolución con dig +short app.example.com antes de continuar. Un nombre que añadió hace un minuto todavía puede estar almacenado como NXDOMAIN en la caché del resolvedor que Let's Encrypt utilice, por lo que conviene esperar y revisar los registros de Caddy si el primer intento de obtener el certificado falla. Volver a ejecutar la instalación no acelera la propagación de DNS.

Cómo alojar OpenAnalytics por cuenta propia con Docker Compose

Compruebe una versión etiquetada. La rama predeterminada es donde se desarrolla el proyecto. La etiqueta de una versión es la que realmente coincide con las imágenes publicadas. Los comandos siguientes presuponen que Docker y el complemento Compose ya están instalados. Esto se explica en ejecutar servicios de Docker Compose en un VPS.

git clone https://github.com/OpenLabs-so/openanalytics
cd openanalytics
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./generate-secrets.sh --domain example.com --email you@example.com --with-geoip
docker compose pull && docker compose up -d

sed '/-/d' en la línea de clonación excluye las etiquetas preliminares. Así se obtiene la versión estable más reciente en lugar de una candidata a versión. --with-geoip descarga la base de datos de ciudades de DB-IP durante la generación. Si se omite, cada evento incluye un país nulo y la vista geográfica no muestra ningún dato. Puede añadirla más adelante ejecutando infra/selfhost/geoip/fetch-dbip.sh, estableciendo GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb en env/collector.env y recreando el collector con docker compose up -d --force-recreate collector. Esta base de datos se actualiza mensualmente. Repita la descarga cada mes para evitar que los datos de ciudades queden desactualizados.

Haz una copia de seguridad de los secretos generados antes de continuar

El generador escribe tres elementos. .env contiene los nombres de dominio y las referencias de imagen. env/*.env contiene un archivo de secretos por servicio. docker-compose.override.yml contiene tres pares de claves Ed25519 como escalares de bloque YAML, porque un PEM multilínea no puede almacenarse en un archivo de entorno. Todo está excluido de Git y no se puede regenerar con los mismos valores.

Copia ahora esos archivos fuera de la máquina. Cada pérdida tiene una consecuencia concreta:

  • Si pierdes las contraseñas de los almacenes, no podrás acceder a Postgres ni a ClickHouse. Sólo podrás restablecerlas desde el interior de los contenedores.
  • Si pierdes OA_CREDENTIAL_KEYRING, ninguna credencial de terceros almacenada podrá recuperarse. Por tanto, cualquiera que haya conectado una cuenta de Stripe tendrá que conectarla de nuevo.
  • Si pierdes ANONYMOUS_IDENTITY_SECRET, la identidad de los visitantes se reinicia: los visitantes de ayer vuelven a contarse como nuevos y la interrupción será visible en los gráficos.
  • Si pierdes AUTH_SECRET, todas las sesiones se invalidan y todos tendrán que iniciar sesión de nuevo.
  • Si pierdes una clave privada de firma, rota el par de claves. No se pierde ningún dato.

Dos secretos deben ser idénticos byte por byte en dos archivos cada uno. ANONYMOUS_IDENTITY_SECRET aparece en collector.env y worker.env porque el collector calcula el hash del visitante y el worker lo escribe. OA_CREDENTIAL_KEYRING aparece en api.env y worker.env. Todos los demás secretos están asignados deliberadamente a un único servicio. Si un servicio recibe un secreto que no debe tener, termina en lugar de iniciarse.

Levante la pila y compruébela

grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose ps

migrate aplica los esquemas de Postgres y ClickHouse y después termina, por lo que un contenedor migrate detenido es el estado final correcto. tracker-build compila oa.js en un volumen que Caddy sirve y también termina. Todo lo demás debería mostrar healthy en docker compose ps. Un servicio que se reinicia en bucle casi siempre falla al validar el entorno. El registro muestra todos los problemas en una sola lista, en lugar de mostrar uno por reinicio. Las dos causas habituales son una variable vacía, que se rechaza en lugar de tratarse como no definida, y un secreto colocado en el archivo de servicio incorrecto.

En arm64 o desde una rama no hay imágenes publicadas, por lo que debe compilar localmente con docker compose up -d --build. Un host con 4 GB se queda sin memoria durante esa compilación. Añada primero espacio de intercambio. Sólo se necesita durante la compilación:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

La compilación tarda aproximadamente diez minutos. La descarga tarda unos minutos, por eso existen las imágenes de la versión.

Reclame la primera cuenta de inmediato

Abra https://app.example.com. Una implementación en la que nadie ha iniciado sesión todavía no muestra un formulario de inicio de sesión: ofrece crear la primera cuenta. Esa cuenta conserva permanentemente los privilegios y es la única que puede ver la pantalla de configuración de la implementación. Una vez creada, la ruta responde con 409, de modo que nadie puede acceder después de usted. Hágalo en cuanto la pila esté operativa, no la semana siguiente.

Instalar el tracker

Añada un sitio en el panel y el sistema le entregará la etiqueta. Su estructura es fija:

<script
  async
  src="https://c.example.com/oa.js"
  data-key="YOUR_TRACKING_KEY"
  data-collector="https://c.example.com"
></script>

Colóquela en la cabecera de la página. La clave de seguimiento es pública por diseño, por lo que debe incluirse en el HTML, donde cualquiera puede leerla. El script instala window.oa y las llamadas como oa("track", ...) se ponen en cola mediante un stub y se vacían cuando se carga el archivo. Por eso, un evento personalizado emitido al principio no se pierde. Si otro componente de la página ya utiliza window.oa, el tracker se instala como window.openanalytics. Si el mismo sitio también responde como servicio onion, no incluya la etiqueta en esa versión, porque un script obtenido de c.example.com devuelve al visitante de Tor Browser a la clearnet y vincula las dos direcciones en la misma carga de página.

A continuación, compruebe toda la ruta de extremo a extremo:

curl -s https://c.example.com/oa.js -o /dev/null -w '%{http_code} %{size_download}\n'
curl -s https://api.example.com/health | head -c 200
docker compose logs --tail=50 worker | grep -i batch

El primer comando debería mostrar 200 y unos pocos kilobytes. Cargue una página de su sitio y, en unos segundos, busque una línea de lote en el registro del worker. El colector responde 202 en cuanto acepta un evento, y 202 significa que está en cola, no que se haya almacenado. El worker mueve los eventos a ClickHouse. Si se aceptan eventos pero no aparece nada en el panel, el worker está bloqueado. Una profundidad de cola de Valkey que sigue aumentando lo confirma. Las causas habituales son unas credenciales incorrectas de ClickHouse en worker.env o la falta de permisos sobre una tabla que una migración acaba de añadir.

Mantener el collector público y proteger el dashboard con autenticación

Caddy se incluye en el archivo compose y obtiene por su cuenta los certificados para los cuatro nombres, por lo que la configuración predeterminada no requiere que configure ningún proxy. Si el servidor ya ejecuta un reverse proxy Nginx, coloque la pila detrás del infra/selfhost/nginx.conf.example proporcionado y mantenga intacto el tratamiento de las cabeceras:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header CF-Connecting-IP "";
proxy_set_header True-Client-IP "";
proxy_set_header Fly-Client-IP "";

El collector calcula el hash diario de visitantes a partir de la IP del cliente, por lo que debe obtener esa dirección de la conexión y nunca de una cabecera. Si reenvía CF-Connecting-IP desde un salto no confiable, cualquier cliente puede declarar cualquier dirección. Esto corrompe la geolocalización y aumenta artificialmente el recuento de visitantes al mismo tiempo.

El acceso se separa claramente por nombre de host. c. y rt. deben ser accesibles para todos los visitantes de todos los sitios que mida, por lo que nunca debe colocar autenticación básica ni una lista de IP permitidas delante de ellos. app. y api. sólo deben ser accesibles para las personas que inician sesión. La autenticación propia de la aplicación protege el dashboard: el inicio de sesión con contraseña está habilitado de forma predeterminada mediante AUTH_PASSWORD_SIGNIN=enabled en env/api.env, y los botones de Google o GitHub sólo aparecen cuando existen el ID de cliente y el secreto de cliente de ese proveedor. Los enlaces mágicos necesitan un transporte de correo. Sin él, la API sólo escribe el envío en una cola de salida, por lo que no se entrega nada y tampoco se produce ningún error. Si sus otras aplicaciones autoalojadas ya están detrás de un único inicio de sesión de Authentik, decida pronto si este dashboard se integra con ellas o mantiene sus propias cuentas, porque la primera cuenta que cree aquí será permanentemente la cuenta con privilegios.

Un ajuste determina si el dashboard funciona. AUTH_TRUSTED_ORIGINS en env/api.env debe coincidir exactamente con el origen del dashboard. Si es incorrecto o falta, la API no emite cabeceras CORS (intercambio de recursos de origen cruzado), el navegador rechaza todas las llamadas y obtiene un dashboard que muestra su diseño pero no los datos, mientras docker compose ps indica que todo funciona correctamente.

Mientras edita la configuración del proxy, gestione también el tráfico automatizado. Los crawlers acceden al collector como cualquier otro cliente, y sus visitas a páginas terminan en ClickHouse y en sus métricas. Bloquear los crawlers de IA en el servidor mantiene parte de ese tráfico fuera de la base de datos antes de que afecte tanto a la precisión como al espacio en disco.

Qué significa aquí «sin cookies» y cuál es su coste

No hay ninguna cookie. La identidad del visitante es un hash con salt, el salt cambia cada día y nunca se almacenan direcciones IP sin procesar. La geolocalización se resuelve localmente con el archivo de DB-IP almacenado en su propio disco, por lo que ninguna consulta sobre un visitante sale del host. Mantener las consultas en local elimina al proveedor, no los datos. Es el mismo límite que aparece cuando ejecuta su propia instancia de SearXNG y la IP de su servidor se convierte en la dirección que ven los motores de búsqueda.

Esto evita que se mantenga un identificador en el dispositivo del visitante. Ese es el elemento concreto que hace que un rastreador quede sujeto a las normas de consentimiento de ePrivacy de la UE. Por ese motivo, las configuraciones que sólo generan datos agregados como esta suelen funcionar sin un banner de consentimiento. El RGPD sigue regulando los datos que almacene y el tiempo que los conserve. Su asesoría jurídica determina cómo se aplica a su caso, no un README.

El coste es perder la identidad entre días. Debido a la rotación del salt, una persona que visita el sitio el lunes y vuelve el miércoles se cuenta como dos visitantes, de forma intencionada y sin alternativa. Los recuentos de visitantes únicos diarios son fiables. Los recuentos únicos semanales y mensuales se construyen a partir de los diarios y sobrestimarán el alcance. Por tanto, cualquier cifra de «visitantes recurrentes» calculada sobre un periodo largo no mide lo que indica su etiqueta. Las sesiones y los recorridos son fiables dentro de un mismo día. La rotación de ANONYMOUS_IDENTITY_SECRET produce el mismo efecto que un límite entre días, por lo que debe tratar esa rotación como un cambio de datos y no como una tarea rutinaria de mantenimiento.

El recolector respeta Do Not Track y Global Privacy Control, la señal del navegador que indica a un sitio que no debe vender ni compartir datos personales. La etiqueta de script incluye sus propios interruptores para el mismo fin: data-respect-gpc, data-respect-dnt y data-require-consent. Esta última mantiene toda la recopilación detenida hasta que se concede el consentimiento y guarda la respuesta en localStorage con la clave oa.consent. Establecer data-storage="none" desactiva por completo el almacenamiento del navegador.

Por qué se llena el disco después de seis meses

Esto es lo que suele dejar inutilizado un servidor de analítica autogestionado. Normalmente, los eventos no son la causa.

Empiece por las imágenes. Una versión publica diez imágenes, que ocupan aproximadamente 13 GB en disco. Durante una actualización, se descargan las imágenes de la nueva generación antes de eliminar las antiguas. Durante un tiempo, por tanto, se conservan dos generaciones. Esto representa la mayor parte de los 25 GB necesarios, incluso antes de recibir una sola visita a una página.

Después están las instantáneas. snapshot.sh detiene la pila, archiva los dos volúmenes de datos junto con todos los secretos y vuelve a iniciarla. Aquí, las únicas copias seguras son las copias en frío, porque ClickHouse fusiona partes en segundo plano y una copia tomada durante una fusión no es coherente. upgrade.sh crea automáticamente una antes de cada actualización, por lo que los archivos se acumulan en el mismo disco hasta que se establece un límite.

./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3

En un host próximo al límite, recupere la generación anterior antes de actualizar. Esto es seguro mientras la pila está en ejecución, porque las imágenes que respaldan contenedores activos siguen estando referenciadas:

docker image prune -a -f

Después están los propios eventos. ClickHouse comprime mucho los datos en formato columnar, por lo que el volumen de eventos sin procesar crece más despacio de lo que suele esperarse. Además, las tablas de agregación que consulta el panel ocupan poco en comparación con la tabla sin procesar. Mida en lugar de hacer suposiciones:

docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse

Para obtener la cifra por tabla, ejecute esto con las credenciales de ClickHouse que el generador escribió en infra/selfhost/env/:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size, sum(rows) AS row_count
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;

Tome esa medida durante la primera semana y repítala durante la cuarta. Dos puntos permiten calcular una tasa de crecimiento, y esa tasa indica cuándo es necesario ampliar el volumen. La guía de autohospedaje no documenta ningún ajuste de retención ni de tiempo de vida para los eventos sin procesar a fecha de agosto de 2026. Por tanto, dimensione el disco según la tasa medida, en lugar de suponer que las filas antiguas caducan por sí solas.

Hay una trampa relacionada con el borrado que conviene conocer antes de encontrársela. Al eliminar un sitio o una cuenta, se pone trabajo en la cola del worker. Ese worker necesita que CLICKHOUSE_MAINTENANCE_USER y CLICKHOUSE_MAINTENANCE_PASSWORD estén definidos, y que exista en ClickHouse un usuario oa_maintenance coincidente. Sin estos valores, la eliminación permanece en cola indefinidamente. El sitio desaparece del panel, pero todas las filas permanecen en el disco. El resultado parece una limpieza, pero no se recupera espacio.

Actualizaciones y los tres costes

git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.sh

upgrade.sh muestra tres costes antes de actuar. El tiempo de inactividad es real: los eventos que se intentan registrar mientras el colector está detenido se pierden, porque el tracker no los reintenta. La reversión pierde datos, ya que rollback.sh --to backups/<snapshot> sustituye por completo ambos almacenes y descarta todas las filas escritas después de crear esa instantánea. El disco es el tercer coste: corresponde al conjunto de instantáneas descrito arriba.

Es fácil equivocarse con dos reglas de reinicio. Inicie el gateway de consultas antes que la API, porque una API más reciente envía campos de consulta que un gateway antiguo rechaza. ClickHouse necesita una recreación en lugar de un reinicio, porque docker compose restart reutiliza el entorno original del contenedor e ignora silenciosamente la modificación:

docker compose up -d --force-recreate clickhouse

El dashboard presenta el mismo tipo de problema. Los tres orígenes de NEXT_PUBLIC_* en env/web.env se compilan en el bundle del navegador y se sustituyen cuando se inicia el contenedor. Por eso, un dashboard que llama al nombre de host incorrecto se corrige con docker compose up -d --force-recreate web y nunca con restart. El registro del contenedor web muestra los orígenes con los que se inició. Es la forma más rápida de confirmar que la corrección se aplicó.

Si ClickHouse no se inicia después de modificar la configuración, lea la primera línea de su registro. Una línea que comienza por oa-entrypoint: indica que el entrypoint ha rechazado un valor configurado por usted. Cualquier otro mensaje suele indicar que el archivo de configuración contiene XML no válido. La causa más habitual es un guion doble dentro de un comentario XML, algo que no está permitido.

AGPL-3.0 y el nombre

El código se distribuye bajo la licencia AGPL-3.0. Ejecutarlo sin modificar para sus propios sitios no crea ninguna obligación de publicación. La obligación comienza cuando modifica el código y ejecuta esa versión modificada como servicio de red: la licencia le exige ofrecer el código fuente modificado a los usuarios de ese servicio. Esto incluye proporcionar a los clientes paneles en su instancia y también integrarlo en un producto que venda. Mantener los cambios en un fork público cumple esta obligación sin ningún trámite adicional.

La marca es independiente del código. El nombre "OpenAnalytics" y el dominio donde el proyecto está alojado identifican la instancia que gestionan sus autores, y no forman parte de la concesión de la licencia. Su despliegue ejecuta el software sin utilizar la marca, por lo que debe asignar un nombre propio al servicio antes de ofrecerlo a clientes de pago.

FAQ

¿Puedo ejecutar OpenAnalytics en un VPS de 1 GB?

No. El proyecto requiere aproximadamente 4 GB de RAM y 25 GB de espacio libre en disco, porque una implementación ejecuta seis servicios de aplicación junto con Postgres, ClickHouse y dos instancias de Valkey. ClickHouse por sí solo no es un proceso pequeño. En un equipo con 1 GB, los contenedores se inician y después el asesino de procesos por falta de memoria del kernel termina uno de ellos, normalmente ClickHouse. Si un plan de 1 GB es una restricción obligatoria, use una herramienta de binario único como GoatCounter, que funciona con SQLite y no necesita una base de datos externa.

¿Necesito un banner de cookies con OpenAnalytics?

Esa es una cuestión para su abogado, y los hechos técnicos son favorables. No hay cookies, la identidad del visitante es un hash con salt que cambia cada día y nunca se almacenan direcciones IP sin procesar, por lo que no se escribe nada persistente que identifique al visitante. El RGPD sigue regulando qué datos almacena y durante cuánto tiempo los conserva. Si quiere controlar explícitamente la recopilación, establezca data-require-consent en la etiqueta de script: el tracker no recopilará nada hasta que se conceda el consentimiento y guardará la respuesta en localStorage bajo oa.consent.

¿Por qué los eventos devuelven 202 pero nunca aparecen en el dashboard?

202 significa que el collector aceptó y puso el evento en cola, no que lo haya almacenado. El worker vacía esa cola en ClickHouse, por lo que un dashboard vacío con peticiones correctas apunta al worker. Lea docker compose logs --tail=50 worker y supervise la profundidad de la cola de Valkey. Una cola que sigue creciendo significa que el worker está bloqueado. Las causas habituales son credenciales incorrectas de ClickHouse en worker.env o la falta de permisos sobre una tabla creada por una migración reciente.

¿Por qué el dashboard está vacío si todos los contenedores están activos?

Compruebe primero AUTH_TRUSTED_ORIGINS en env/api.env. Debe coincidir exactamente con el origen del dashboard. Si no coincide, la API no emite cabeceras CORS, por lo que el navegador rechaza todas las llamadas y muestra una interfaz funcional sin datos. Lo segundo que debe comprobar son los tres valores NEXT_PUBLIC_* en env/web.env, que se sustituyen cuando se inicia el contenedor web. Para corregirlos debe ejecutar docker compose up -d --force-recreate web, porque un reinicio normal conserva los valores antiguos.

¿AGPL-3.0 me impide ofrecer esto a clientes?

No, pero impone una condición. Si ejecuta el código sin modificarlo, no debe nada a nadie. Si lo modifica y ejecuta esa versión modificada como un servicio que utilizan otras personas, debe ofrecer a esos usuarios el código fuente modificado. Un fork público cumple este requisito. Por separado, el nombre "OpenAnalytics" no se licencia junto con el código, por lo que todo lo que venda necesita su propio nombre.