Acortador de URL autohospedado con Shlink y Docker
Instala Shlink 5.1 en un VPS con Docker Compose: configura DNS y HTTPS, Postgres, API keys, cliente web, códigos QR y estadísticas de clics.
Qué vas a crear
Un acortador de URL autohospedado es un servidor pequeño que convierte un enlace largo en uno corto que controlas y cuenta cada clic que recibe. Shlink es la opción recomendada: es de código abierto, se distribuye como una imagen de Docker y realiza todo el trabajo en un contenedor y una base de datos. Esta guía lo instala en un VPS detrás de un dominio corto real, con HTTPS, una clave de API, códigos QR y estadísticas de clics.
Dos componentes hacen que funcione como un acortador comercial. El servidor de API responde a las redirecciones y almacena los datos. El cliente web es una aplicación estática independiente que se comunica con esa API desde el navegador. Puedes ejecutar ambos o ejecutar solo la API y controlarla desde la línea de comandos.
Los números de versión indicados aquí eran los actuales en julio de 2026: Shlink 5.1 y shlink-web-client 4.8.
Primero, apunte un dominio corto al servidor
El dominio es el producto. s.example.com/abc123 es el enlace que ven las personas, así que elija un nombre corto y defínalo antes de instalar cualquier componente. Shlink almacena el dominio con cada URL corta, y cambiarlo después hace que todos los enlaces que ya haya distribuido dejen de funcionar.
Cree un registro DNS A para el dominio corto y apúntelo a la dirección IPv4 pública de su VPS. Añada también un registro AAAA si el servidor tiene IPv6. Después, confirme que se resuelve antes de continuar.
dig +short s.example.com ALa salida debe ser la dirección de su servidor. Si está vacía, el registro todavía no se ha propagado y todos los pasos posteriores fallarán de forma confusa, porque no se puede emitir un certificado TLS (seguridad de la capa de transporte) para un nombre que no se resuelve.
El archivo de Compose
Shlink necesita una base de datos. SQLite sirve para una prueba, pero Postgres es la opción adecuada para cualquier instalación que vaya a conservar datos, porque las filas de visitas se acumulan y Postgres gestiona mejor los índices y las escrituras simultáneas. Guarde esto en /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:Ambos puertos publicados se enlazan a 127.0.0.1, por lo que nada queda accesible desde Internet hasta que el proxy inverso de la sección siguiente esté configurado. Docker escribe sus propias reglas de redirección antes que el firewall del host, por lo que una línea 8080:8080 simple expondría la aplicación incluso en un equipo cuyo firewall pareciera estar cerrado. Enlazar con la dirección de loopback evita ese problema. El mismo patrón se aplica a cualquier aplicación que ejecute de esta forma y se explica con más detalle en la guía de Docker Compose en un VPS.
La contraseña de la base de datos procede de un archivo .env junto al archivo de Compose, por lo que nunca se incluye en el archivo 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/.envInícielo y supervise hasta que la API esté disponible.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkEl primer inicio ejecuta las migraciones de la base de datos, por lo que tarda más que los siguientes. Cuando se estabilice, compruebe que el servicio responde localmente.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthUn 200 indica que la API está activa y que la conexión con la base de datos funciona. Un 500 aquí casi siempre indica un problema con la base de datos: el DB_PASSWORD de .env no coincide con el valor con el que se creó Postgres, porque la imagen de Postgres solo lee POSTGRES_PASSWORD cuando inicializa un directorio de datos vacío. Cambiar la contraseña después no tiene efecto hasta que elimine el volumen y vuelva a iniciar el servicio.
Termina TLS delante de Shlink
Shlink sirve HTTP sin cifrar en el puerto 8080. TLS debe configurarse en un proxy inverso. La configuración importante es reenviar el nombre de host original. Shlink determina a qué dominio pertenece un código corto mediante la cabecera Host. Si el proxy la reescribe, los enlaces existentes devuelven respuestas 404 y las estadísticas de visitas se asignan al dominio incorrecto.
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;
}
}A continuación, emite el certificado. El procedimiento completo, incluido el temporizador de renovación, está en la guía de Certbot para nginx en Ubuntu 24.04.
sudo certbot --nginx -d s.example.comIS_HTTPS_ENABLED: "true" en el archivo compose hace que Shlink incluya https:// en las URL cortas que devuelve. No habilita TLS por sí solo. Déjalo como false detrás de un proxy HTTPS. De este modo, cada enlace que devuelve la API es un enlace http:// que después redirige. Esto añade un salto de red y se ve incorrecto en el cliente web.
Crear una clave de API
Nada puede comunicarse con la API sin una clave. Genere una mediante la CLI dentro del contenedor.
sudo docker compose exec shlink shlink api-key:generate --name "web client"El comando muestra la clave una sola vez. Cópiela ahora, porque se almacena con hash y no se puede volver a mostrar. shlink api-key:list muestra los nombres y si cada clave está habilitada, pero nunca la clave en sí. Revoque una con shlink api-key:disable y el nombre.
Cada llamada REST incluye la clave en un encabezado X-Api-Key.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsUn objeto JSON con una clave shortUrls indica que la clave funciona. Un 401 que contiene INVALID_API_KEY indica que la clave es incorrecta, está deshabilitada o ha caducado.
Cree enlaces cortos desde la línea de comandos
La CLI es la forma más rápida de crear enlaces y permite automatizar mejor las tareas mediante 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 proporciona un enlace legible en lugar de un código generado. Los slugs son únicos por dominio, por lo que un segundo intento con un slug ya utilizado falla en lugar de sobrescribir silenciosamente el primer enlace. --tag se puede repetir, y las etiquetas permiten agrupar los enlaces de los que más adelante querrá obtener estadísticas combinadas.
Enumere los elementos existentes y, después, consulte el tráfico de un enlace.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits muestra una fila por clic con la fecha, el referente y el agente de usuario. Las columnas de país y ciudad quedan vacías a menos que establezca una variable de entorno GEOLITE_LICENSE_KEY, que corresponde a una clave gratuita de MaxMind que Shlink utiliza para descargar la base de datos GeoLite2. Sin esta variable, las visitas se siguen registrando, pero no se geolocalizan.
El cliente web y los códigos QR
El cliente web está ahora en 127.0.0.1:8081 y necesita su propia entrada de proxy, o un túnel SSH si prefiere no publicarlo. En la primera carga, solicita la URL del servidor y una clave de API. Introduzca https://s.example.com y la clave que generó. El cliente guarda ambos datos en el almacenamiento del navegador y llama directamente a su API, por lo que ningún dato pasa por terceros.
Los códigos QR no requieren ninguna configuración. Añada /qr-code a cualquier URL corta y la API devuelve la imagen.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size es el ancho en píxeles y acepta valores de 50 a 1000, con 300 como valor predeterminado. format es png o svg. margin es el espacio de separación alrededor del código, en píxeles, y la imagen final mide el tamaño más el doble del margen. Añada errorCorrection=Q para que el código se pueda escanear aunque se imprima pequeño o esté parcialmente cubierto.
Mantenerlo en ejecución
Un acortador puede fallar sin avisar. Los enlaces dejan de redirigir y nadie se lo comunica, porque la persona que hizo clic asumió que el enlace estaba inactivo. Configure una comprobación de disponibilidad sobre una URL corta real, no sobre la página de inicio, y genere una alerta ante cualquier respuesta que no sea una redirección. Una instancia de Uptime Kuma autohospedada funciona bien para esto y puede comprobar un código de estado específico.
Haga una copia de seguridad de la base de datos, no del contenedor. Un comando la vuelca.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzEse archivo y el archivo de compose permiten reconstruir todo el servicio en un servidor nuevo. Las actualizaciones consisten en sudo docker compose pull y después sudo docker compose up -d, y Shlink ejecuta las migraciones nuevas al iniciarse. Cree el volcado antes de hacer pull, porque una migración no se puede revertir.
FAQ
¿Por qué mis enlaces cortos devuelven 404 después de añadir un proxy inverso?
Shlink compara el código corto con el dominio del encabezado Host. Si el proxy envía su propio nombre o una dirección interna, Shlink busca ese código en un dominio que no tiene enlaces y responde con 404. Establezca proxy_set_header Host $host; en el bloque de ubicación de nginx y recargue el proxy. Los enlaces empiezan a funcionar de inmediato, sin reiniciar el contenedor.
¿Necesito Postgres o SQLite es suficiente?
SQLite es adecuado para probar Shlink y no requiere un segundo contenedor. Cambie a Postgres antes de publicar enlaces importantes, porque las filas de visitas aumentan con cada clic y SQLite serializa las escrituras. Cambiar más adelante requiere exportar y volver a importar los enlaces, por lo que elegir Postgres desde el principio evita esa migración.
¿Puedo recuperar una clave de API que olvidé copiar?
No. Shlink almacena un hash de la clave, por lo que api-key:list muestra los nombres y el estado, pero nunca el valor. Genere un reemplazo con shlink api-key:generate, péguelo en el cliente web y desactive la clave antigua con shlink api-key:disable para que deje de funcionar.
¿Por qué las columnas de país están vacías en las estadísticas de visitas?
La geolocalización necesita la base de datos GeoLite2, que Shlink solo descarga cuando se proporciona un GEOLITE_LICENSE_KEY. MaxMind proporciona la clave de forma gratuita. Añádala a la sección de entorno, vuelva a crear el contenedor y las nuevas visitas se geolocalizarán. Las visitas registradas antes de ese cambio seguirán sin datos hasta que ejecute shlink visit:locate.
¿Cómo traslado Shlink a otro servidor?
Conserve el dominio y traslade los datos. Haga un volcado de la base de datos con pg_dump, copie el volcado y el archivo compose al nuevo servidor, inicie el stack y restaure el volcado en la base de datos vacía antes de que llegue tráfico real. Cambie el registro DNS al final. Los códigos cortos y su historial de visitas se conservan porque todo está en la base de datos.