OpenSEO: instalando na VPS e auditando um site
Olá meus Unicórnios! 🦄✨
Sabe aquela ferramenta de SEO que todo mundo recomenda e que custa mais por mês do que a sua VPS custa no ano? 😅 Pois é. Semrush e Ahrefs são ótimos, mas eu queria só uma coisa: pegar o meu site, o palomamacetko.com.br, e perguntar para um robô o que ele enxerga lá dentro.
Foi aí que eu cheguei no OpenSEO: um projeto aberto, com licença MIT, que se apresenta como alternativa ao Semrush e ao Ahrefs e roda no seu próprio servidor. Instalei na minha VPS, apontei para o meu site e... levei um susto. O robô olhou para as sete páginas do meu portfólio e disse que todas tinham zero palavras. 🤯
Neste artigo eu mostro o caminho inteiro: o que é o OpenSEO, como instalar numa VPS Ubuntu com Docker (explicando cada comando, mesmo que você nunca tenha mexido num servidor), como abrir a tela com segurança, e o que a auditoria revelou sobre o meu site. Spoiler: o robô estava certo, e o problema era meu.
🧭 O que é o OpenSEO
O OpenSEO é um painel de SEO que você hospeda. Ele junta num lugar só as tarefas que as ferramentas pagas fazem: pesquisa de palavras-chave, visão geral de um domínio, backlinks, acompanhamento de posição no Google e auditoria técnica do site. Ele ainda conversa com agentes de IA pelo protocolo MCP, mas isso é assunto para outro dia.
O detalhe que muda tudo é de onde vêm os dados. O OpenSEO não tem um banco próprio com o Google inteiro indexado (ninguém tem isso de graça). Ele usa a DataForSEO, um serviço pago por uso, e você coloca a sua chave. Não há assinatura: você paga só as consultas que fizer.
Só que nem tudo passa pela DataForSEO, e isso eu descobri na prática:
| Parte do painel | Precisa da chave da DataForSEO? |
|---|---|
| Auditoria do site (Site Audit) | Não. O próprio OpenSEO visita as páginas |
| Nota do Lighthouse dentro da auditoria | Sim |
| Visão geral do domínio, palavras-chave, backlinks | Sim |
| Assistente de IA (SAM) | Não, mas pede uma chave da OpenRouter |
| Dados do Search Console | Não, mas pede um cliente OAuth do Google |
Ou seja: dá para instalar hoje, sem gastar nada, e já usar a auditoria de verdade. É exatamente o que vamos fazer.
🖥️ O que a VPS precisa ter
Usei uma VPS com Ubuntu 24.04, 4 vCPU e 7,8 GB de RAM. O OpenSEO parado ocupa uns 800 MB de memória, mas na primeira subida ele compila o app inteiro dentro do contêiner, e nessa hora eu medi 1,6 GB. A imagem ocupa 3,69 GB de disco. Uma VPS de 4 GB de RAM dá conta, desde que não esteja no limite com outras coisas.
Para entrar na VPS, abra o terminal do seu computador (no Windows, o PowerShell; no Mac e no Linux, o Terminal) e conecte com o ssh, trocando pelo IP da sua máquina:
ssh root@SEU_IP_AQUI
Ele pede a senha. Um aviso que trava todo mundo na primeira vez: enquanto você digita a senha, nada aparece na tela, nem asterisco. Não está travado, é assim mesmo. Digite e aperte Enter.
O OpenSEO roda em Docker, que é um jeito de rodar um programa dentro de uma "caixinha" com tudo de que ele precisa. Se a sua VPS ainda não tem Docker, é um comando só:
curl -fsSL https://get.docker.com | sh
E para conferir que ele está lá, junto com o docker compose, que é quem lê o arquivo de configuração do OpenSEO:
docker --version
docker compose version
Docker version 29.8.1, build 4a63305
Docker Compose version v5.5.1
📥 Baixando o OpenSEO
Vamos guardar o projeto em /opt, a pasta que o Linux reserva para programas instalados à mão. O cd entra numa pasta, e o git clone baixa o repositório do GitHub:
cd /opt
git clone https://github.com/every-app/open-seo.git
cd open-seo
Se o git reclamar que não existe, instale com apt install -y git e repita.
Um detalhe que eu gostei: você não precisa compilar nada aqui. O repositório serve para trazer o compose.yaml e o modelo de configuração; o programa em si vem pronto do registro do GitHub, como imagem Docker.
📝 O arquivo .env
A configuração mora num arquivo chamado .env. O projeto traz um modelo, o .env.example, e o primeiro passo é copiar esse modelo com o nome certo. O cp copia arquivos:
cp .env.example .env
Se você rodar ls agora, vai achar que a cópia não funcionou, porque o .env não aparece. Calma: arquivo cujo nome começa com ponto é oculto no Linux. Para ver, use ls -la, que mostra todos.
Agora vamos editar. O nano é um editor de texto que roda dentro do terminal:
nano .env
O arquivo é quase todo comentário (linha que começa com # é ignorada). Nesta primeira subida eu só acrescentei uma linha no final, para desligar a telemetria anônima que o OpenSEO envia (ela manda só contagens, mas eu prefiro desligada). Use as setas do teclado para descer até o fim e escreva:
OPENSEO_TELEMETRY_DISABLED=1
Para salvar no nano: aperte Ctrl+O (a letra O), confirme com Enter, e saia com Ctrl+X. O rodapé do nano mostra esses atalhos com um ^ na frente, e o ^ quer dizer Ctrl.
Repare que eu não preenchi a chave da DataForSEO ainda. A linha dela continua comentada, e isso é proposital: quero mostrar o que funciona sem ela.
🚀 Subindo o OpenSEO
Com o .env no lugar, um comando baixa a imagem e liga o contêiner. O -d significa "em segundo plano": ele devolve o terminal para você em vez de ficar preso na tela.
docker compose up -d
Aqui a imagem levou 1 minuto e meio para baixar. E aí vem a parte em que todo mundo acha que deu errado: o contêiner liga, mas a tela não abre ainda. Na primeira vez, o OpenSEO compila o app lá dentro, e isso leva de 1 a 2 minutos. Para acompanhar, veja o log:
docker compose logs -f
O -f fica seguindo o log ao vivo. Para sair, Ctrl+C: isso fecha só a visualização, o OpenSEO continua ligado. As primeiras linhas são as mais úteis, porque ele confere a configuração antes de começar:
open-seo-1 | --- OpenSEO self-host preflight ---
open-seo-1 | [ ok ] AUTH_MODE: local_noauth — no auth, single admin user. Do not expose publicly without your own auth in front.
open-seo-1 | [warn] DATAFORSEO_API_KEY: Not set — all SEO data features will be unavailable until it is. It is the base64 of your DataForSEO login:password (NOT the dashboard API key). See docs/DATAFORSEO_API_KEY.md.
open-seo-1 | [info] Search Console: Not configured (optional). See docs/SELF_HOSTING_GOOGLE_SEARCH_CONSOLE.md.
open-seo-1 | [info] AI features: OPENROUTER_API_KEY not set (optional) — SAM, the in-app SEO agent, is disabled.
open-seo-1 | [info] ALLOWED_HOST: Not set — only localhost access will work. Behind a reverse proxy or tunnel, set ALLOWED_HOST=yourdomain.com or requests are blocked with Vite's "Blocked request" page.
open-seo-1 | [info] Scheduled checks: Rank-tracking schedules do not run in Docker mode — trigger checks from the Rank Tracking page.
open-seo-1 | Preflight passed. The app now builds inside the container (~1-2 minutes on first start before it serves).
open-seo-1 | Building client + server (first start, changed build env, or new image)...
Leia com calma, porque essas seis linhas contam quase tudo que este artigo vai tratar: roda sem login, a chave da DataForSEO está faltando, e o acesso só funciona por localhost. O [warn] não impede nada; ele só avisa.
Uma curiosidade: mesmo com OPENSEO_TELEMETRY_DISABLED=1, o log começa com a frase "OpenSEO sends an anonymous usage heartbeat". Fui olhar o script de entrada, e essa frase é um echo fixo que aparece sempre. Quem lê a variável é o app, depois.
Quando aparecer Local: http://localhost:3001/ no log, está pronto. Confira o estado com:
docker compose ps
time="2026-09-24T14:10:03+02:00" level=warning msg="The \"DATAFORSEO_API_KEY\" variable is not set. Defaulting to a blank string."
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
open-seo-open-seo-1 ghcr.io/every-app/open-seo:latest "docker-entrypoint.s…" open-seo 2 minutes ago Up 2 minutes (healthy) 127.0.0.1:3001->3001/tcp
A primeira linha, o warning sobre o DATAFORSEO_API_KEY, vai aparecer em todo comando docker compose enquanto a chave estiver comentada no .env. Não é erro, é o Compose avisando que a variável está vazia. O (healthy) quer dizer que ele passou na checagem de saúde. E olhe bem a coluna PORTS: 127.0.0.1:3001. Essa é a próxima seção inteira.
🔒 Por que ele não abre pelo IP (e ainda bem)
A primeira coisa que eu fiz foi abrir http://SEU_IP_AQUI:3001 no navegador. Não abriu. Pelo terminal, a conexão é recusada na hora:
curl -s -m 8 -o /dev/null -w "%{http_code}\n" http://SEU_IP_AQUI:3001/ ; echo "exit=$?"
000
exit=7
O código 000 com saída 7 é o jeito do curl dizer "nem consegui conectar". E isso não é defeito: o compose.yaml do projeto publica a porta só em 127.0.0.1, o endereço que quer dizer "a própria máquina".
O motivo está naquela primeira linha do log: local_noauth. No modo Docker, o OpenSEO não tem tela de login. Quem abrir o endereço entra direto como administrador. Se a porta ficasse aberta para a internet, qualquer pessoa usaria o seu painel e, com a chave configurada, gastaria o seu crédito da DataForSEO. 😳
Então como a gente abre a tela? Com um túnel SSH. Ele faz a porta 3001 do seu computador "virar" a porta 3001 da VPS, passando por dentro da conexão SSH, que já é criptografada e já pede senha. No terminal do seu computador (não na VPS):
ssh -N -L 3001:127.0.0.1:3001 root@SEU_IP_AQUI
Traduzindo: -L 3001:127.0.0.1:3001 liga a porta 3001 daqui ao 127.0.0.1:3001 de lá, e o -N diz "não quero abrir um terminal, só o túnel". Ele pede a senha, e depois parece que travou, sem mostrar nada. É assim mesmo: deixe essa janela aberta enquanto usa o OpenSEO. Fechou a janela, fechou o túnel.
Agora, no navegador do seu computador, abra:
http://localhost:3001
E o OpenSEO aparece, já pedindo a chave da DataForSEO:
Clique em Dismiss por enquanto. A janela volta a cada troca de tela, e uma faixa amarela fica no topo lembrando da chave, mas nada disso impede o que vamos fazer a seguir.
🔍 Auditando o meu site
No menu da esquerda, em MY SITE, clique em Site Audit. A tela é simples: o endereço do site, quantas páginas no máximo ele pode visitar, e uma chave para incluir o Lighthouse.
Coloquei https://www.palomamacetko.com.br/, deixei o limite padrão de 50 páginas e o Lighthouse desligado, porque a nota do Lighthouse é buscada pela DataForSEO e eu ainda estava sem chave. Cliquei em Start Audit.
Enquanto ele trabalha, vale saber como ele visita o site, porque isso explica o resultado. Fui ler o código do auditor, e ele:
- lê primeiro o
robots.txte respeita o que ele proíbe; - descobre as páginas pelo
sitemap.xmle pelos links que encontra; - se apresenta como
OpenSEO-Audit/1.0, então dá para achar as visitas dele no log do seu servidor; - faz uma requisição por segundo e, se o servidor responder
429(muitas requisições), espera e tenta de novo, em vez de martelar; - e lê o HTML com um analisador de texto, sem montar a página e sem executar JavaScript. Guarde essa última.
Em poucos segundos veio o resultado:
7 páginas, 41 problemas, 37 ms de resposta média. A resposta rápida me deixou orgulhosa por um segundo. Os 41 problemas, nem tanto. Traduzindo os seis tipos de aviso:
| Aviso | O que quer dizer | Páginas |
|---|---|---|
| Descrição repetida | A mesma meta description em várias páginas | 7 |
| Título repetido | O mesmo <title> em várias páginas | 7 |
| Sem H1 | A página não tem título principal | 7 |
| Sem links de saída | A página não aponta para nenhuma outra | 7 |
| Conteúdo raso | Menos de 150 palavras de texto | 7 |
| Página órfã | Nenhuma outra página aponta para ela | 6 |
Na tela eles aparecem em inglês: Duplicate meta description, Duplicate title, Missing H1 heading, Page has no outgoing links, Thin content e Orphan page. O limite de 150 palavras do conteúdo raso está no código do auditor (THIN_CONTENT_WORDS = 150).
Clicando em cada aviso, ele abre a explicação, o jeito de corrigir e a lista de páginas afetadas:
Nenhuma página com H1? Nenhum link de saída? Eu sei que o meu site tem títulos e um menu cheio de links. Eu mesma escrevi. 🤨
😳 A tabela que me deixou de boca aberta
A aba Pages mostra o que o auditor extraiu de cada página. E foi aqui que eu entendi:
Sete páginas, todas com status 200, todas com o mesmo título, e todas com 0 H1, 0 palavras, 0 imagens. Não é que faltasse conteúdo. É que, para o robô, não havia conteúdo nenhum.
O meu portfólio é feito em React. E site em React, do jeito mais comum, funciona assim: o servidor entrega uma página quase vazia, com uma <div id="root"> e um arquivo JavaScript. Quem monta os textos, os títulos e os links é o navegador, depois de rodar esse JavaScript. Quem não roda JavaScript fica com a casca.
Dá para confirmar sem o OpenSEO, com o curl, que baixa a página do jeito que o servidor entrega. Primeiro o título de três páginas diferentes:
for p in / /sobre /projetos; do curl -s "https://www.palomamacetko.com.br$p" | grep -o "<title>.*</title>"; done
<title>Paloma Macetko — Desenvolvedora Web</title>
<title>Paloma Macetko — Desenvolvedora Web</title>
<title>Paloma Macetko — Desenvolvedora Web</title>
O mesmo título nas três. Agora, quantos <h1> tem a página "Sobre", e qual o tamanho dela em bytes:
curl -s https://www.palomamacetko.com.br/sobre | grep -c "<h1"
curl -s https://www.palomamacetko.com.br/sobre | wc -c
0
3794
Zero H1, e a página inteira tem 3.794 bytes, dos quais boa parte é um comentário meu explicando o esqueleto da capa. 😅 Já no navegador, depois que o React monta, a mesma página "Sobre" tem um título próprio (a aba do navegador passa a mostrar Sobre antes do meu nome), um H1 "O começo do encantamento", 461 palavras e 16 links. Tudo isso existe, só que nasce no navegador.
E aí os 41 avisos fazem sentido, um por um:
- Título e descrição repetidos: o servidor manda o mesmo
<head>para todas as rotas. O título certo só é trocado pelo JavaScript. - Sem H1 e conteúdo raso: o texto não está no HTML.
- Sem links de saída e páginas órfãs: os links do menu também são montados pelo React. Então como o robô achou as sete páginas? Pelo
sitemap.xml, que lista exatamente essas sete. Sem ele, o auditor teria parado na primeira.
Eu podia ficar brava com a ferramenta. Mas ela está mostrando exatamente o que um robô que não executa JavaScript enxerga. O Google até executa JavaScript, só que numa etapa separada, depois de baixar o HTML; e muito robô por aí (inclusive vários de IA) lê só o HTML e vai embora. Para esses, o meu portfólio é uma página em branco com um nome no título. 🙈
🆚 A prova: o mesmo auditor no blog
Faltava descartar a hipótese de o OpenSEO estar quebrado. Então rodei a mesma auditoria, com limite de 20 páginas, aqui no blog. O blog é PHP puro: cada página sai do servidor já com todo o texto dentro.
Agora sim: um H1 por página, títulos diferentes, e contagens de palavras que batem com os artigos (3.562 no da API Mágica, 5.798 no do DeskcommCRM). Mesmo auditor, mesma configuração, resultado completamente diferente. Os zeros eram do meu site, não da ferramenta.
Se você só for levar uma coisa deste artigo, leve esta: antes de acreditar numa auditoria de SEO, olhe a aba de páginas. Se tudo estiver zerado e o site for feito em React, Vue ou parecido, o problema não é a falta de H1 em sete lugares. É um só: o HTML chega vazio. 🙏
🗝️ A chave da DataForSEO
Para liberar o resto do painel (visão geral do domínio, palavras-chave, backlinks e a nota do Lighthouse), você precisa da sua própria conta na DataForSEO. Pela documentação do OpenSEO, conta nova ganha US$ 1 de crédito para testar, e a recarga mínima é de US$ 50.
E aqui mora a armadilha que o próprio log já avisava em maiúsculas: "NOT the dashboard API key". O DATAFORSEO_API_KEY não é uma chave que você copia do painel. Ele é o base64 de login:senha da API. O caminho:
- Entre em API Access no painel da DataForSEO (
app.dataforseo.com/api-access). - Clique em Send by email. Eles mandam as credenciais por e-mail.
- No e-mail, copie o valor mais comprido, o que vem rotulado como Base64.
Se preferir montar o valor você mesma, o comando base64 do Linux faz isso. Com um login e senha de exemplo:
echo -n '[email protected]:SUA_SENHA_DA_API' | base64
dm9jZUBleGVtcGxvLmNvbTpTVUFfU0VOSEFfREFfQVBJ
O -n é importante: sem ele, o echo acrescenta uma quebra de linha no fim, ela entra na conta, e o base64 sai diferente (e errado).
Com o valor em mãos, abra o .env de novo com nano .env, ache a linha # DATAFORSEO_API_KEY=, apague o # do começo e cole o seu valor depois do =, sem espaço e sem aspas:
DATAFORSEO_API_KEY=SUA_CHAVE_AQUI
Salve (Ctrl+O, Enter, Ctrl+X) e aplique. E aqui vem a segunda armadilha: docker compose restart não relê o .env. Ele só religa o mesmo contêiner, com as variáveis antigas. O comando certo recria o contêiner:
docker compose up -d --force-recreate open-seo
E prepare-se para esperar de novo. O contêiner novo não tem o app compilado do anterior, então ele refaz o build inteiro, aquele de 1 a 2 minutos. Eu vi a diferença no log: depois de um restart aparece Reusing existing build, e depois de um --force-recreate aparece Building client + server de novo. Não é travamento, é compilação.
🤥 Duas mensagens que enganam
Testei os dois jeitos de errar a chave, porque é o erro mais provável de quem instala. E nos dois o OpenSEO diz uma coisa que não ajuda muito.
Sem chave nenhuma, tentei a visão geral do domínio (Domain Overview) com palomamacetko.com.br. A tela respondeu com um erro genérico:
"Um erro inesperado, olhe o log do servidor." Só que não tem nada de inesperado. O motivo de verdade está no log, que você vê com docker compose logs:
open-seo-1 | server.function error: Error: Missing required environment variable: DATAFORSEO_API_KEY
Com uma chave errada, a mentira é outra, e mais traiçoeira. Coloquei no .env aquele base64 de exemplo, de uma conta que não existe, e recriei o contêiner. O log de conferência respondeu:
open-seo-1 | [ ok ] DATAFORSEO_API_KEY: Set
[ ok ]. Para uma chave falsa. 😳 É que essa checagem só confere se a variável está preenchida, não se ela funciona. O endereço de saúde, /api/health, diz a mesma coisa ("dataforseo": {"status": "ok", "detail": "Set"}). A prova real só vem na primeira busca:
Essa mensagem, pelo menos, é ótima: diz que a DataForSEO recusou e lembra que o valor é o base64 de login:password. No log aparece o código HTTP que a DataForSEO devolveu:
docker compose logs --since 3m | grep -i -E 'dataforseo|401' | head -6
O --since 3m mostra só os últimos 3 minutos, o grep filtra as linhas que falam da DataForSEO, e o head -6 para nas seis primeiras:
open-seo-1 | server.function error: AppError [DataForSEOHttpError]: DataForSEO HTTP 401 on /v3/dataforseo_labs/google/domain_rank_overview/live
open-seo-1 | at async requestDataforseo (file:///app/dist/server/assets/index-DrWy7yOr.js:136534:20)
open-seo-1 | at async meterDataforseoCall (file:///app/dist/server/assets/index-DrWy7yOr.js:138809:21)
open-seo-1 | code: 'DATAFORSEO_AUTH_FAILED',
open-seo-1 | provider: 'dataforseo',
open-seo-1 | providerStatus: '401',
A regra prática: depois de colocar a chave, não confie no [ ok ]. Faça uma busca no Domain Overview, que é quem conversa de verdade com a DataForSEO. Se ela voltar com rejected the API key, o valor do .env não é o base64 de login:senha.
E um cuidado a mais, agora que a chave está lá: cada busca nessas telas gasta crédito da sua conta. A auditoria de site continua de graça, porque é o próprio OpenSEO que visita as páginas.
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Perguntas frequentes
O OpenSEO é gratuito?
Por que o OpenSEO não abre pelo IP da VPS?
compose.yaml publica a porta só em 127.0.0.1:3001, porque no modo Docker o OpenSEO roda sem login: quem abrir a tela entra como administrador. O jeito seguro de usar é um túnel SSH (ssh -N -L 3001:127.0.0.1:3001 root@SEU_IP_AQUI) e abrir http://localhost:3001.Qual chave da DataForSEO eu coloco no .env?
DATAFORSEO_API_KEY é o base64 de login:senha da API, e a própria DataForSEO manda esse valor pronto por e-mail, rotulado como Base64. Com o valor errado, a primeira busca responde "DataForSEO rejected the API key".A auditoria deu H1 zero e zero palavras em todas as páginas. O OpenSEO está com defeito?
Coloquei a chave no .env e nada mudou. Por quê?
docker compose restart não relê o .env. Use docker compose up -d --force-recreate open-seo. E espere de 1 a 2 minutos: o contêiner novo refaz o build do app antes de voltar a responder.Quanta memória o OpenSEO usa?
Leia também
changedetection.io: vigiando mudanças em sites na sua VPS
Instale o changedetection.io numa VPS com Docker, vigie um site com filtro CSS, receba o aviso no celular e acabe com o alarme falso do RSS.
Project NOMAD: seu servidor offline de emergência
Instale o Project NOMAD numa VPS Ubuntu e tenha Wikipédia, mapas, cursos e IA funcionando sem nenhuma conexão com a internet.
7 repositórios open source que valem o clone
CRM com agente de IA, SEO sem Semrush, monitor de sites, clonador de páginas, raspagem, OCR e OSINT: sete projetos open source e a armadilha de cada um.