Como instalar um encurtador de URLs com Shlink e Docker
Instale o Shlink 5.1 em um VPS com Docker Compose, domínio curto, Postgres, HTTPS, API, cliente web, QR codes e estatísticas de cliques.
O que você vai criar
Um encurtador de URLs auto-hospedado é um pequeno servidor que transforma um link longo num link curto sob o seu controlo e contabiliza cada clique. A opção recomendada é o Shlink: é open source, é distribuído como uma imagem Docker e executa todo o trabalho com um contentor e uma base de dados. Este guia instala-o num VPS atrás de um domínio curto dedicado, com HTTPS, uma chave de API, códigos QR e estatísticas de cliques.
Duas componentes fazem com que a solução se comporte como um encurtador comercial. O servidor da API responde aos redirecionamentos e armazena os dados. O cliente web é uma aplicação estática separada que comunica com essa API através do navegador. Pode executar ambos ou executar apenas a API e controlá-la pela linha de comandos.
Os números de versão apresentados correspondem às versões atuais em julho de 2026: Shlink 5.1 e shlink-web-client 4.8.
Aponte primeiro um domínio curto para o servidor
O domínio é o endereço público do serviço. s.example.com/abc123 é o link que as pessoas veem, por isso escolha um nome curto antes de instalar qualquer componente. O Shlink armazena o domínio em cada URL curta. Alterá-lo mais tarde faz com que todos os links já distribuídos deixem de funcionar.
Crie um registo DNS A para o domínio curto, apontado para o endereço IPv4 público do seu VPS. Adicione também um registo AAAA se o servidor tiver IPv6. Em seguida, confirme que o domínio é resolvido antes de continuar.
dig +short s.example.com AO resultado deve ser o endereço do seu servidor. Se estiver vazio, o registo ainda não foi propagado. Todas as etapas seguintes vão falhar de forma pouco clara, porque não é possível emitir um certificado TLS (transport layer security) para um nome que não é resolvido.
O ficheiro compose
O Shlink precisa de uma base de dados. O SQLite é suficiente para um teste, mas o Postgres é a escolha certa para qualquer instalação que pretenda manter, porque as linhas de visitas se acumulam e o Postgres lida melhor com os índices e as escritas concorrentes. Coloque este conteúdo em /opt/shlink/compose.yaml.
services:
shlink:
image: shlinkio/shlink:stable
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
DEFAULT_DOMAIN: s.example.com
IS_HTTPS_ENABLED: "true"
DB_DRIVER: postgres
DB_HOST: database
DB_NAME: shlink
DB_USER: shlink
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
- database
database:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: shlink
POSTGRES_USER: shlink
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- shlink_db:/var/lib/postgresql/data
web-client:
image: shlinkio/shlink-web-client:stable
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
volumes:
shlink_db:As duas portas publicadas estão associadas a 127.0.0.1, por isso nada fica acessível a partir da Internet até o reverse proxy da secção seguinte estar configurado. O Docker escreve as suas próprias regras de encaminhamento antes da firewall do host, o que significa que uma linha 8080:8080 simples exporia a aplicação mesmo num sistema cuja firewall pareça fechada. Associar as portas ao endereço de loopback evita esse problema. O mesmo padrão aplica-se a qualquer aplicação executada desta forma e é explicado com mais detalhe em o guia do Docker Compose numa VPS.
A palavra-passe da base de dados vem de um ficheiro .env junto do ficheiro compose, por isso nunca fica gravada no YAML.
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.envInicie os serviços e monitorize o arranque da API.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkO primeiro arranque executa as migrações da base de dados, por isso demora mais do que os arranques seguintes. Quando estabilizar, confirme localmente que o serviço responde.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthUm 200 significa que a API está ativa e que a ligação à base de dados funciona. Um 500 neste ponto é quase sempre causado pela base de dados: o DB_PASSWORD em .env não corresponde ao valor usado quando o Postgres foi criado, porque a imagem do Postgres lê POSTGRES_PASSWORD apenas quando inicializa um diretório de dados vazio. Alterar a palavra-passe posteriormente não produz efeito até remover o volume e iniciar novamente.
Termine o HTTPS à frente do serviço
O Shlink serve HTTP simples na porta 8080. O TLS deve ser terminado num reverse proxy, e a configuração mais importante é transmitir o nome de host original. O Shlink determina a que domínio pertence um código curto lendo o cabeçalho Host. Se o proxy o reescrever, os links existentes devolvem respostas 404 e as estatísticas de visitas ficam associadas ao domínio errado.
server {
server_name s.example.com;
listen 80;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Em seguida, emita o certificado. O passo a passo completo, incluindo o temporizador de renovação, está no guia do Certbot para nginx no Ubuntu 24.04.
sudo certbot --nginx -d s.example.comIS_HTTPS_ENABLED: "true" no ficheiro compose faz o Shlink incluir https:// nos URLs curtos que devolve. Isto não ativa o TLS por si só. Mantenha-o false atrás de um proxy HTTPS. Assim, todos os links devolvidos pela API serão links http:// que depois redirecionam, o que acrescenta uma viagem de ida e volta e parece incorreto no cliente web.
Criar uma chave de API
Nada pode comunicar com a API sem uma chave. Gere uma através da CLI dentro do contentor.
sudo docker compose exec shlink shlink api-key:generate --name "web client"O comando apresenta a chave uma única vez. Copie-a agora, porque é armazenada com hash e não pode ser apresentada novamente. shlink api-key:list apresenta os nomes e indica se cada chave está ativa, mas nunca apresenta a própria chave. Revogue uma chave com shlink api-key:disable e o respetivo nome.
Cada chamada REST envia a chave num cabeçalho X-Api-Key.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsUm objeto JSON com uma chave shortUrls indica que a chave funciona. Um 401 que contenha INVALID_API_KEY indica que a chave está incorreta, desativada ou ultrapassou a data de validade.
Criar links curtos pela linha de comandos
A CLI é a forma mais rápida de criar links e funciona bem com scripts.
sudo docker compose exec shlink shlink short-url:create https://example.com/a/very/long/path
sudo docker compose exec shlink shlink short-url:create https://example.com/docs --custom-slug docs --tag reference--custom-slug fornece um link legível em vez de um código gerado. Os slugs são exclusivos por domínio. Por isso, uma segunda tentativa de usar um slug já ocupado falha, em vez de substituir silenciosamente o primeiro link. --tag pode ser repetida. As tags permitem agrupar links para os quais pretende obter estatísticas combinadas mais tarde.
Liste o que existe e, em seguida, consulte o tráfego de um link.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits mostra uma linha por clique, com a data, o referenciador e o agente do utilizador. As colunas de país e cidade ficam vazias, a menos que defina uma variável de ambiente GEOLITE_LICENSE_KEY. Esta variável contém uma chave gratuita da MaxMind que o Shlink usa para transferir a base de dados GeoLite2. Sem ela, as visitas continuam a ser registadas, mas não são geolocalizadas.
O cliente web e os códigos QR
O cliente web está agora em 127.0.0.1:8081 e precisa da sua própria entrada de proxy, ou de um túnel SSH se preferir não o publicar. Na primeira abertura, pede o URL do servidor e uma chave de API. Introduza https://s.example.com e a chave que gerou. O cliente mantém ambos no armazenamento do navegador e chama diretamente a sua API, portanto os dados não passam por terceiros. Separar a interface da API é um padrão importante, porque é o mesmo que permite ao Halcyon apresentar uma biblioteca Jellyfin como uma loja de aluguer dos anos 1990 sem alterar o servidor de multimédia por trás dela.
Os códigos QR não precisam de qualquer configuração. Anexe /qr-code a qualquer URL curto e a API devolve a imagem.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size é a largura em píxeis e aceita valores de 50 a 1000, com 300 como valor predefinido. format é png ou svg. margin é o espaço livre à volta do código, em píxeis, e a imagem final mede o tamanho mais duas vezes a margem. Adicione errorCorrection=Q para obter um código que continue a ser lido quando for impresso em tamanho reduzido ou ficar parcialmente coberto.
Mantenha o serviço em execução
Um encurtador falha silenciosamente. Os links deixam de redirecionar e ninguém avisa, porque a pessoa que clicou presumiu que o link estava inativo. Aponte uma verificação de disponibilidade para um URL curto real, e não para a página inicial, e gere um alerta para qualquer resposta que não seja um redirecionamento. Uma instância self-hosted do Uptime Kuma faz isso bem e pode verificar um código de estado específico.
Faça backup da base de dados, não do contentor. Um comando cria o dump.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzEsse ficheiro, juntamente com o ficheiro compose, reconstrói o serviço completo num servidor novo. Cada aplicação no servidor precisa da sua própria versão desse par, e uma biblioteca de fotografias é o caso complicado, porque PhotoPrism e Immich mantêm os originais em disco, além das linhas numa base de dados, por isso um dump sozinho não restaura nada. As atualizações são sudo docker compose pull seguidas de sudo docker compose up -d, e o Shlink executa quaisquer migrações novas no arranque. Faça o dump antes de fazer pull, porque uma migração não pode ser revertida.
FAQ
Porque é que os meus links curtos devolvem 404 depois de adicionar um reverse proxy?
O Shlink associa um código curto ao domínio no cabeçalho Host. Um proxy que envia o próprio nome ou um endereço interno faz com que o Shlink procure esse código num domínio sem links e, por isso, devolva 404. Defina proxy_set_header Host $host; no bloco de localização do nginx e recarregue o proxy. Os links começam a funcionar imediatamente, sem reiniciar o contentor.
Preciso de Postgres ou o SQLite é suficiente?
O SQLite é adequado para experimentar o Shlink e não requer um segundo contentor. Mude para Postgres antes de publicar links importantes, porque as linhas de visitas aumentam a cada clique e o SQLite serializa as escritas. Mudar mais tarde implica exportar e importar novamente os links, por isso escolher Postgres desde o início evita essa migração.
Posso recuperar uma chave de API que me esqueci de copiar?
Não. O Shlink armazena um hash da chave, por isso api-key:list mostra os nomes e o estado, mas nunca o valor. Gere uma chave de substituição com shlink api-key:generate, cole-a no cliente web e desative a antiga com shlink api-key:disable para que deixe de funcionar.
Porque é que as colunas de país estão vazias nas minhas estatísticas de visitas?
A geolocalização requer a base de dados GeoLite2, que o Shlink só descarrega quando lhe fornece um GEOLITE_LICENSE_KEY. A chave é gratuita e pode ser obtida junto da MaxMind. Adicione-a à secção de ambiente, recrie o contentor e as novas visitas serão localizadas. As visitas registadas anteriormente permanecem sem dados até executar shlink visit:locate.
Como movo o Shlink para outro servidor?
Mantenha o domínio e mova os dados. Faça um dump da base de dados com pg_dump, copie o dump e o ficheiro compose para o novo servidor, inicie a stack e restaure o dump na base de dados vazia antes de chegar tráfego real. Altere o registo DNS por último. Os códigos curtos e o respetivo histórico de visitas são preservados, porque tudo está na base de dados.