Acortador de URL autoalojado con Shlink y Docker
Instale Shlink 5.1 y su cliente web 4.8 en un VPS con Docker Compose: dominio corto, HTTPS, Postgres, API, códigos QR y estadísticas de clics.
Qué va a configurar
Un acortador de URL autoalojado es un servidor pequeño que convierte un enlace largo en uno corto que usted controla y registra cada clic. Shlink es una buena opción: es de código abierto, se distribuye como 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. Puede ejecutar ambos o usar sólo la API y controlarla desde la línea de comandos.
Los números de versión indicados eran los vigentes en julio de 2026: Shlink 5.1 y shlink-web-client 4.8.
Apunte primero un dominio corto al servidor
El dominio es el producto. s.example.com/abc123 es el enlace que verán los usuarios, así que elija un nombre corto y defínalo antes de instalar nada. Shlink guarda el dominio con cada URL corta. Si lo cambia más adelante, todos los enlaces que ya haya distribuido dejarán 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 el dominio 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. Todos los pasos posteriores fallarán de forma confusa, porque no se puede emitir un certificado TLS (transport layer security) para un nombre que no se resuelve.
El archivo 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, porque las filas de visitas se acumulan y Postgres gestiona mejor los índices y las escrituras simultáneas. Guarde lo siguiente 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 es accesible desde Internet hasta que el reverse proxy de la sección siguiente esté configurado. Docker escribe sus propias reglas de reenvío antes que el firewall del host, de modo que una línea 8080:8080 sin más expondría la aplicación incluso en un equipo cuyo firewall pareciera estar cerrado. Enlazar con la dirección de loopback evita este 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 situado junto al archivo compose, por lo que nunca se incluye en el 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 cómo se inicia la API.
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 significa 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: DB_PASSWORD en .env no coincide con el valor con el que se creó Postgres, porque la imagen de Postgres sólo 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.
Terminar HTTPS delante de Shlink
Shlink ofrece HTTP sin cifrado en el puerto 8080. TLS debe configurarse en un reverse proxy. La única configuración importante es reenviar el nombre de host original. Shlink determina a qué dominio pertenece un código corto leyendo la cabecera Host. Por tanto, un proxy que la reescriba devuelve respuestas 404 para enlaces existentes y asigna las estadísticas de visitas 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, emita el certificado. La guía completa, 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 de composición hace que Shlink incluya https:// en las URL cortas que devuelve. No habilita TLS por sí solo. Déjelo false detrás de un proxy HTTPS y cada enlace que devuelva la API será un enlace http:// que después redirige. Esto añade un salto de red y se muestra de forma incorrecta 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 una cabecera 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.
Crear enlaces cortos desde la línea de comandos
La CLI es la forma más rápida de crear enlaces y funciona bien con 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 permite usar 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 para los que quiera obtener estadísticas combinadas más adelante.
Enumere los elementos existentes y revise 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 del país y la ciudad quedan vacías a menos que defina una variable de entorno GEOLITE_LICENSE_KEY. Esta variable corresponde a una clave gratuita de MaxMind que Shlink usa para descargar la base de datos GeoLite2. Sin ella, 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 del 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 valores en el almacenamiento del navegador y llama directamente a su API, por lo que ningún dato pasa por terceros. Separar la interfaz de la API es un patrón importante, porque es el mismo que permite que Halcyon presente una biblioteca de Jellyfin como un videoclub de 1990 sin modificar el servidor multimedia que hay detrás.
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 admite valores de 50 a 1000, con 300 como valor predeterminado. format es png o svg. margin es el espacio libre alrededor del código, medido en píxeles, y la imagen final mide el tamaño más el doble del margen. Añada errorCorrection=Q para obtener un código que siga siendo legible al escanearlo cuando se imprima en pequeño o quede parcialmente cubierto.
Manténgalo en ejecución
Un acortador puede fallar sin mostrar errores. Los enlaces dejan de redirigir y nadie se lo comunica, porque la persona que hizo clic supone que el enlace está roto. 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 compose permiten reconstruir todo el servicio en un servidor nuevo. Cada aplicación del servidor necesita su propia versión de ese par. Una biblioteca de fotos es el caso problemático, porque PhotoPrism e Immich guardan los originales en el disco y también registros en una base de datos, por lo que un volcado por sí solo no restaura nada. Las actualizaciones se realizan mediante sudo docker compose pull y después sudo docker compose up -d, y Shlink ejecuta las migraciones nuevas al iniciar. Haga el volcado antes de actualizar, porque una migración no se puede revertir.
FAQ
¿Por qué mis enlaces cortos devuelven 404 después de añadir un reverse proxy?
Shlink compara un código corto con el dominio de la cabecera 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 devuelve 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 basta con SQLite?
SQLite es suficiente 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 implica 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é están vacías las columnas de país en mis estadísticas de visitas?
La geolocalización necesita la base de datos GeoLite2, que Shlink sólo descarga cuando se le proporciona un GEOLITE_LICENSE_KEY. MaxMind ofrece 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 seguirán sin datos hasta que ejecute shlink visit:locate.
¿Cómo traslado Shlink a otro servidor?
Conserve el dominio y traslade los datos. Vuelque 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 recibir tráfico real. Cambie el registro DNS al final. Los códigos cortos y su historial de visitas se conservan porque todo está almacenado en la base de datos.