SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-15

Cómo autoalojar LinkBreeze con Docker Compose

Despliega LinkBreeze en un VPS con Docker Compose y Caddy. Usa tags de imagen fijados, tracking sin cookies y un único volumen para todo el sitio.

Qué es LinkBreeze

LinkBreeze es una alternativa autoalojada a Linktree: un contenedor Docker que sirve una página pública de enlaces en la biografía y un panel de administración, con todo el estado almacenado en un único archivo SQLite. Se distribuye con licencia MIT, está escrito en TypeScript sobre Next.js y se publica como ghcr.io/manak-hash/linkbreeze. Para ejecutarlo necesita un VPS, un dominio con un registro A que apunte a ese VPS, los puertos 80 y 443 abiertos y Docker Engine con el plugin Compose.

Esta guía cubre el despliegue que el repositorio admite realmente: Docker Compose detrás de un reverse proxy que obtiene sus propios certificados. También explica qué puede fallar, porque un enlace en una biografía es una URL pública en la que otras personas hacen clic, y si deja de funcionar perderá ese clic.

Antes de todo, tenga claro lo reciente que es este proyecto.

¿Es LinkBreeze lo bastante maduro para un enlace de perfil público?

En agosto de 2026, el repositorio tiene 178 estrellas, 17 forks y un único mantenedor. La primera versión etiquetada, v1.0.0, está fechada el 1 de julio de 2026. Es un proyecto con unas pocas semanas de antigüedad, no con unos pocos años.

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 v1.0.0, el proyecto ha publicado 17 versiones etiquetadas en 7 semanas naturales. La última semana de ese gráfico aún estaba en curso cuando se redactó esta guía y ya incluía 3 de ellas.

Interprételo como dos hechos independientes. El mantenedor está activo y los errores se corrigen en cuestión de días. El esquema y los valores predeterminados también siguen cambiando, por lo que una instancia que despliegue y deje sin supervisión puede alejarse mucho del código que se está desarrollando.

La licencia le protege frente al peor caso. La licencia MIT, una imagen de contenedor y un archivo SQLite en su propio disco significan que, si el desarrollo se detiene, lo que ya tiene seguirá funcionando. Lo que no le protege es una aplicación web pública que deje de recibir correcciones de seguridad y se convierta en una responsabilidad con el tiempo. Despliegue esto como un servicio que seguirá actualizando y mantenga operativa desde el primer día la rutina de copias de seguridad que se describe a continuación.

Fije la etiqueta de la imagen y no use latest

El flujo de publicación inserta exactamente 2 etiquetas por versión: latest y el número de versión sin el prefijo v. Por tanto, la etiqueta fijada para la versión v1.2.7 es ghcr.io/manak-hash/linkbreeze:1.2.7. Escribir :v1.2.7 no descarga ninguna imagen y Docker informa manifest unknown, porque esa etiqueta nunca se publicó.

Fije la etiqueta porque latest cambia. Con la frecuencia mostrada en el gráfico anterior, un docker compose pull contra latest actualiza sin revisión una página que está utilizando su audiencia. Con una etiqueta fijada, la actualización se produce cuando edita el archivo.

Hay otro aspecto relacionado con la imagen. El flujo de publicación compila sin establecer platforms:, por lo que la imagen publicada sólo es linux/amd64. En un host arm64, la descarga falla con no matching manifest for linux/arm64/v8 in the manifest list entries. Si ejecuta un VPS ARM en lugar de x86, compile la imagen en el propio 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 .

Después, use linkbreeze:1.2.7 como nombre de la imagen en el archivo compose siguiente.

Implementar LinkBreeze detrás de Caddy con TLS automático

Caddy solicita y renueva los certificados de Let's Encrypt por su cuenta, por lo que TLS (seguridad de la capa de transporte) no requiere un paso independiente para el certificado. Toda la implementación consta de tres archivos en un mismo directorio.

Genere primero el secreto:

mkdir -p ~/linkbreeze && cd ~/linkbreeze
printf 'SECRET_KEY=%s\n' "$(openssl rand -hex 32)" > .env
chmod 600 .env

SECRET_KEY firma la cookie de sesión del administrador y añade salt a la huella de los visitantes de analytics. El archivo Compose publicado en el repositorio lo establece de forma predeterminada en ${SECRET_KEY:-changeme-in-production}. Por tanto, una instancia en la que omita este paso se ejecuta con una clave de firma de sesión publicada en GitHub. Establézcalo antes del primer arranque, porque cambiarlo después cerrará su sesión y restablecerá el salt de analytics.

Escriba 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 es opcional, pero conviene establecerlo. Indica a la aplicación su dirección pública real, de modo que una petición con una cabecera Host falsificada no pueda hacer que la aplicación genere enlaces al dominio de otra persona.

Escriba Caddyfile junto a él, con su propio dominio:

links.example.com {
    encode zstd gzip
    reverse_proxy linkbreeze:3000
}

Caddy establece X-Forwarded-For y X-Forwarded-Proto de forma predeterminada en las peticiones reenviadas, y analytics depende de ellas. Inicie los contenedores:

docker compose up -d
docker compose ps
docker compose logs -f caddy

docker compose ps debería mostrar el contenedor de LinkBreeze como healthy. La imagen incluye su propia comprobación de estado, wget --spider -q http://127.0.0.1:3000/api/health, por lo que no necesita añadir otra. No copie la comprobación de estado del ejemplo de Caddy del propio repositorio: ejecuta curl, y la imagen está basada en node:22-alpine, que incluye busybox wget y no incluye curl. Ese contenedor informa unhealthy aunque sirva las páginas correctamente.

Abra https://links.example.com en un navegador. La primera visita muestra el asistente de configuración en /setup, que crea la única cuenta de administrador. Después, el panel está en /dashboard y el formulario de inicio de sesión en /login. Esa cuenta es local a esta instancia y la aplicación no tiene integración con single sign-on. Si quiere que el panel use el mismo inicio de sesión que el resto de servicios que aloja, debe colocar delante un proxy de forward auth, como Authentik autohospedado.

Observe lo que no hace el archivo Compose: nunca publica el puerto 3000. Sólo Caddy escucha en la interfaz pública. Si la sintaxis de los archivos Compose es nueva para usted, los conceptos básicos de Docker Compose para un VPS cubren las partes que presupone este archivo. Si ya ejecuta otro servicio delante, comparativa entre Nginx, Caddy y Traefik explica qué cambia. El repositorio incluye ejemplos funcionales para Nginx con Certbot, Traefik y un túnel de Cloudflare.

Dónde se almacenan los datos y qué debe contener una copia de seguridad

DATABASE_PATH apunta a /app/data/linkbreeze.db. Los avatares subidos y las miniaturas de los enlaces se escriben junto a él, en /app/data/uploads. Ambos están en el volumen con nombre linkbreeze-data, por lo que la unidad de copia de seguridad es el volumen, no el archivo de base de datos por separado. Si restaura el archivo sin el directorio de cargas, todas las imágenes de la página devuelven un error 404.

Todo lo demás está realmente en esa única base de datos: páginas, enlaces, configuración, tema, suscriptores de correo electrónico y filas de analíticas.

Detenga el contenedor antes de realizar la copia:

docker compose stop linkbreeze
docker compose cp linkbreeze:/app/data ./backup-$(date +%F)
docker compose start linkbreeze

Debe detenerlo primero porque copiar una base de datos SQLite mientras un proceso escribe en ella puede capturar una transacción incompleta. La copia se abriría entonces como un archivo dañado. La página permanece fuera de servicio mientras se realiza la copia. La restauración consiste en hacer lo mismo en 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

El panel también ofrece una exportación JSON, disponible en /api/backup como linkbreeze-backup-YYYY-MM-DD.json. Incluye el perfil, los enlaces, la configuración y los temas guardados. No incluye el historial de analíticas, los suscriptores de correo electrónico ni las imágenes subidas. Además, al restaurarla se eliminan las filas actuales de esas cuatro tablas antes de insertar las del archivo. Trátela como una instantánea de configuración para migrar entre hosts o deshacer un error de edición. La copia del volumen es la copia de seguridad.

Aquí se aplican las mismas dos reglas de almacenamiento que en cualquier otro caso de ejecutar SQLite en producción en un VPS. Mantenga la base de datos en el disco local, porque el bloqueo de SQLite no es fiable en un sistema de archivos de red y una página dañada es la forma de descubrirlo. Si sustituye el volumen con nombre por un montaje de enlace del host, ejecute primero chown sobre el directorio del host: el contenedor se ejecuta como el usuario no root node, con uid 1000 en node:22-alpine, y un directorio creado por root no permite la escritura de ese usuario. Por eso, la aplicación no puede abrir la base de datos y el contenedor termina durante el arranque. Montajes de enlace frente a volúmenes con nombre en Compose explica completamente esta diferencia.

La analítica y el banner de consentimiento que no necesita

Esta es la función que justifica alojar por cuenta propia una página que podría obtener gratis en otro sitio.

La analítica no usa cookies. No se establece ninguna cookie para el visitante y no se carga ningún script de terceros en la página pública. El visitante se identifica mediante un hash SHA-256 de la dirección IP, la cadena del agente de usuario y un salt, truncado a 16 caracteres hexadecimales. El salt es, a su vez, un hash de la fecha UTC actual y de su SECRET_KEY, por lo que cambia a medianoche UTC y los hash del día anterior no se pueden comparar con los del día actual. La dirección IP sin procesar nunca se escribe en la base de datos.

Los clics se cuentan en el servidor. Cada enlace http de la página pública apunta a /go/<id> en su propio dominio. Este registra el clic y después responde con una redirección 302 al destino real. Por tanto, el recuento funciona para los lectores que tienen JavaScript desactivado y en los navegadores integrados en aplicaciones que bloquean las solicitudes en segundo plano. Las vistas de página se registran mediante /api/track.

Conviene conocer dos exclusiones. Las solicitudes que incluyen una sesión de administrador válida se omiten, por lo que editar su propia página no incrementa las cifras. También se omiten los agentes de usuario de rastreadores conocidos.

En cuanto al consentimiento: no se almacena nada en el dispositivo del lector, y una cookie almacenada en el dispositivo del lector es precisamente aquello para lo que un banner de cookies solicita permiso. Sus obligaciones dependen igualmente del lugar donde residan sus lectores, así que debe comprobarlas. Sin embargo, aquí no hay ninguna cookie de seguimiento que declarar ni ningún tercero que reciba los datos.

Hay una particularidad que suele sorprender: si cambia SECRET_KEY, el salt diario también cambia, por lo que todos los visitantes que regresen se contabilizan como nuevos desde ese momento.

¿Por qué está vacía la columna de países de Analytics?

Porque ningún componente de la pila establece una cabecera de país. LinkBreeze obtiene el país de cabeceras del proxy como cf-ipcountry y x-vercel-ip-country. En un VPS detrás de su propio Caddy o Nginx, ninguna de esas cabeceras existe. Por eso el país se registra como null y el desglose queda vacío. El contenedor no incluye una base de datos GeoIP.

Hay dos formas de rellenarla. Puede colocar Cloudflare delante del dominio. Cloudflare añade cf-ipcountry a cada petición que pone en proxy. También puede establecer una de esas cabeceras en su propio reverse proxy a partir de una consulta GeoIP local.

El problema relacionado es más grave, así que compruébelo. Los controladores de clics y vistas leen primero la dirección del cliente desde X-Forwarded-For, después desde X-Real-IP y, si ninguna de esas cabeceras está presente, usan 0.0.0.0. Si publica el puerto 3000 directamente en Internet sin un proxy delante, todos los visitantes generan el mismo valor hash. Por tanto, el número de visitantes únicos permanece en 1 y el límite de tasa por IP de 60 eventos por minuto se aplica a toda la audiencia a la vez. Con la directiva reverse_proxy anterior, Caddy establece la cabecera automáticamente y ambos problemas desaparecen.

Importación desde Linktree y elementos que no se transfieren

El asistente de migración del panel acepta la URL de un perfil público o un archivo exportado. Reconoce páginas de linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me y LittleLink, además de exportaciones HTML y JSON genéricas. Para una URL de Linktree o Bento, lee el JSON __NEXT_DATA__ que esas páginas incluyen. En una página estática, lee las etiquetas anchor.

Se transfieren el título, la URL, la descripción y la imagen de cada enlace, si el enlace corresponde a un perfil social, y también el nombre visible, la biografía y el avatar. Puede elegir qué enlaces encontrados conservar antes de que se escriba cualquier dato en la base de datos.

No se transfieren el historial de analíticas, el tema y el diseño, los suscriptores de correo electrónico, las fechas de publicación programadas ni ningún elemento que la plataforma anterior mantenga protegido detrás de su propio inicio de sesión. Debe reconstruir el aspecto manualmente y aceptar que el historial de clics permanece en el servicio anterior.

El importador obtiene la URL desde su servidor y no desde el navegador, por lo que rechaza las direcciones que no son públicas. Private/local URLs are not allowed significa que proporcionó una dirección dentro de su propia red. El rechazo es deliberado: sin esta protección, cualquier persona con acceso al panel podría usar su servidor para explorar máquinas a las que sólo su servidor puede acceder. Los otros mensajes que puede ver son Only http and https URLs are allowed, Request timed out y Response too large.

El scraping depende del marcado de otra persona. Si el asistente no encuentra nada en una página que claramente contiene enlaces, esa plataforma cambió su HTML desde que se escribió el parser. Añada los enlaces manualmente en lugar de esperar una corrección. Si lo que realmente necesita son enlaces cortos medibles y no una página de perfil, un acortador de URL autoalojado como Shlink cumple esa función y funciona sin problemas en el mismo servidor.

Actualizar un despliegue fijado

# edit the image tag in docker-compose.yml, then
docker compose pull
docker compose up -d
docker compose logs -f linkbreeze

Las migraciones del esquema se ejecutan automáticamente cuando se inicia el contenedor. No existe un procedimiento documentado para revertirlas, por lo que debe crear primero una copia del volumen. Una actualización que no se puede revertir sólo es segura si puede restaurar el estado anterior.

El panel muestra un aviso cuando existe una versión más reciente. Comprueba esta información descargando un archivo pequeño con la versión desde el repositorio del proyecto en GitHub una vez cada 24 horas, y no envía ningún dato sobre su instancia. Lea las notas de la versión antes de cambiar la etiqueta, porque en esta etapa del proyecto una versión menor puede cambiar valores predeterminados de los que depende.

Modos de fallo y mensajes que verá

manifest unknown al extraer la imagen. La etiqueta se escribió como :v1.2.7. Las etiquetas del registro no contienen v, por lo que debe usar :1.2.7.

no matching manifest for linux/arm64/v8 in the manifest list entries. La imagen publicada sólo está disponible para amd64. Créela en el host ARM a partir del código fuente etiquetado.

El contenedor informa unhealthy mientras la página carga correctamente. Una comprobación de estado del archivo compose está llamando a curl, que no está incluida en la imagen. Elimínela y deje que se ejecute la comprobación de estado wget propia de la imagen.

Caddy muestra un error de certificado o no muestra nada. Compruebe docker compose logs caddy. Las causas habituales son que un registro A todavía no apunte a este VPS o que el puerto 80 esté cerrado en el firewall. Esto bloquea el desafío HTTP de ACME (entorno de gestión automática de certificados) que Caddy utiliza para demostrar que controla el dominio.

Unique visitors permanece en 1. Ningún proxy está estableciendo X-Forwarded-For, por lo que todos los visitantes generan el mismo hash.

El contenedor se detiene justo después de iniciar, aunque ayer funcionaba. Si cambió de un volumen con nombre a un montaje bind del host, el directorio de datos pertenece a root y la aplicación se ejecuta con uid 1000. Por tanto, no puede abrir el archivo de base de datos. sudo chown -R 1000:1000 el directorio del host.

Las solicitudes de seguimiento reciben HTTP 429. Se alcanzó el límite por IP de /api/track y /go/<id>. Los visitantes siguen siendo redirigidos a su destino; el clic simplemente no se contabiliza.

FAQ

¿LinkBreeze está listo para un enlace público en la biografía?

Es un proyecto reciente. En agosto de 2026, el repositorio tiene 178 estrellas, 17 forks y un único mantenedor, y la primera versión se publicó el 1 de julio de 2026. En promedio, se publican versiones más de dos veces por semana, por lo que los errores se corrigen rápido, pero el comportamiento también cambia con rapidez. La licencia MIT y el archivo SQLite local permiten conservar una página funcional aunque el desarrollo se detenga. Sin embargo, una aplicación web pública sin correcciones de seguridad se convierte en un riesgo. Considere que deberá mantener este software actualizado, no instalarlo una sola vez.

¿Qué etiqueta de imagen de LinkBreeze debo ejecutar?

Use la etiqueta de versión, por ejemplo ghcr.io/manak-hash/linkbreeze:1.2.7, y cámbiela de forma deliberada. El flujo de publicación sólo sube latest y el número de versión sin etiqueta adicional. Por tanto, :v1.2.7 con v no existe y Docker responde manifest unknown. La imagen sólo se compila para linux/amd64. En un VPS arm64, debe clonar la etiqueta y compilarla localmente.

¿Por qué el desglose por país permanece vacío en las estadísticas de LinkBreeze?

LinkBreeze obtiene el país del visitante a partir de cabeceras del proxy como cf-ipcountry o x-vercel-ip-country. No incluye una base de datos GeoIP propia. Un VPS situado detrás de su propio Caddy o Nginx no establece ninguna de esas cabeceras, por lo que el país se almacena como null. Coloque Cloudflare delante del dominio o configure el proxy inverso para establecer una de esas cabeceras mediante una consulta GeoIP local.

¿Qué debo respaldar exactamente y cómo lo restauro?

Respalde todo el volumen linkbreeze-data, no sólo el archivo de base de datos. /app/data/linkbreeze.db contiene todos los enlaces, la página, la configuración, los suscriptores y las filas de estadísticas. /app/data/uploads contiene las imágenes de avatar y miniatura a las que hace referencia la página. Detenga el contenedor, ejecute docker compose cp linkbreeze:/app/data ./backup-$(date +%F) y vuelva a iniciarlo. Para restaurarlo, copie el directorio de nuevo en el contenedor detenido y arránquelo. La exportación JSON del panel es una instantánea de la configuración del perfil, los enlaces, los ajustes y los temas. No contiene estadísticas ni imágenes.

¿La importación desde Linktree conserva mis estadísticas y mi tema?

No. El asistente de migración lee los títulos de los enlaces, las URL, las descripciones y las imágenes del perfil público anterior, además del nombre visible, la biografía y el avatar. El historial de estadísticas, el tema, los suscriptores de correo electrónico y las fechas de publicación programada no se importan. Después de la importación, reconstruya el diseño en el editor de temas y tenga en cuenta que el historial de clics seguirá en la plataforma anterior.

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