SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-15

Come installare LinkBreeze con Docker su un VPS

Installa LinkBreeze con Docker Compose e Caddy: scopri i tag delle immagini, il tracking dei clic senza cookie e l’unico volume che contiene tutto il sito.

Che cos’è LinkBreeze

LinkBreeze è un’alternativa self-hosted a Linktree: un container Docker che pubblica una pagina pubblica di link nella bio e una dashboard di amministrazione, memorizzando tutto lo stato in un singolo file SQLite. È distribuito con licenza MIT, è scritto in TypeScript su Next.js e viene pubblicato come ghcr.io/manak-hash/linkbreeze. Per eseguirlo servono un VPS, un dominio con un record A che punti al VPS, le porte 80 e 443 aperte e Docker Engine con il plugin Compose.

Questa guida descrive il deployment effettivamente supportato dal repository: Docker Compose dietro un reverse proxy che acquisisce autonomamente i certificati. Descrive anche cosa può non funzionare, perché un link nella bio è un URL pubblico su cui fanno clic altre persone e un link non funzionante fa perdere quel clic.

Prima di tutto, è importante considerare quanto sia recente questo progetto.

Ad agosto 2026 il repository conta 178 stelle, 17 fork e un solo maintainer. La prima release con tag, v1.0.0, è datata 1 luglio 2026. Si tratta di un progetto che ha poche settimane, non diversi anni.

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
  }
]

Dalla v1.0.0 il progetto ha pubblicato 17 release con tag distribuite su 7 settimane di calendario. L'ultima settimana mostrata nel grafico era ancora in corso quando è stata scritta questa guida e conteneva già 3 di queste release.

Considera questi due aspetti separatamente. Il maintainer è attivo e i bug vengono corretti nel giro di pochi giorni. Anche lo schema e i valori predefiniti sono ancora soggetti a modifiche, quindi un'istanza che installi e poi dimentichi può allontanarsi notevolmente dal codice in fase di sviluppo.

La licenza offre protezione nello scenario peggiore. La licenza MIT, un'immagine container e un file SQLite sul tuo disco fanno sì che, se lo sviluppo si interrompe, ciò che hai installato continui a funzionare. Non ti protegge però da un'applicazione web esposta pubblicamente che smette di ricevere correzioni di sicurezza e che, nel tempo, diventa un rischio. Esegui il deployment considerando che dovrai continuare ad aggiornarla e mantieni operativa fin dal primo giorno la procedura di backup descritta di seguito.

Blocca il tag dell’immagine e non usare latest

Il workflow di release pubblica esattamente due tag per ogni versione: latest e il numero di versione con il prefisso v rimosso. Il tag bloccato per la release v1.2.7 è quindi ghcr.io/manak-hash/linkbreeze:1.2.7. Scrivere :v1.2.7 non scarica nulla e Docker restituisce manifest unknown, perché quel tag non è mai stato pubblicato.

Blocca il tag perché latest cambia. Con la frequenza indicata nel grafico precedente, un docker compose pull su latest equivale a un aggiornamento non verificato di una pagina utilizzata dal pubblico. Con un tag bloccato, l’aggiornamento avviene quando modifichi il file.

C’è un altro aspetto da considerare per l’immagine. Il workflow di release esegue la build senza impostare platforms:, quindi l’immagine pubblicata è solo linux/amd64. Su un host arm64 il download dell’immagine fallisce con no matching manifest for linux/arm64/v8 in the manifest list entries. Se esegui un VPS ARM invece di x86, crea l’immagine direttamente sul server:

git clone --branch v1.2.7 --depth 1 https://github.com/Manak-hash/LinkBreeze.git
cd LinkBreeze
docker build -t linkbreeze:1.2.7 .

Usa quindi linkbreeze:1.2.7 come nome dell’immagine nel file compose seguente.

Distribuire LinkBreeze dietro Caddy con TLS automatico

Caddy richiede e rinnova autonomamente i certificati da Let's Encrypt, quindi per TLS (transport layer security) non serve una procedura separata per i certificati. L'intera distribuzione consiste in tre file nella stessa directory.

Generare prima il secret:

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

SECRET_KEY firma il cookie della sessione amministrativa e aggiunge il salt all'hash dei visitatori per l'analisi. Il file Compose pubblicato nel repository lo imposta per impostazione predefinita su ${SECRET_KEY:-changeme-in-production}. Se si salta questo passaggio, l'istanza usa una chiave di firma della sessione pubblicata su GitHub. Impostarla prima del primo avvio, perché modificarla in seguito disconnette l'utente e reimposta il salt delle analisi.

Scrivere 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 è facoltativo, ma conviene impostarlo: comunica all'applicazione il proprio indirizzo pubblico effettivo, impedendo che una richiesta con un header Host contraffatto induca l'applicazione a generare link verso il dominio di un altro utente.

Scrivere Caddyfile accanto a esso, usando il proprio dominio:

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

Per impostazione predefinita, Caddy imposta X-Forwarded-For e X-Forwarded-Proto nelle richieste inoltrate al proxy; le analisi dipendono da questi valori. Avviare i container:

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

docker compose ps dovrebbe mostrare il container LinkBreeze nello stato healthy. L'immagine include già il proprio healthcheck, wget --spider -q http://127.0.0.1:3000/api/health, quindi non è necessario aggiungerne uno. Non copiare l'healthcheck dall'esempio Caddy incluso nel repository: esegue curl, mentre l'immagine è basata su node:22-alpine, che include busybox wget ma non include curl. Per questo il container segnala unhealthy anche quando serve correttamente le pagine.

Aprire https://links.example.com in un browser. Alla prima visita viene mostrata la procedura guidata di configurazione all'indirizzo /setup, che crea l'unico account amministrativo. In seguito, la dashboard è disponibile all'indirizzo /dashboard e il modulo di accesso all'indirizzo /login. L'account è locale a questa istanza e l'applicazione non dispone di un'integrazione per il single sign-on. Se si vuole che la dashboard utilizzi lo stesso accesso di tutti gli altri servizi pubblicati, è necessario configurare un proxy forward auth davanti all'applicazione, ad esempio Authentik self-hosted.

Si noti cosa non fa il file Compose: non pubblica mai la porta 3000. Solo Caddy è in ascolto sull'interfaccia pubblica. Se la sintassi dei file Compose è nuova, le basi di Docker Compose per un VPS illustrano gli elementi presupposti da questo file. Se invece si utilizza già un altro componente davanti all'applicazione, confronto tra Nginx, Caddy e Traefik spiega quali modifiche applicare. Il repository include esempi funzionanti per Nginx con Certbot, Traefik e un tunnel Cloudflare.

Dove risiedono i dati e cosa deve contenere un backup

DATABASE_PATH punta a /app/data/linkbreeze.db. Gli avatar caricati e le miniature dei link vengono scritti accanto a questo file, in /app/data/uploads. Entrambi si trovano nel volume denominato linkbreeze-data, quindi l'unità di backup è il volume, non il solo file del database. Se si ripristina il file senza la directory degli upload, tutte le immagini della pagina restituiscono un errore 404.

Tutto il resto si trova effettivamente in quell'unico database: pagine, link, impostazioni, tema, iscritti alle email e righe di analytics.

Eseguire la copia con il container arrestato:

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

Arrestarlo prima è necessario perché la copia di un database SQLite mentre un processo vi sta scrivendo può acquisire una transazione incompleta. In questo caso la copia viene poi aperta come file corrotto. La pagina non è disponibile durante la copia. Il ripristino segue la stessa procedura al contrario:

docker compose stop linkbreeze
docker compose cp ./backup-2026-08-14/. linkbreeze:/app/data
docker compose start linkbreeze
docker compose logs -f linkbreeze

La dashboard offre anche un'esportazione JSON, disponibile in /api/backup come linkbreeze-backup-YYYY-MM-DD.json. Contiene il profilo, i link, le impostazioni e i temi salvati. Non contiene la cronologia degli analytics, gli iscritti alle email o le immagini caricate. Inoltre, durante il ripristino elimina le righe correnti di queste quattro tabelle prima di inserire quelle presenti nel file. Considerarla uno snapshot della configurazione per spostare il servizio su un altro host o annullare una modifica errata. La copia del volume è il backup.

Qui si applicano le stesse due regole di archiviazione valide in ogni altro caso quando si esegue SQLite in produzione su un VPS. Conservare il database su un disco locale, perché il locking di SQLite non è affidabile su un filesystem di rete e una pagina corrotta è il modo in cui ci si accorge del problema. Se si sostituisce il volume denominato con un bind mount dell'host, eseguire prima chown sulla directory dell'host: il container viene eseguito con l'utente non-root node, UID 1000 in node:22-alpine, e una directory creata da root non è scrivibile da quell'utente. Di conseguenza, l'applicazione non riesce ad aprire il database e il container termina all'avvio. Bind mount al posto dei volumi denominati in Compose descrive completamente questo compromesso.

L'analisi e il banner per il consenso che non servono

Questa è la funzionalità che giustifica il self-hosting di una pagina che altrove potresti ottenere gratuitamente.

L'analisi non usa cookie. Per un visitatore non viene impostato alcun cookie e nella pagina pubblica non viene caricato alcuno script di terze parti. Un visitatore viene identificato tramite un hash SHA-256 dell'indirizzo IP, della stringa user agent e di un salt, troncato a 16 caratteri esadecimali. Il salt è a sua volta un hash della data UTC corrente e del tuo SECRET_KEY, quindi cambia a mezzanotte UTC e gli hash di ieri non possono essere confrontati con quelli di oggi. L'indirizzo IP in chiaro non viene mai scritto nel database.

I clic vengono conteggiati sul server. Ogni link HTTP nella pagina pubblica punta a /go/<id> sul tuo dominio, che registra il clic e poi risponde con un redirect 302 verso la destinazione reale. Il conteggio funziona quindi anche per i lettori con JavaScript disabilitato e nei browser integrati nelle applicazioni che bloccano le richieste in background. Le visualizzazioni di pagina vengono registrate tramite /api/track.

È utile conoscere due esclusioni. Le richieste che includono una sessione admin valida vengono ignorate, quindi modificare la propria pagina non altera i numeri. Vengono ignorati anche gli user agent noti dei crawler.

Per quanto riguarda il consenso: sul dispositivo del lettore non viene memorizzato nulla e un cookie memorizzato sul dispositivo del lettore è esattamente l'elemento per il quale un banner per il consenso richiede l'autorizzazione. I tuoi obblighi dipendono comunque dal luogo in cui risiedono i lettori, quindi verificabili, ma qui non c'è alcun cookie di tracciamento da dichiarare e nessuna terza parte riceve i dati.

C'è un aspetto che spesso sorprende: se ruoti SECRET_KEY, cambia anche il salt giornaliero, quindi da quel momento ogni visitatore che torna viene conteggiato come nuovo.

Perché la colonna del paese nelle analisi è vuota?

Perché nulla nel tuo stack imposta un header del paese. LinkBreeze ricava il paese dagli header del proxy, ad esempio cf-ipcountry e x-vercel-ip-country. Su un VPS dietro il tuo Caddy o Nginx, nessuno di questi header esiste. Il paese viene quindi registrato come null e la suddivisione resta vuota. Nel container non è presente alcun database GeoIP.

Puoi risolvere il problema in due modi. Puoi mettere Cloudflare davanti al dominio: aggiungerà cf-ipcountry a ogni richiesta inoltrata. In alternativa, puoi impostare uno di questi header nel tuo reverse proxy usando una ricerca GeoIP locale.

Il problema correlato è più grave, quindi verificalo. I gestori dei clic e delle visualizzazioni leggono prima l'indirizzo client da X-Forwarded-For, poi da X-Real-IP e, se nessuno dei due header è presente, usano 0.0.0.0. Se pubblichi direttamente su Internet la porta 3000 senza un proxy davanti, ogni visitatore viene associato allo stesso valore hash. Il numero di visitatori unici resterà quindi sempre pari a 1 e il rate limiting di 60 eventi al minuto per indirizzo IP si applicherà contemporaneamente a tutto il pubblico. Con la direttiva reverse_proxy indicata sopra, Caddy imposta l'header automaticamente e risolve entrambi i problemi.

Importazione da Linktree e contenuti non trasferiti

La procedura guidata di migrazione nella dashboard accetta l’URL di un profilo pubblico o un file esportato. Riconosce le pagine di linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me e LittleLink, oltre agli export generici in HTML e JSON. Per un URL di Linktree o Bento legge il JSON __NEXT_DATA__ incorporato nella pagina. Per una pagina statica legge i tag anchor.

Vengono trasferiti il titolo, l’URL, la descrizione e l’immagine di ogni link, l’indicazione che il link sia un profilo social, oltre al nome visualizzato, alla bio e all’avatar. Prima di scrivere qualsiasi dato nel database, puoi scegliere quali link individuati mantenere.

Non vengono trasferiti la cronologia delle analisi, il tema e il layout, gli iscritti alle email, le date di pubblicazione pianificate e qualsiasi contenuto che la vecchia piattaforma protegge con il proprio login. Devi quindi ricreare manualmente l’aspetto della pagina e considerare che la cronologia dei clic resterà sul vecchio servizio.

L’importatore recupera l’URL dal server, non dal browser, quindi rifiuta gli indirizzi non pubblici. Private/local URLs are not allowed indica che hai fornito un indirizzo della tua rete interna. Il rifiuto è intenzionale: senza questo controllo, chiunque abbia accesso alla dashboard potrebbe usare il server per sondare macchine raggiungibili soltanto dal server stesso. Gli altri messaggi che potresti visualizzare sono Only http and https URLs are allowed, Request timed out e Response too large.

Lo scraping dipende dal markup di un sistema esterno. Se la procedura guidata non trova nulla in una pagina che contiene chiaramente dei link, significa che la piattaforma ha modificato il proprio HTML dopo la scrittura del parser. Aggiungi i link manualmente invece di attendere una correzione. Se ti servono realmente short link misurabili e non una pagina profilo, un URL shortener self-hosted come Shlink svolge questa funzione e può essere eseguito senza problemi sullo stesso server.

Aggiornamento di un deployment con versione bloccata

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

Le migrazioni dello schema vengono eseguite automaticamente all'avvio del container. Non esiste una procedura documentata per eseguirle a ritroso, quindi crea prima una copia del volume. Un aggiornamento che non puoi annullare è sicuro solo se puoi ripristinare lo stato precedente.

La dashboard mostra un banner quando è disponibile una release più recente. Verifica la disponibilità recuperando ogni 24 ore un piccolo file di versione dal repository GitHub del progetto e non invia informazioni sulla tua istanza. Leggi le note di rilascio prima di modificare il tag, perché in questa fase del progetto anche una versione minor può cambiare i valori predefiniti da cui dipendi.

Modalità di errore e messaggi visualizzati

manifest unknown durante il pull. Il tag è stato scritto come :v1.2.7. I tag del registry non contengono v, quindi usa :1.2.7.

no matching manifest for linux/arm64/v8 in the manifest list entries. L'immagine pubblicata è disponibile solo per amd64. Esegui la build sull'host ARM a partire dal sorgente associato al tag.

Il container segnala unhealthy mentre la pagina viene caricata correttamente. Un healthcheck nel file compose richiama curl, che non è incluso nell'immagine. Eliminalo e lascia che venga eseguito l'healthcheck wget integrato nell'immagine.

Caddy restituisce un errore relativo al certificato oppure non restituisce alcun contenuto. Controlla docker compose logs caddy. Le cause più comuni sono un record A che non punta ancora a questo VPS oppure la porta 80 chiusa dal firewall. In questo caso viene bloccata la challenge HTTP ACME (automatic certificate management environment) che Caddy usa per dimostrare di controllare il dominio.

Unique visitors è fermato su 1. Nessun proxy imposta X-Forwarded-For, quindi l'hash calcolato è identico per ogni visitatore.

Il container termina subito dopo l'avvio, anche se il giorno precedente funzionava. Se hai sostituito un volume nominato con un bind mount dell'host, la directory dei dati appartiene a root e l'applicazione viene eseguita con uid 1000. Di conseguenza, non può aprire il file del database. sudo chown -R 1000:1000 la directory sull'host.

Le richieste di tracking ricevono una risposta HTTP 429. È stato raggiunto il limite per IP su /api/track e /go/<id>. I visitatori vengono comunque reindirizzati alla destinazione; il clic semplicemente non viene conteggiato.

FAQ

È un progetto recente. Ad agosto 2026 il repository conta 178 stelle, 17 fork e un solo maintainer; la prima release è del 1 luglio 2026. In media vengono pubblicate più di 2 release alla settimana, quindi i bug vengono corretti rapidamente, ma anche il comportamento del software cambia rapidamente. La licenza MIT e il file SQLite locale consentono di mantenere una pagina funzionante anche se lo sviluppo si interrompe. Tuttavia, un'applicazione web pubblica senza aggiornamenti di sicurezza diventa un rischio, quindi considera LinkBreeze un software da aggiornare regolarmente e non da installare una sola volta.

Quale tag dell'immagine LinkBreeze devo usare?

Usa il tag della versione, ad esempio ghcr.io/manak-hash/linkbreeze:1.2.7, e aggiorna il tag deliberatamente. Il workflow di release pubblica soltanto latest e il numero di versione senza prefissi, quindi :v1.2.7 con v non esiste e Docker restituisce manifest unknown. L'immagine è compilata soltanto per linux/amd64; su una VPS arm64 devi clonare il tag e compilare l'immagine localmente.

Perché la suddivisione per paese resta vuota nelle analisi di LinkBreeze?

LinkBreeze legge il paese del visitatore da header del proxy come cf-ipcountry o x-vercel-ip-country e non include un proprio database GeoIP. Una VPS dietro Caddy o Nginx gestiti da te non imposta nessuno di questi header, quindi il paese viene memorizzato come null. Metti Cloudflare davanti al dominio oppure configura il reverse proxy in modo che imposti uno di questi header usando una ricerca GeoIP locale.

Che cosa devo sottoporre esattamente a backup e come lo ripristino?

Esegui il backup dell'intero volume linkbreeze-data, non soltanto del file del database. /app/data/linkbreeze.db contiene ogni link, pagina, impostazione, iscritto e riga delle analisi; /app/data/uploads contiene le immagini dell'avatar e delle miniature referenziate dalla pagina. Arresta il container, esegui docker compose cp linkbreeze:/app/data ./backup-$(date +%F), quindi avvialo di nuovo. Per il ripristino, copia nuovamente la directory nel container arrestato e avvialo. L'esportazione JSON dalla dashboard è uno snapshot della configurazione di profilo, link, impostazioni e temi; non contiene né le analisi né le immagini.

L'importazione da Linktree trasferisce anche le analisi e il tema?

No. La procedura guidata di migrazione legge dal vecchio profilo pubblico i titoli dei link, gli URL, le descrizioni e le immagini, oltre al nome visualizzato, alla biografia e all'avatar. La cronologia delle analisi, il tema, gli iscritti alle e-mail e le date di pubblicazione pianificate non vengono trasferiti. Dopo l'importazione, ricrea l'aspetto nell'editor dei temi e considera che la cronologia dei clic resterà sulla piattaforma precedente.

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