Como hospedar o OpenAnalytics em um VPS
Veja os requisitos reais do OpenAnalytics: ClickHouse, Postgres, Valkey, 4 GB de RAM, 25 GB livres e quatro DNS, além do que ocupa o disco.
A infraestrutura antes da primeira etapa
Para alojar o OpenAnalytics por conta própria, precisa de um VPS Linux com cerca de 4 GB de RAM, 25 GB de espaço livre em disco, Docker com o plugin Compose e quatro registos DNS já apontados para o servidor. Este é o requisito essencial e deve ser apresentado antes do primeiro comando, não depois.
A stack é composta por seis serviços de aplicação e três sistemas de armazenamento de dados. O Postgres armazena o plano de controlo: contas, sites, chaves de API e links de partilha. O ClickHouse armazena os eventos brutos e os agregados que o dashboard consulta. O Valkey é executado duas vezes: uma instância funciona como fila de eventos persistente e outra como cache que pode ser perdida, porque estas duas funções exigem políticas de expulsão opostas. Apenas um processo, o gateway de consultas, pode ler o ClickHouse. Antes de executar cada consulta, verifica uma assinatura Ed25519 no envelope da consulta.
Se pretendia um único binário e um único ficheiro de configuração, esta não é a opção adequada. O GoatCounter é a alternativa de binário único nesta categoria: um executável Go, SQLite por predefinição e nenhuma base de dados externa. A stack mais pesada oferece funis, métricas de experiência web, atribuição de receitas a partir da sua própria conta Stripe e um servidor MCP (model context protocol). Escolher entre ferramentas de análise alojadas por conta própria é o artigo que avalia esse compromisso. Este guia parte do princípio de que já tomou essa decisão.
Aponte primeiro os quatro registos DNS para o servidor
Os quatro subdomínios devem resolver para o IP público do servidor antes de iniciar qualquer operação, porque o Caddy solicita certificados Let's Encrypt no primeiro arranque e o desafio falha quando o nome ainda não resolve.
app.example.comdisponibiliza o dashboard.api.example.comdisponibiliza a API e os callbacks OAuth.c.example.comdisponibiliza o coletor e o script do tracker.rt.example.comdisponibiliza o fluxo em tempo real.
Utilize quatro registos A ou um registo A e três registos CNAME que apontem para ele. Confirme com dig +short app.example.com antes de continuar. Um nome adicionado há um minuto ainda pode estar em cache como NXDOMAIN pelo resolvedor que o Let's Encrypt utilizar, por isso vale a pena aguardar e consultar os logs do Caddy quando a primeira tentativa de obter o certificado falhar. Executar novamente a instalação não acelera a propagação do DNS.
Como hospedar o OpenAnalytics por conta própria com Docker Compose
Confira uma release marcada. O branch padrão é usado para o desenvolvimento, enquanto uma tag de release corresponde exatamente às imagens publicadas. Os comandos abaixo pressupõem que o Docker e o plugin Compose já estão instalados, conforme explicado em executar serviços Docker Compose numa 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 -dO sed '/-/d' na linha de checkout ignora tags de pré-release, para que seja obtida a versão estável mais recente em vez de uma release candidate. O --with-geoip obtém a base de dados de cidades DB-IP durante a geração. Se ignorar esse comando, cada evento terá um país nulo e a vista geográfica não mostrará nada. Pode adicioná-la mais tarde executando infra/selfhost/geoip/fetch-dbip.sh, definindo GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb em env/collector.env e recriando o collector com docker compose up -d --force-recreate collector. Essa base de dados é atualizada mensalmente. Repita a obtenção todos os meses para evitar que os dados das cidades fiquem desatualizados.
Faça uma cópia de segurança dos segredos gerados antes de continuar
O gerador escreve três elementos. .env contém os nomes de domínio e as referências das imagens. env/*.env contém um ficheiro de segredos por serviço. docker-compose.override.yml contém três pares de chaves Ed25519 como escalares de bloco YAML, porque um PEM com várias linhas não pode ser armazenado num ficheiro de ambiente. Todos estes ficheiros são ignorados pelo Git, e nenhum pode ser regenerado com os mesmos valores.
Copie agora esses ficheiros para fora da máquina. Cada perda tem uma consequência específica:
- Se perder as palavras-passe dos serviços de armazenamento, fica sem acesso ao Postgres e ao ClickHouse. Só é possível redefini-las a partir do interior dos contentores.
- Se perder
OA_CREDENTIAL_KEYRING, todas as credenciais de terceiros armazenadas ficam irrecuperáveis. Qualquer pessoa que tenha ligado uma conta Stripe terá de a ligar novamente. - Se perder
ANONYMOUS_IDENTITY_SECRET, a identidade dos visitantes é recalibrada: os visitantes de ontem passam a contar como novos, e a interrupção fica visível nos gráficos. - Se perder
AUTH_SECRET, todas as sessões são invalidadas e todos têm de iniciar sessão novamente. - Se perder uma chave privada de assinatura, rode o par de chaves. Nada é perdido.
Dois segredos têm de ser idênticos byte a byte em dois ficheiros cada. ANONYMOUS_IDENTITY_SECRET aparece em collector.env e worker.env, porque o collector calcula o hash do visitante e o worker escreve-o. OA_CREDENTIAL_KEYRING aparece em api.env e worker.env. Todos os restantes segredos estão deliberadamente limitados a exatamente um serviço. Se um serviço receber um segredo que não deve possuir, termina em vez de iniciar.
Inicie a stack e verifique-a
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate aplica os esquemas do Postgres e do ClickHouse e depois termina, por isso um contentor migrate parado é o estado final correto. tracker-build compila oa.js num volume servido pelo Caddy e também termina. Todos os restantes serviços devem apresentar healthy em docker compose ps. Um serviço que reinicia continuamente quase sempre está a falhar na validação do ambiente. O log apresenta todos os problemas numa única lista, em vez de mostrar um problema por reinício. As duas causas habituais são uma variável deixada em branco, que é rejeitada em vez de ser tratada como não definida, e um segredo colocado no ficheiro de serviço errado.
Em arm64, ou a partir de um branch, não existem imagens publicadas e a compilação é feita localmente com docker compose up -d --build. Um host com 4 GB fica sem memória a meio dessa compilação. Adicione primeiro swap. Isto só é necessário durante a compilação:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabA compilação demora aproximadamente dez minutos. O pull demora alguns minutos, e é por isso que existem imagens da release.
Reivindique imediatamente a primeira conta
Abra https://app.example.com. Uma instalação na qual ninguém iniciou sessão ainda não apresenta um formulário de início de sessão: oferece a criação da primeira conta. Essa conta permanece permanentemente como a conta privilegiada e é a única que vê o ecrã de definições da instalação. Depois de criada, a rota responde com 409, para que ninguém possa entrar depois de si. Faça isto assim que a stack estiver saudável, não na semana seguinte.
Instale o tracker
Adicione um site no dashboard para receber a tag. O formato é fixo:
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>Coloque-a no cabeçalho da página. A chave de tracking é pública por definição, por isso deve ficar no HTML, onde qualquer pessoa a pode ler. O script instala window.oa, e chamadas como oa("track", ...) são colocadas em fila por um stub e processadas quando o ficheiro é carregado. Assim, um evento personalizado emitido no início não é perdido. Se outro elemento da página já utilizar window.oa, o tracker instala-se como window.openanalytics.
Depois, verifique todo o percurso de ponta a ponta:
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 batchO primeiro comando deve apresentar 200 e alguns kilobytes. Carregue uma página do seu site e procure uma linha de lote no log do worker dentro de alguns segundos. O collector responde 202 assim que aceita um evento, e 202 significa colocado em fila, não armazenado. O worker é responsável por mover os eventos para o ClickHouse. Se os eventos forem aceites, mas nada aparecer no dashboard, o worker está bloqueado. Um crescimento contínuo da profundidade da fila do Valkey confirma-o. As causas habituais são credenciais incorretas do ClickHouse em worker.env ou uma permissão em falta numa tabela adicionada por uma migration recente.
Mantenha o collector público e o dashboard protegido por autenticação
O Caddy vem incluído no ficheiro compose e obtém automaticamente certificados para os quatro nomes, pelo que o caminho predefinido não exige configuração de proxy. Se a máquina já executar um reverse proxy Nginx, coloque a stack atrás do infra/selfhost/nginx.conf.example fornecido e mantenha o tratamento dos cabeçalhos intacto:
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 "";O collector calcula o hash diário dos visitantes a partir do IP do cliente. Por isso, tem de obter esse endereço da ligação e nunca de um cabeçalho. Encaminhar CF-Connecting-IP a partir de um intermediário não confiável permite que qualquer cliente alegue ter qualquer endereço. Isso corrompe a geolocalização e aumenta artificialmente a contagem de visitantes.
O acesso é separado de forma simples pelo nome do host. c. e rt. têm de estar acessíveis a todos os visitantes de todos os sites que mede. Nunca coloque autenticação básica nem uma lista de permissões de IP à frente desses dois endereços. app. e api. só precisam de estar acessíveis a utilizadores autenticados. A autenticação da própria aplicação protege o dashboard. O início de sessão por palavra-passe está ativado por predefinição através de AUTH_PASSWORD_SIGNIN=enabled em env/api.env. Os botões do Google ou do GitHub só aparecem quando existem o ID do cliente e o segredo do cliente desse fornecedor. As ligações mágicas precisam de um transporte de correio. Sem esse transporte, a API apenas regista o envio numa outbox. Nada é entregue e nenhum erro é gerado.
Uma definição determina se o dashboard funciona. AUTH_TRUSTED_ORIGINS em env/api.env tem de corresponder exatamente à origem do dashboard. Se estiver incorreta ou ausente, a API não envia cabeçalhos CORS (partilha de recursos entre origens), o browser recusa todas as chamadas e o dashboard apresenta o layout, mas não mostra dados, enquanto docker compose ps indica que está tudo saudável.
Enquanto estiver a editar a configuração do proxy, trate também do tráfego automatizado. Os crawlers acedem ao collector como qualquer outro cliente, e as visualizações das páginas ficam registadas no ClickHouse e nas suas métricas. Bloquear crawlers de IA no servidor impede que parte desse tráfego chegue à base de dados antes de afetar a precisão e consumir espaço em disco.
O que significa não usar cookies neste caso e qual é o custo
Não existe cookie. A identidade do visitante é um hash com salt, o salt muda todos os dias e os endereços IP brutos nunca são armazenados. A geolocalização é resolvida localmente com base no ficheiro DB-IP armazenado no seu próprio disco. Assim, nenhuma consulta sobre um visitante sai do host.
A vantagem é não existir um identificador persistido no dispositivo do visitante. É precisamente esse identificador que sujeita um tracker às regras de consentimento do ePrivacy na UE. Por esse motivo, configurações que guardam apenas dados agregados, como esta, são normalmente executadas sem um banner de consentimento. O GDPR continua a aplicar-se a tudo o que armazenar e ao período de retenção. A sua assessoria jurídica deve avaliar o seu caso, não um README.
O custo é perder a identidade entre dias. Como o salt muda, uma pessoa que visite o site na segunda-feira e novamente na quarta-feira será contada como dois visitantes. Isto é intencional e não existe uma forma de contornar o comportamento. As contagens de visitantes únicos por dia são consistentes. As contagens semanais e mensais de visitantes únicos são calculadas a partir das contagens diárias e irão sobrestimar o alcance. Por isso, qualquer valor de "visitantes recorrentes" para períodos longos não mede o que o respetivo rótulo indica. As sessões e os percursos são fiáveis dentro de um único dia. A rotação de ANONYMOUS_IDENTITY_SECRET tem o mesmo efeito que a mudança de dia. Trate essa rotação como uma alteração de dados, e não como manutenção de rotina.
O coletor respeita Do Not Track e Global Privacy Control, o sinal do navegador que informa um site de que não deve vender nem partilhar dados pessoais. A tag de script inclui as suas próprias opções para o mesmo fim: data-respect-gpc, data-respect-dnt e data-require-consent. Esta última mantém toda a recolha suspensa até o consentimento ser concedido e guarda a resposta em localStorage, usando a chave oa.consent. Definir data-storage="none" desativa totalmente o armazenamento no navegador.
Por que o disco fica cheio ao fim de seis meses
É isto que derruba um servidor de analytics self-hosted. Normalmente, os eventos não são a causa.
Comece pelas imagens. Uma release publica dez imagens, que ocupam cerca de 13 GB em disco. Uma atualização obtém a nova geração antes de remover a antiga. Durante algum tempo, ficam duas gerações no disco. Isto representa a maior parte dos 25 GB necessários, antes de chegar uma única visualização de página.
Depois, há os snapshots. snapshot.sh interrompe a stack, arquiva os dois volumes de dados juntamente com todos os secrets e reinicia a stack. Neste caso, as cópias a frio são as únicas seguras, porque o ClickHouse faz merge das partes em segundo plano e uma cópia criada durante um merge não é consistente. upgrade.sh cria automaticamente um snapshot antes de cada atualização. Assim, os arquivos acumulam-se no mesmo disco até definir um limite.
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3Num host próximo do limite, recupere a geração anterior antes de atualizar. Isto é seguro enquanto a stack está em execução, porque as imagens usadas pelos containers em execução continuam referenciadas:
docker image prune -a -fDepois, há os próprios eventos. O ClickHouse comprime fortemente os dados colunares. Por isso, o volume de eventos brutos cresce mais lentamente do que a maioria das pessoas espera. As tabelas de rollup lidas pelo dashboard são pequenas em comparação com a tabela bruta. Meça em vez de fazer estimativas:
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhousePara obter o valor por tabela, execute isto com as credenciais do ClickHouse que o gerador escreveu em 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;Faça essa medição na primeira semana e repita-a na quarta semana. Dois pontos permitem calcular uma taxa de crescimento. Essa taxa indica quando é necessário redimensionar o volume. Em agosto de 2026, o guia de self-hosting não documenta nenhuma opção de retenção ou time-to-live para eventos brutos. Por isso, dimensione o disco com base na taxa medida. Não presuma que as linhas antigas expiram automaticamente.
É importante conhecer uma armadilha relacionada com eliminações. Ao eliminar um site ou uma conta, o trabalho é colocado numa fila para o worker. Esse worker precisa que CLICKHOUSE_MAINTENANCE_USER e CLICKHOUSE_MAINTENANCE_PASSWORD estejam definidos, e que exista um utilizador correspondente em oa_maintenance no ClickHouse. Sem esses valores, a eliminação fica eternamente na fila. O site desaparece do dashboard, mas todas as linhas permanecem no disco. O resultado parece uma limpeza, mas nenhum espaço é recuperado.
Atualizações e os três custos
git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.shupgrade.sh apresenta três custos antes de agir. A indisponibilidade é real: os eventos tentados enquanto o coletor está parado são perdidos, porque o tracker não tenta processá-los novamente. O rollback causa perda de dados, porque rollback.sh --to backups/<snapshot> substitui ambos os armazenamentos por completo e descarta todas as linhas gravadas depois da criação desse snapshot. O disco é o terceiro custo: corresponde ao conjunto de snapshots descrito acima.
É fácil aplicar incorretamente duas regras de reinício. Inicie o gateway de consultas antes da API, porque uma API mais recente envia campos de consulta que um gateway mais antigo rejeita. O ClickHouse precisa de uma recriação, e não de um reinício, porque docker compose restart reutiliza o ambiente original do container e ignora silenciosamente a sua alteração:
docker compose up -d --force-recreate clickhouseO dashboard tem o mesmo tipo de armadilha. As três origens NEXT_PUBLIC_* em env/web.env são compiladas no bundle do browser e substituídas quando o container inicia. Por isso, um dashboard que chama o hostname errado é corrigido com docker compose up -d --force-recreate web e nunca com restart. O log do container web mostra as origens com que ele iniciou. Esta é a forma mais rápida de confirmar que a correção foi aplicada.
Se o ClickHouse se recusar a iniciar depois de uma alteração de configuração, leia a primeira linha do log. Uma linha que começa com oa-entrypoint: indica que o entrypoint rejeitou um valor definido por si. Qualquer outra mensagem normalmente significa que o ficheiro de configuração contém XML inválido. A causa mais comum é um hífen duplo dentro de um comentário XML, o que é ilegal nesse contexto.
AGPL-3.0 e o nome
O código é licenciado ao abrigo da AGPL-3.0. Executá-lo sem modificações nos seus próprios sites não cria qualquer obrigação de disponibilização do código-fonte. A obrigação começa quando modifica o código e executa essa versão modificada como um serviço de rede: a licença passa a exigir que disponibilize o código-fonte modificado aos utilizadores desse serviço. Isto aplica-se à disponibilização de dashboards aos clientes na sua instância e também à inclusão do software num produto que vende. Manter as alterações num fork público cumpre esse requisito sem qualquer processo adicional.
A marca é independente do código. O nome "OpenAnalytics" e o domínio onde o projeto está alojado identificam a instância operada pelos autores e não fazem parte da concessão da licença. A sua implementação executa o software sem utilizar a marca, por isso atribua ao serviço um nome próprio antes de o disponibilizar a clientes pagantes.
FAQ
Posso executar o OpenAnalytics num VPS de 1 GB?
Não. O projeto requer cerca de 4 GB de RAM e 25 GB de espaço livre em disco, porque uma implementação executa seis serviços de aplicação juntamente com o Postgres, o ClickHouse e duas instâncias do Valkey. O ClickHouse, por si só, não é um processo pequeno. Num servidor com 1 GB, os contentores arrancam e o kernel termina depois um deles por falta de memória, normalmente o ClickHouse. Se um plano de 1 GB for uma restrição rígida, use uma ferramenta de binário único, como o GoatCounter, que utiliza SQLite sem uma base de dados externa.
Preciso de um banner de cookies com o OpenAnalytics?
Essa é uma questão para o seu advogado, mas os factos técnicos são favoráveis. Não existe cookie, a identidade do visitante é um hash com salt que roda diariamente e os endereços IP brutos nunca são armazenados. Assim, não é gravado nada persistente para identificar o visitante. O RGPD continua a reger o que armazena e durante quanto tempo o conserva. Se quiser condicionar explicitamente a recolha, defina data-require-consent na tag de script: o tracker não recolhe nada até o consentimento ser concedido e mantém a resposta em localStorage sob oa.consent.
Por que motivo os eventos devolvem 202, mas nunca aparecem no dashboard?
202 significa que o collector aceitou e colocou o evento numa fila, não que o armazenou. O worker esvazia essa fila para o ClickHouse. Portanto, um dashboard vazio com pedidos bem-sucedidos aponta para o worker. Leia docker compose logs --tail=50 worker e monitorize a profundidade da fila do Valkey. Uma fila que continua a crescer significa que o worker está bloqueado. As causas mais comuns são credenciais incorretas do ClickHouse em worker.env ou uma permissão em falta numa tabela criada por uma migração recente.
Por que motivo o dashboard está vazio quando todos os contentores estão saudáveis?
Verifique primeiro AUTH_TRUSTED_ORIGINS em env/api.env. Tem de corresponder exatamente à origem do dashboard. Quando não corresponde, a API não envia cabeçalhos CORS. O browser recusa então todas as chamadas e apresenta um layout funcional sem dados. A segunda verificação deve incidir nos três valores NEXT_PUBLIC_* em env/web.env, que são substituídos quando o contentor web arranca. Para os corrigir, é necessário docker compose up -d --force-recreate web, porque um simples restart mantém os valores antigos.
A AGPL-3.0 impede-me de disponibilizar isto a clientes?
Não, mas impõe uma condição. Se executar o código sem alterações, não deve nada a ninguém. Se o modificar e executar essa versão modificada como um serviço utilizado por outras pessoas, tem de disponibilizar o código-fonte modificado a esses utilizadores. Um fork público cumpre esse requisito. Separadamente, o nome "OpenAnalytics" não está licenciado juntamente com o código. Por isso, qualquer produto que venda precisa de ter um nome próprio.