Como hospedar o LinkBreeze com Docker e Caddy
Veja como hospedar o LinkBreeze em um VPS com Docker Compose e Caddy, fixar tags de imagem, rastrear cliques sem cookies e preservar todo o site em um volume.
O que é o LinkBreeze
O LinkBreeze é uma alternativa auto-hospedada ao Linktree: um único contêiner Docker que disponibiliza uma página pública de links na bio e um painel de administração, com todo o estado armazenado num único ficheiro SQLite. É licenciado sob a MIT, escrito em TypeScript com Next.js e publicado como ghcr.io/manak-hash/linkbreeze. Para o executar, precisa de um VPS, de um domínio com um registo A a apontar para esse VPS, das portas 80 e 443 abertas e do Docker Engine com o plugin Compose.
Este guia aborda a implementação efetivamente suportada pelo repositório: Docker Compose atrás de um reverse proxy que obtém os próprios certificados. Também aborda o que pode falhar, porque um link na bio é um URL público em que outras pessoas clicam, e um link indisponível faz perder esse clique.
Antes de tudo isso, tenha em conta que este projeto é muito recente.
O LinkBreeze já é suficientemente maduro para um link de perfil público?
Em agosto de 2026, o repositório tinha 178 stars, 17 forks e um único maintainer. A primeira release marcada, v1.0.0, está datada de 1 July 2026. Este é um projeto com algumas semanas, não com alguns anos.
The data behind this chart
[
{
"week": "2026-06-29",
"releases": 3,
"cumulative": 3
},
{
"week": "2026-07-06",
"releases": 3,
"cumulative": 6
},
{
"week": "2026-07-13",
"releases": 1,
"cumulative": 7
},
{
"week": "2026-07-20",
"releases": 2,
"cumulative": 9
},
{
"week": "2026-07-27",
"releases": 3,
"cumulative": 12
},
{
"week": "2026-08-03",
"releases": 2,
"cumulative": 14
},
{
"week": "2026-08-10",
"releases": 3,
"cumulative": 17
}
]Desde a v1.0.0, o projeto teve 17 releases marcadas distribuídas por 7 semanas de calendário. A última semana desse gráfico ainda estava a decorrer quando este guia foi escrito e já incluía 3 dessas releases.
Considere estes dois factos separadamente. O maintainer está ativo e os bugs são corrigidos em poucos dias. O schema e os valores predefinidos também continuam a mudar, por isso uma instância que seja instalada e esquecida pode afastar-se bastante do código que está a ser desenvolvido.
A licença protege-o do pior cenário. MIT, uma imagem de contentor e um ficheiro SQLite no seu próprio disco significam que, se o desenvolvimento parar, o que tem continua a funcionar. O que a licença não protege é uma aplicação Web pública que deixe de receber correções de segurança e que, com o tempo, se torne um risco. Faça esta instalação pensando em mantê-la atualizada e mantenha a rotina de backup abaixo a funcionar desde o primeiro dia.
Fixe a tag da imagem e não use latest
O fluxo de release envia exatamente duas tags por versão: latest e o número da versão sem o v inicial. Portanto, a tag fixa para a release v1.2.7 é ghcr.io/manak-hash/linkbreeze:1.2.7. Escrever :v1.2.7 não obtém nenhuma imagem, e o Docker apresenta manifest unknown, porque essa tag nunca foi enviada.
Fixe a tag porque latest muda. Com a cadência apresentada no gráfico acima, um docker compose pull de latest é uma atualização não revista de uma página que o seu público está a utilizar. Com uma tag fixa, a atualização ocorre quando edita o ficheiro.
Há mais um aspeto relacionado com a imagem. O fluxo de release compila sem definir platforms:, portanto a imagem publicada é apenas linux/amd64. Num host arm64, o pull falha com no matching manifest for linux/arm64/v8 in the manifest list entries. Se executar um VPS ARM em vez de x86, compile a imagem no próprio servidor:
git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .Depois, use linkbreeze:1.2.7 como nome da imagem no ficheiro compose abaixo.
Implantar o LinkBreeze atrás do Caddy com TLS automático
O Caddy solicita e renova os certificados da Let's Encrypt automaticamente, portanto o TLS (segurança da camada de transporte) não requer uma etapa separada para o certificado. Toda a implantação consiste em três ficheiros num único diretório.
Gere primeiro o segredo:
mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .envSECRET_KEY assina o cookie da sessão de administração e aplica salt ao hash dos visitantes das análises. O ficheiro compose publicado no repositório define-o por defeito como ${SECRET_KEY:-changeme-in-production}, portanto uma instância em que esta etapa seja ignorada fica a funcionar com uma chave de assinatura de sessão publicada no GitHub. Defina-a antes do primeiro arranque, porque alterá-la mais tarde termina a sua sessão e repõe o salt das análises.
Escreva docker-compose.yml:
services:
linkbreeze:
image: ghcr.io/manak-hash/linkbreeze:1.2.7
restart: unless-stopped
volumes:
- linkbreeze-data:/app/data
environment:
- DATABASE_PATH=/app/data/linkbreeze.db
- SECRET_KEY=${SECRET_KEY}
- BASE_URL=https://links.example.com
networks:
- linkbreeze-net
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
networks:
- linkbreeze-net
networks:
linkbreeze-net:
volumes:
linkbreeze-data:
caddy-data:
caddy-config:BASE_URL é opcional, mas é recomendável defini-lo: informa a aplicação do seu endereço público real, para que um pedido recebido com um cabeçalho Host falsificado não faça a aplicação gerar ligações para o domínio de outra pessoa.
Escreva Caddyfile ao lado dele, usando o seu próprio domínio:
links.example.com {
encode zstd gzip
reverse_proxy linkbreeze:3000
}O Caddy define X-Forwarded-For e X-Forwarded-Proto nos pedidos encaminhados por proxy por defeito, e as análises dependem desses valores. Inicie os serviços:
docker compose up -d
docker compose ps
docker compose logs -f caddydocker compose ps deve mostrar o contentor do LinkBreeze como healthy. A imagem inclui a sua própria verificação de estado, wget --spider -q http://127.0.0.1:3000/api/health, portanto não é necessário adicionar outra. Não copie a verificação de estado do exemplo próprio do Caddy no repositório: ela chama curl, mas a imagem é baseada em node:22-alpine, que inclui busybox wget e não inclui curl. Esse contentor comunica unhealthy enquanto continua a servir as páginas corretamente.
Abra https://links.example.com num navegador. A primeira visita abre o assistente de configuração em /setup, que cria a única conta de administração. Depois disso, o painel fica disponível em /dashboard e o formulário de início de sessão em /login. Essa conta é local a esta instância e a aplicação não tem integração com início de sessão único. Se quiser que o painel use o mesmo início de sessão que os restantes serviços alojados, essa integração terá de ser fornecida por um proxy de autenticação à frente da aplicação, como um Authentik autoalojado.
Observe o que o ficheiro compose não faz: nunca publica a porta 3000. Apenas o Caddy escuta na interface pública. Se a sintaxe dos ficheiros Compose for nova para si, as noções básicas do Docker Compose para um VPS explicam as partes assumidas por este ficheiro. Se já tiver outro serviço à frente, Nginx, Caddy e Traefik em comparação explica o que muda. O repositório fornece exemplos funcionais para Nginx com Certbot, Traefik e um túnel Cloudflare.
Onde os seus dados ficam e o que um backup deve conter
DATABASE_PATH aponta para /app/data/linkbreeze.db. Os avatares carregados e as miniaturas dos links são gravados junto dele, em /app/data/uploads. Ambos ficam no volume nomeado linkbreeze-data, por isso a unidade de backup é o volume, não apenas o ficheiro da base de dados. Se restaurar o ficheiro sem o diretório de uploads, todas as imagens da página passam a devolver 404.
Todo o resto está nessa mesma base de dados: páginas, links, definições, tema, subscritores de email e linhas de analytics.
Faça a cópia com o contentor parado:
docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreezePare primeiro, porque copiar uma base de dados SQLite enquanto um processo está a escrever nela pode capturar uma transação incompleta. A cópia pode então abrir como um ficheiro corrompido. A página fica offline enquanto a cópia é feita. A restauração é o mesmo procedimento no sentido inverso:
docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreezeO dashboard também disponibiliza uma exportação JSON, servida a partir de /api/backup como linkbreeze-backup-YYYY-MM-DD.json. Ela inclui o perfil, os links, as definições e os temas guardados. Não inclui o histórico de analytics, os subscritores de email nem as imagens carregadas. Ao restaurá-la, elimina as linhas atuais dessas quatro tabelas antes de inserir as linhas do ficheiro. Trate-a como um snapshot de configuração para mover a aplicação entre hosts ou desfazer um erro de edição. A cópia do volume é o backup.
Aplicam-se aqui as mesmas duas regras de armazenamento que em qualquer outro caso de execução de SQLite em produção num VPS. Mantenha a base de dados num disco local, porque o bloqueio do SQLite não é fiável num sistema de ficheiros de rede e uma página corrompida é a forma de descobrir isso. Se substituir o volume nomeado por um bind mount do host, faça chown ao diretório do host primeiro: o contentor é executado como o utilizador não-root node, com uid 1000 em node:22-alpine, e um diretório criado por root não pode ser escrito por esse utilizador. A aplicação não consegue abrir a base de dados e o contentor termina no arranque. Bind mounts em relação a volumes nomeados no Compose explica essa decisão em detalhe.
As análises e o banner de consentimento de que não precisa
Esta é a funcionalidade que justifica alojar localmente uma página que poderia obter gratuitamente noutro lugar.
As análises não usam cookies. Não é definido nenhum cookie para o visitante e nenhum script de terceiros é carregado na página pública. Um visitante é identificado por um hash SHA-256 do endereço IP, da cadeia do user agent e de um salt, truncado para 16 caracteres hexadecimais. O salt é, por sua vez, um hash da data UTC atual e do seu SECRET_KEY. Por isso, muda à meia-noite UTC e os hashes de ontem não podem ser associados aos de hoje. O endereço IP original nunca é gravado na base de dados.
Os cliques são contabilizados no servidor. Cada link http na página pública aponta para /go/<id> no seu próprio domínio. Esse endpoint regista o clique e responde depois com um redirecionamento 302 para o destino real. A contabilização funciona, portanto, para leitores com JavaScript desativado e nos browsers integrados nas aplicações que bloqueiam pedidos em segundo plano. As visualizações de página são registadas através de /api/track.
Há duas exclusões que deve conhecer. Um pedido que inclua uma sessão de administrador válida é ignorado, para que a edição da sua própria página não aumente os números. Os user agents de crawlers conhecidos também são ignorados.
Quanto ao consentimento: nada é armazenado no dispositivo do leitor, e um cookie armazenado no dispositivo do leitor é precisamente aquilo para que um banner de cookies pede autorização. As suas obrigações continuam a depender do local onde vivem os seus leitores, por isso verifique-as. No entanto, não existe aqui nenhum cookie de rastreamento a divulgar e nenhum terceiro recebe os dados.
Há uma ressalva que surpreende algumas pessoas: rode SECRET_KEY e o salt diário muda com ele. A partir desse momento, cada visitante que regressa é contabilizado como novo.
Por que a coluna de país das análises está vazia?
Porque nada na sua stack define um cabeçalho de país. O LinkBreeze obtém o país a partir de cabeçalhos de proxy como cf-ipcountry e x-vercel-ip-country. Numa VPS atrás do seu próprio Caddy ou Nginx, nenhum desses cabeçalhos existe. Por isso, o país é registado como nulo e a discriminação fica vazia. Não existe uma base de dados GeoIP dentro do contentor.
Há duas formas de preencher a coluna. Coloque o Cloudflare à frente do domínio. Ele adiciona cf-ipcountry a cada pedido que encaminha. Em alternativa, defina um desses cabeçalhos no seu próprio reverse proxy, usando uma consulta GeoIP local.
O problema relacionado é pior, por isso verifique-o. Os handlers de cliques e visualizações leem primeiro o endereço do cliente a partir de X-Forwarded-For, depois de X-Real-IP e usam 0.0.0.0 como fallback quando nenhum dos cabeçalhos está presente. Se publicar a porta 3000 diretamente na Internet, sem um proxy à frente, todos os visitantes serão associados ao mesmo valor de hash. Como resultado, o número de visitantes únicos ficará sempre em 1, e o limite de 60 eventos por minuto por IP será aplicado a todo o seu público ao mesmo tempo. Com a diretiva reverse_proxy acima, o Caddy define o cabeçalho automaticamente e ambos os problemas desaparecem.
Importe do Linktree e o que não é transferido
O assistente de migração no dashboard aceita um URL de perfil público ou um ficheiro exportado. Reconhece páginas do linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me e LittleLink, além de exportações genéricas em HTML e JSON. Para um URL do Linktree ou do Bento, lê o JSON __NEXT_DATA__ incorporado nessas páginas. Numa página estática, lê as tags anchor.
São transferidos o título, o URL, a descrição e a imagem de cada link, a indicação de que o link é um perfil social e o seu nome de apresentação, biografia e avatar. Antes de qualquer gravação na base de dados, pode escolher quais dos links encontrados pretende manter.
Não são transferidos o histórico de analytics, o tema e o layout, os subscritores de email, as datas de publicação agendadas nem qualquer conteúdo que a plataforma antiga mantenha protegido pelo próprio login. Conte com reconstruir o aspeto manualmente e aceite que o histórico de cliques permanece no serviço antigo.
O importador obtém o URL a partir do seu servidor e não do browser. Por isso, recusa endereços que não sejam públicos. Private/local URLs are not allowed significa que forneceu um endereço dentro da sua própria rede. A recusa é deliberada: sem ela, qualquer pessoa com acesso ao dashboard poderia usar o seu servidor para sondar máquinas que apenas o seu servidor consegue alcançar. As outras mensagens que poderá ver são Only http and https URLs are allowed, Request timed out e Response too large.
O scraping depende da marcação de outra pessoa. Se o assistente não encontrar nada numa página que claramente contém links, essa plataforma alterou o HTML desde que o parser foi escrito. Adicione os links manualmente em vez de esperar por uma correção. Se o que pretende são short links mensuráveis, e não uma página de perfil, um encurtador de URLs self-hosted como o Shlink executa essa tarefa e funciona sem problemas no mesmo servidor.
Atualizar uma implantação fixada
# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreezeAs migrações do esquema são executadas automaticamente quando o contentor arranca. Não existe uma forma documentada de as reverter, por isso faça primeiro uma cópia do volume. Uma atualização que não pode ser revertida só é segura quando é possível restaurar o estado anterior.
O dashboard mostra um aviso quando existe uma versão mais recente. Verifica essa condição obtendo um pequeno ficheiro de versão do repositório do projeto no GitHub uma vez a cada 24 horas e não envia informações sobre a sua instância. Leia as notas da versão antes de alterar a tag, porque, nesta fase do projeto, uma versão menor pode alterar predefinições das quais depende.
Modos de falha e as mensagens que verá
manifest unknown ao fazer pull. A tag foi escrita como :v1.2.7. As tags do registry não contêm v, por isso use :1.2.7.
no matching manifest for linux/arm64/v8 in the manifest list entries. A imagem publicada existe apenas para amd64. Faça o build no host ARM a partir do código-fonte identificado pela tag.
O contentor comunica unhealthy, mas a página carrega normalmente. Um healthcheck no seu ficheiro compose está a chamar curl, que não existe na imagem. Remova-o e deixe executar o healthcheck wget fornecido pela própria imagem.
O Caddy apresenta um erro de certificado ou não apresenta conteúdo. Verifique docker compose logs caddy. As causas habituais são um registo A que ainda não aponta para este VPS ou a porta 80 bloqueada na firewall. Isso impede o desafio HTTP do ACME (ambiente de gestão automática de certificados) que o Caddy usa para provar que controla o domínio.
O número de visitantes únicos está bloqueado em 1. Nenhum proxy está a definir X-Forwarded-For, por isso todos os visitantes produzem o mesmo hash.
O contentor termina logo após o arranque, apesar de ter funcionado ontem. Se mudou de um volume nomeado para uma montagem bind do host, o diretório de dados pertence a root e a aplicação é executada com uid 1000. Por isso, não consegue abrir o ficheiro da base de dados. sudo chown -R 1000:1000 o diretório no host.
Os pedidos de tracking respondem com HTTP 429. O limite por IP em /api/track e /go/<id> foi atingido. Os visitantes continuam a ser redirecionados para o destino, mas o clique não é contabilizado.
FAQ
O LinkBreeze está pronto para um link público na bio?
É um projeto recente. Em agosto de 2026, o repositório tinha 178 stars, 17 forks e um único maintainer, e a primeira release é de 1 July 2026. Em média, são publicadas releases mais de duas vezes por semana. Por isso, os bugs são corrigidos rapidamente, mas o comportamento também muda rapidamente. A licença MIT e o ficheiro SQLite local permitem manter uma página funcional mesmo que o desenvolvimento pare. No entanto, uma aplicação web pública sem correções de segurança torna-se um risco. Considere isto um software que continuará a atualizar, e não algo que instala uma vez.
Que tag da imagem do LinkBreeze devo executar?
Execute a tag da versão, por exemplo ghcr.io/manak-hash/linkbreeze:1.2.7, e altere-a deliberadamente. O workflow de release publica apenas latest e o número de versão simples. Por isso, :v1.2.7 com o v não existe, e o Docker responde com manifest unknown. A imagem é criada apenas para linux/amd64. Num VPS arm64, terá de clonar a tag e fazer o build localmente.
Por que motivo a distribuição por país permanece vazia nas analytics do LinkBreeze?
O LinkBreeze lê o país do visitante a partir de cabeçalhos do proxy, como cf-ipcountry ou x-vercel-ip-country. Não inclui uma base de dados GeoIP própria. Um VPS atrás do seu Caddy ou Nginx não define nenhum desses cabeçalhos. Por isso, o país é armazenado como null. Coloque o Cloudflare à frente do domínio ou configure o reverse proxy para definir um desses cabeçalhos a partir de uma consulta GeoIP local.
Do que devo fazer backup e como faço a restauração?
Faça backup de todo o volume linkbreeze-data, não apenas do ficheiro da base de dados. /app/data/linkbreeze.db contém todos os links, páginas, definições, subscritores e registos de analytics. /app/data/uploads contém as imagens de avatar e miniaturas referenciadas pela página. Pare o container, execute docker compose cp linkbreeze:/app/data ./backup-$(date +%F) e inicie-o novamente. Para restaurar, copie o diretório de volta para o container parado e inicie-o. A exportação JSON do dashboard é um snapshot da configuração do perfil, dos links, das definições e dos temas. Não contém analytics nem imagens.
A importação do Linktree também traz as minhas analytics e o meu tema?
Não. O assistente de migração lê os títulos, URLs, descrições e imagens dos links do seu perfil público antigo. Também lê o nome apresentado, a bio e o avatar. O histórico de analytics, o tema, os subscritores de email e as datas de publicação agendada não são importados. Recrie o visual no editor de temas depois da importação. O histórico de cliques continuará na plataforma antiga.