SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-15

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.

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.

ChartLinkBreeze tagged releases per week, v1.0.0 to v1.2.7
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 .env

SECRET_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 caddy

docker 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 linkbreeze

Pare 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 linkbreeze

O 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 linkbreeze

As 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

É 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.

#linkbreeze#linktree-alternative#docker-compose#sqlite#self-hosting#analytics