Como criar um encurtador de URLs com Shlink e Docker
Instale um encurtador de URLs próprio no VPS com Shlink 5.1 e Docker Compose, usando domínio curto, Postgres, API, cliente web, QR codes e estatísticas.
O que você vai criar
Um encurtador de URLs auto-hospedado é um servidor pequeno que transforma um link longo em um link curto que você controla e conta cada clique nele. Shlink é a opção recomendada: é de código aberto, está disponível como uma imagem Docker e executa todo o trabalho em um container e um banco de dados. Este guia instala o serviço em um VPS atrás de um domínio curto real, com HTTPS, uma chave de API, códigos QR e estatísticas de cliques.
Duas partes fazem o serviço funcionar como um encurtador comercial. O servidor da API responde aos redirecionamentos e armazena os dados. O cliente web é um aplicativo estático separado que se comunica com essa API pelo navegador. Você pode executar os dois ou executar apenas a API e controlá-la pela linha de comando.
Os números de versão deste guia eram os 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 produto. s.example.com/abc123 é o link que as pessoas veem, então escolha algo curto e defina-o antes de instalar qualquer coisa. O Shlink armazena o domínio em cada URL curta, e alterá-lo depois fará com que todos os links já distribuídos parem de funcionar.
Crie um registro DNS A para o domínio curto, apontando para o endereço IPv4 público do seu VPS. Adicione também um registro AAAA se o servidor tiver IPv6. Em seguida, confirme se ele é resolvido antes de continuar.
dig +short s.example.com AA saída deve ser o endereço do seu servidor. Se estiver vazia, o registro ainda não foi propagado, e todas as etapas seguintes falharão de forma confusa, porque não é possível emitir um certificado TLS (transport layer security) para um nome que não é resolvido.
O arquivo do Compose
O Shlink precisa de um banco de dados. O SQLite funciona para testes, mas o Postgres é a escolha certa para qualquer instalação que você pretende manter, porque as linhas de visitas se acumulam e o Postgres lida melhor com os índices e com as gravações simultâneas. Coloque isto 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 fazem bind em 127.0.0.1, portanto nada fica acessível pela Internet até que o proxy reverso da próxima seção esteja configurado. O Docker grava suas próprias regras de encaminhamento antes do firewall do host. Isso significa que uma linha 8080:8080 simples exporia o aplicativo mesmo em um sistema cujo firewall pareça fechado. Fazer bind no endereço de loopback evita isso. O mesmo padrão se aplica a qualquer aplicativo executado dessa forma e é explicado em mais detalhes no guia do Docker Compose em um VPS.
A senha do banco de dados vem de um arquivo .env ao lado do arquivo Compose, portanto nunca é 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-o e monitore a inicialização da API.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkA primeira inicialização executa as migrações do banco de dados, portanto demora mais que as seguintes. Quando a inicialização terminar, verifique se o serviço responde localmente.
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 conexão com o banco de dados funciona. Um 500 aqui quase sempre indica um problema no banco de dados: o DB_PASSWORD em .env não corresponde ao valor usado na criação do Postgres, porque a imagem do Postgres lê POSTGRES_PASSWORD somente quando inicializa um diretório de dados vazio. Alterar a senha depois não tem efeito até que você remova o volume e inicie novamente.
Termine o HTTPS na frente dele
O Shlink disponibiliza HTTP simples na porta 8080. O TLS deve ser configurado em um proxy reverso. A configuração essencial é encaminhar o nome do host original. O Shlink determina a qual domínio um código curto pertence lendo o cabeçalho Host. Portanto, um proxy que o reescreve gera respostas 404 para links existentes e associa as estatísticas de acesso 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 arquivo compose faz o Shlink imprimir https:// nos URLs curtos que retorna. Isso não ativa o TLS por si só. Deixe false atrás de um proxy HTTPS. Assim, todo link que a API retornar será um link http:// que depois redirecionará, consumindo uma viagem de ida e volta e parecendo incorreto no cliente web.
Criar uma chave de API
Nada pode acessar a API sem uma chave. Gere uma usando a CLI dentro do contêiner.
sudo docker compose exec shlink shlink api-key:generate --name "web client"O comando exibe a chave uma única vez. Copie-a agora, porque ela é armazenada com hash e não pode ser exibida novamente. shlink api-key:list mostra os nomes e se cada chave está habilitada, mas nunca a própria chave. Revogue uma chave com shlink api-key:disable e o nome.
Cada chamada REST envia a chave em um 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 significa que a chave funciona. Um 401 contendo INVALID_API_KEY significa que a chave está incorreta, desabilitada ou expirou.
Criar links curtos pela linha de comando
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á existente falha, em vez de substituir silenciosamente o primeiro link. --tag pode ser repetido. As tags permitem agrupar links para os quais você desejará obter estatísticas combinadas posteriormente.
Liste o que existe e examine 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 exibe uma linha por clique, com a data, o referenciador e o user agent. As colunas de país e cidade permanecem vazias, a menos que você defina uma variável de ambiente GEOLITE_LICENSE_KEY. Essa variável contém uma chave gratuita da MaxMind, usada pelo Shlink para baixar o banco de dados GeoLite2. Sem ela, as visitas continuam sendo registradas, mas não são geolocalizadas.
O cliente web e os códigos QR
O cliente web está disponível em 127.0.0.1:8081 e precisa de sua própria entrada de proxy, ou de um túnel SSH se você preferir não publicá-lo. Na primeira inicialização, ele solicita a URL do servidor e uma chave de API. Informe https://s.example.com e a chave que você gerou. O cliente mantém ambos no armazenamento do navegador e chama sua API diretamente, portanto nenhum dado passa por terceiros.
Os códigos QR não exigem nenhuma configuração. Anexe /qr-code a qualquer URL curta, e a API retornará a imagem.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size é a largura em pixels e aceita valores de 50 a 1000, com 300 como padrão. format é png ou svg. margin é o espaço livre ao redor do código, em pixels, e a imagem final mede o tamanho mais o dobro da margem. Adicione errorCorrection=Q para obter um código que continue sendo lido quando for impresso em tamanho pequeno ou estiver 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. Configure uma verificação de disponibilidade para uma URL curta real, em vez da página inicial, e gere um alerta para qualquer resposta que não seja um redirecionamento. Uma instância autogerenciada do Uptime Kuma faz isso bem e pode verificar um código de status específico.
Faça backup do banco de dados, não do container. Um comando faz o dump.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzEsse arquivo e o arquivo compose reconstroem todo o serviço em um novo servidor. As atualizações são sudo docker compose pull seguidas de sudo docker compose up -d, e o Shlink executa todas as novas migrações na inicialização. Faça o dump antes de executar o pull, porque não é possível reverter uma migração.
FAQ
Por que meus links curtos retornam 404 depois que adiciono um proxy reverso?
O Shlink compara um código curto com o domínio no cabeçalho Host. Um proxy que envia o próprio nome ou um endereço interno faz o Shlink procurar esse código em um domínio que não tem links. Por isso, ele retorna 404. Defina proxy_set_header Host $host; no bloco de location do nginx e recarregue o proxy. Os links voltam a funcionar imediatamente, sem reiniciar o container.
Preciso do Postgres ou o SQLite é suficiente?
O SQLite é adequado para testar o Shlink e não exige um segundo container. Migre para o Postgres antes de publicar links importantes, porque as linhas de visitas aumentam a cada clique e o SQLite serializa as gravações. Se você fizer a migração depois, precisará exportar e importar os links. Escolher o Postgres desde o início evita essa migração.
Posso recuperar uma chave de API que esqueci de copiar?
Não. O Shlink armazena um hash da chave. Por isso, api-key:list mostra os nomes e o status, mas nunca o valor. Gere uma substituta com shlink api-key:generate, cole-a no cliente web e desative a antiga com shlink api-key:disable para impedir seu uso.
Por que as colunas de país estão vazias nas minhas estatísticas de visitas?
A geolocalização precisa do banco de dados GeoLite2. O Shlink só o baixa quando você fornece um GEOLITE_LICENSE_KEY. A chave é gratuita e pode ser obtida na MaxMind. Adicione-a à seção de ambiente, recrie o container e as novas visitas serão geolocalizadas. As visitas registradas antes disso continuarão sem dados até que você execute shlink visit:locate.
Como movo o Shlink para outro servidor?
Mantenha o domínio e mova os dados. Faça o dump do banco de dados com pg_dump, copie o dump e o arquivo compose para o novo servidor, inicie a stack e restaure o dump no banco de dados vazio antes que chegue tráfego real. Altere o registro DNS por último. Os códigos curtos e o histórico de visitas serão preservados, porque tudo fica armazenado no banco de dados.