Cómo alojar OpenAnalytics en un VPS
Conozca los requisitos reales de OpenAnalytics: 4 GB de RAM, 25 GB libres, Docker Compose y cuatro registros DNS, además de qué llena el disco.
La infraestructura necesaria 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 indicarse 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 compartidos. ClickHouse almacena los eventos sin procesar y las agregaciones que lee el panel. Valkey se ejecuta dos veces: una instancia 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. Antes de ejecutar una consulta, verifica una firma Ed25519 en cada sobre de consulta.
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 completa añade embudos, métricas web, atribución de ingresos desde su propia cuenta de Stripe y un servidor MCP (model context protocol). Elegir entre herramientas de analítica autoalojadas es el artículo que analiza esa decisión. Esta guía parte de que ya ha elegido.
Primero, apunte los registros DNS al servidor
Los cuatro subdominios deben resolver a la IP pública del servidor antes de iniciar cualquier otra cosa, porque Caddy solicita los certificados de Let's Encrypt en el primer arranque y el desafío falla si el nombre todavía no resuelve.
app.example.comsirve el dashboard.api.example.comsirve la API y las devoluciones de OAuth.c.example.comsirve el collector y el script del tracker.rt.example.comsirve el flujo en tiempo real.
Use cuatro registros A, o un registro A y tres registros CNAME que apunten a él. Confirme con dig +short app.example.com antes de continuar. Un nombre que haya añadido hace un minuto todavía puede estar almacenado en caché como NXDOMAIN por el resolver que Let's Encrypt utilice, por lo que conviene esperar y revisar los logs 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 con Docker Compose
Compruebe una versión etiquetada. La rama predeterminada es donde se desarrolla el proyecto, y la etiqueta de una versión es la que corresponde realmente a las imágenes publicadas. Los comandos siguientes presuponen que Docker y el complemento de 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 -dEl sed '/-/d' de la línea de clonación descarta las etiquetas de versiones preliminares, de modo que se obtiene la versión estable más reciente en lugar de una release candidate. --with-geoip descarga la base de datos de ciudades de DB-IP durante la generación. Si lo omite, todos los eventos tendrán un país nulo y la vista geográfica no mostrará 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, por lo que debe repetir la descarga cada mes para evitar que los datos de ciudades queden desactualizados.
Haga 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 de varias líneas no puede almacenarse en un archivo de entorno. Todo está excluido de Git y no se puede volver a generar con los mismos valores.
Copie ahora esos archivos fuera de la máquina. Cada pérdida tiene una consecuencia concreta:
- Si pierde las contraseñas de los almacenes, no podrá acceder a Postgres ni a ClickHouse. Sólo podrá restablecerlas desde el interior de los contenedores.
- Si pierde
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 pierde
ANONYMOUS_IDENTITY_SECRET, la identidad de los visitantes se vuelve a establecer desde cero: los visitantes de ayer se contabilizan como nuevos y el corte será visible en los gráficos. - Si pierde
AUTH_SECRET, todas las sesiones se invalidan y todos tendrán que iniciar sesión de nuevo. - Si pierde una clave privada de firma, rote el par. No se pierde ningún dato.
Dos secretos deben ser idénticos byte a 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 intencionadamente a un único servicio, y un servicio al que se entregue un secreto que no debe tener termina en lugar de iniciarse.
Inicie la pila y compruébela
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate 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. El resto debería mostrar healthy en docker compose ps. Si un servicio se reinicia en bucle, casi siempre falla la validación del 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 ubicado en el archivo del 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/fstabLa 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, por lo 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 recibirá el tag. La 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óquelo en la cabecera de la página. La clave de tracking es pública por diseño, por lo que debe estar 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 disparado antes no se pierde. Si otro elemento de la página ya utiliza window.oa, el tracker se instala como window.openanalytics.
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 batchEl primer comando debería mostrar 200 y unos pocos kilobytes. Cargue una página del sitio y busque una línea de lote en el registro del worker durante los segundos siguientes. El collector responde con 202 en cuanto acepta un evento, y 202 significa que está en cola, no almacenado. El worker es el que mueve los eventos a ClickHouse. Si se aceptan eventos pero no aparece nada en el panel, significa que 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 en una tabla que acaba de añadir una migración.
Mantener el colector público y el panel protegido con autenticación
Caddy se incluye en el archivo de compose y obtiene por sí solo los certificados para los cuatro nombres, por lo que la configuración predeterminada no requiere ningún trabajo de proxy. Si el servidor ya ejecuta un proxy inverso nginx, coloque la pila detrás del infra/selfhost/nginx.conf.example proporcionado y mantenga intacto su tratamiento de 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 colector calcula el hash diario de visitantes a partir de la IP del cliente, por lo que debe tomar esa dirección de la conexión y nunca de una cabecera. Pasar CF-Connecting-IP desde un salto que no es de confianza permite que cualquier usuario declare cualquier dirección, lo que 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 esos dos. app. y api. sólo deben ser accesibles para las personas que inicien sesión. La autenticación propia de la aplicación protege el panel: el inicio de sesión mediante contraseña está activado de forma predeterminada a través de 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 bandeja de salida, por lo que no se entrega nada y no se produce ningún error.
Un ajuste determina si el panel funciona. AUTH_TRUSTED_ORIGINS en env/api.env debe coincidir exactamente con el origen del panel. Si es incorrecto o falta, la API no emite cabeceras CORS (intercambio de recursos entre orígenes), el navegador rechaza todas las llamadas y el panel muestra su diseño, pero no 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 rastreadores acceden al colector como cualquier otro cliente, y sus visitas a páginas se almacenan en ClickHouse y se incluyen en las métricas. Bloquear los rastreadores de IA en el servidor evita que una parte de ese tráfico llegue a la base de datos antes de que afecte a la precisión y al espacio en disco.
Qué significa aquí «sin cookies» y qué implica
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 el propio disco, por lo que ninguna consulta sobre un visitante sale del host.
Esto evita que se conserve un identificador en el dispositivo del visitante. Ese identificador es precisamente lo que hace que un tracker 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 debe determinar cómo se aplica a su caso, no un README.
El coste es perder la identidad entre días. Como el salt cambia, una persona que visita el sitio el lunes y vuelve el miércoles se cuenta como dos visitantes, de forma intencionada y sin posibilidad de evitarlo. Los recuentos de visitantes únicos diarios son fiables. Los recuentos de visitantes únicos semanales y mensuales se calculan a partir de los diarios y sobrestimarán el alcance. Por tanto, cualquier cifra de «visitantes recurrentes» para periodos largos no mide lo que indica su etiqueta. Las sesiones y los recorridos son fiables dentro de un mismo día. Rotar ANONYMOUS_IDENTITY_SECRET produce el mismo efecto que cambiar de día. Trate esa rotación como un cambio de datos y no como una tarea rutinaria de mantenimiento.
El colector 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 detiene toda la recopilación hasta que se concede el consentimiento y guarda la respuesta en localStorage con la clave oa.consent. Configurar data-storage="none" desactiva por completo el almacenamiento del navegador.
Por qué se llena el disco a los seis meses
Esto es lo que deja fuera de servicio a un servidor de analítica autohospedado. Normalmente, los eventos no son la causa.
Empiece por las imágenes. Una versión publica diez, que ocupan aproximadamente 13 GB en disco. Una actualización descarga la nueva generación antes de eliminar la anterior, por lo que durante un tiempo se conservan dos generaciones. Eso representa la mayor parte de los 25 GB necesarios, antes de recibir siquiera una visita a una página.
Después están las instantáneas. snapshot.sh detiene la pila, archiva ambos volúmenes de datos junto con todos los secretos y la reinicia. Aquí sólo son seguras las copias en frío, porque ClickHouse combina partes en segundo plano y una copia realizada durante una combinación no es coherente. upgrade.sh crea una automáticamente 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 3En un host próximo al límite, recupere la generación anterior antes de actualizar. Es seguro hacerlo mientras la pila está en ejecución, porque las imágenes que respaldan los contenedores en ejecución siguen estando referenciadas:
docker image prune -a -fDespués están los propios eventos. ClickHouse comprime mucho los datos en columnas, por lo que el volumen de eventos sin procesar crece más despacio de lo que espera la mayoría de los usuarios. Además, las tablas de agregación que lee el dashboard son pequeñas 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/clickhousePara 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 medición 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 ninguna opción de retención o 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.
Conviene conocer una trampa relacionada con el borrado antes de que cause problemas. Al eliminar un sitio o una cuenta, el trabajo se pone en cola para el worker. Este necesita que CLICKHOUSE_MAINTENANCE_USER y CLICKHOUSE_MAINTENANCE_PASSWORD estén configurados, y que exista en ClickHouse un usuario oa_maintenance correspondiente. Sin estos elementos, el borrado queda en cola indefinidamente. El sitio desaparece del dashboard, 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.shupgrade.sh muestra tres costes antes de actuar. El tiempo de inactividad es real: los eventos que se intentan enviar mientras el collector está detenido se pierden, porque el tracker no los reintenta. La reversión provoca pérdida de 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 query gateway 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 edición:
docker compose up -d --force-recreate clickhouseEl dashboard presenta la misma trampa. Los tres orígenes NEXT_PUBLIC_* de env/web.env se compilan en el paquete del navegador y se sustituyen cuando se inicia el contenedor, por lo que 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 editar 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, lo cual no está permitido.
AGPL-3.0 y el nombre
El código se distribuye con la licencia AGPL-3.0. Ejecutarlo sin modificaciones 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 este requisito sin ningún proceso adicional.
La marca es independiente del código. El nombre "OpenAnalytics" y el dominio utilizado por el proyecto 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 al servicio un nombre propio 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 servidor de 1 GB, los contenedores se inician y el kernel termina uno de ellos mediante el mecanismo de falta de memoria, normalmente ClickHouse. Si un plan de 1 GB es una limitación estricta, use una herramienta de binario único como GoatCounter, que se ejecuta 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, pero los hechos técnicos están de su parte. No hay cookies, la identidad del visitante es un hash con salt que cambia a diario y nunca se almacenan las direcciones IP sin procesar, por lo que no se escribe nada persistente que permita identificar al visitante. El RGPD sigue regulando qué datos almacena y durante cuánto tiempo los conserva. Si quiere controlar explícitamente la recopilación mediante el consentimiento, 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 panel?
202 significa que el collector aceptó y puso el evento en cola, no que lo almacenara. El worker vacía esa cola en ClickHouse, por lo que un panel 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 indica que el worker está bloqueado. Las causas habituales son unas credenciales incorrectas de ClickHouse en worker.env o la falta de permisos sobre una tabla creada por una migración reciente.
¿Por qué el panel está vacío si todos los contenedores están funcionando correctamente?
Compruebe primero AUTH_TRUSTED_ORIGINS en env/api.env. Debe coincidir exactamente con el origen del panel. Si no coincide, la API no envía cabeceras CORS, por lo que el navegador rechaza todas las peticiones y muestra el diseño cargado, pero sin datos. Lo segundo que debe comprobar son los tres valores NEXT_PUBLIC_* de env/web.env, que se sustituyen cuando se inicia el contenedor web. Para corregirlos se necesita 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 ese requisito. Por separado, el nombre "OpenAnalytics" no se distribuye bajo la misma licencia que el código, por lo que cualquier producto que venda necesita su propio nombre.