SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-15

LinkBreeze zelf hosten met Docker en Caddy

Host uw eigen Linktree alternatief met Docker Compose en Caddy. Leer hoe u cookieloze tracking instelt, image tags pint en uw volledige site beheert via één enkel SQLite volume.

Wat LinkBreeze is

LinkBreeze is een zelfgehost alternatief voor Linktree: één Docker-container die een openbare link-in-bio-pagina en een beheerdashboard serveert, waarbij alle statusgegevens in één enkel SQLite-bestand worden opgeslagen. Het is MIT-gelicentieerd, geschreven in TypeScript op basis van Next.js en gepubliceerd als ghcr.io/manak-hash/linkbreeze. Om het uit te voeren heeft u een VPS nodig, een domein met een A-record dat naar die VPS wijst, openstaande poorten 80 en 443, en Docker Engine met de Compose-plugin.

Deze handleiding behandelt de implementatie die de repository daadwerkelijk ondersteunt: Docker Compose achter een reverse proxy die zijn eigen certificaten ophaalt. Het behandelt ook wat er mis kan gaan, aangezien een link in een bio een openbare URL is waar anderen op klikken, en een defecte link kost u de klik.

Voordat u hiermee begint, moet u zich realiseren hoe nieuw dit project is.

Per augustus 2026 heeft de repository 178 sterren, 17 forks en één enkele beheerder. De eerste getagde release, v1.0.0, dateert van 1 juli 2026. Dit is een project van enkele weken oud, niet van enkele jaren.

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

Sinds v1.0.0 heeft het project 17 getagde releases uitgebracht over 7 kalenderweken. De laatste week in die grafiek liep nog toen deze handleiding werd geschreven en bevatte al 3 releases.

Beschouw dit als twee afzonderlijke feiten. De beheerder is actief en bugs worden binnen enkele dagen opgelost. Het schema en de standaardinstellingen zijn echter nog in beweging, waardoor een instantie die u implementeert en vervolgens vergeet, ver zal afwijken van de code die momenteel wordt geschreven.

De licentie beschermt u tegen het ergste scenario. MIT in combinatie met een container image en een SQLite-bestand op uw eigen schijf betekent dat als de ontwikkeling stopt, de huidige versie blijft functioneren. Waar het u niet tegen beschermt, is een publieke webapplicatie die geen beveiligingsupdates meer ontvangt, wat na verloop van tijd een risico vormt. Implementeer dit als iets dat u zult blijven bijwerken en zorg dat de onderstaande back-upprocedure vanaf de eerste dag werkt.

Pin de image-tag en gebruik niet latest

De release-workflow pusht exact twee tags per versie: latest en het versienummer waarbij de voorloop-v is verwijderd. De vastgezette (pinned) tag voor release v1.2.7 is daarom ghcr.io/manak-hash/linkbreeze:1.2.7. Het schrijven van :v1.2.7 haalt niets op en Docker rapporteert manifest unknown, omdat die tag nooit is gepusht.

Zet de tag vast omdat latest verandert. Bij het tempo in de bovenstaande tabel is een docker compose pull tegen latest een ongecontroleerde upgrade van een pagina die uw publiek gebruikt. Met een vastgezette tag vindt een upgrade pas plaats wanneer u het bestand bewerkt.

Nog één ding over de image. De release-workflow bouwt zonder platforms:-instelling, dus de gepubliceerde image is alleen linux/amd64. Op een arm64-host mislukt de pull met no matching manifest for linux/arm64/v8 in the manifest list entries. Als u een ARM VPS in plaats van x86 gebruikt, bouw de image dan op de machine zelf:

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

Gebruik daarna linkbreeze:1.2.7 als de image-naam in het onderstaande compose-bestand.

LinkBreeze achter Caddy implementeren met automatische TLS

Caddy vraagt zelf certificaten aan bij Let's Encrypt en vernieuwt deze automatisch, waardoor TLS (transport layer security) geen aparte certificaatstap vereist. De volledige implementatie bestaat uit drie bestanden in één map.

Genereer eerst het geheim:

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

SECRET_KEY ondertekent de admin-sessiecookie en voorziet de analytics-bezoekershash van een salt. Het compose-bestand in de repository stelt dit standaard in op ${SECRET_KEY:-changeme-in-production}. Een instantie waarbij u deze stap overslaat, draait dus met een sessie-ondertekeningssleutel die publiekelijk zichtbaar is op GitHub. Stel dit in vóór de eerste start, omdat het later wijzigen ervan u uitlogt en de analytics-salt reset.

Schrijf 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 is optioneel maar aanbevolen: het vertelt de applicatie wat het werkelijke publieke adres is. Hierdoor kan een verzoek dat binnenkomt met een vervalste Host-header er niet voor zorgen dat de applicatie links genereert naar het domein van iemand anders.

Schrijf Caddyfile daarnaast, met uw eigen domein:

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

Caddy stelt standaard X-Forwarded-For en X-Forwarded-Proto in op geproxiede verzoeken, waar de analytics afhankelijk van zijn. Start de container:

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

docker compose ps hoort de LinkBreeze-container als healthy te tonen. De image bevat een eigen healthcheck, wget --spider -q http://127.0.0.1:3000/api/health, dus u hoeft er zelf geen toe te voegen. Kopieer de healthcheck niet uit het Caddy-voorbeeld van de repository: deze roept curl aan, terwijl de image is gebouwd op node:22-alpine. Deze bevat busybox wget en geen curl. Die container rapporteert unhealthy terwijl hij pagina's uitstekend serveert.

Open https://links.example.com in een browser. Het eerste bezoek leidt naar de installatiewizard op /setup, waar u het enige admin-account aanmaakt. Daarna bevindt het dashboard zich op /dashboard en het inlogformulier op /login. Dat account is lokaal voor deze instantie en de applicatie bevat geen single sign-on-koppeling. Als u wilt dat het dashboard reageert op dezelfde inloggegevens als uw andere gehoste diensten, moet dit worden afgehandeld door een forward auth-proxy ervoor, zoals een zelfgehoste Authentik.

Let op wat het compose-bestand niet doet: het publiceert poort 3000 nooit. Alleen Caddy luistert op de publieke interface. Als de syntaxis van het Compose-bestand nieuw voor u is, behandelt de basis van Docker Compose voor een VPS de onderdelen waar dit bestand van uitgaat. Als u al iets anders voor uw diensten heeft draaien, legt Nginx, Caddy en Traefik vergeleken uit wat er verandert. De repository bevat werkende voorbeelden voor Nginx met Certbot, Traefik en een Cloudflare-tunnel.

Waar uw data zich bevindt en wat een back-up moet bevatten

DATABASE_PATH wijst naar /app/data/linkbreeze.db. Geüploade avatars en link-thumbnails worden daarnaast geschreven in /app/data/uploads. Beide bevinden zich in het named volume linkbreeze-data, dus de eenheid van back-up is het volume, niet enkel het databasebestand. Als u het bestand herstelt zonder de uploads-directory, geeft elke afbeelding op de pagina een 404-foutmelding.

Al het overige staat in die ene database: pagina's, links, instellingen, thema's, e-mailabonnees en analytics-rijen.

Maak de kopie terwijl de container is gestopt:

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

Stop de container eerst, omdat het kopiëren van een SQLite-database terwijl een proces ernaar schrijft kan leiden tot een halfvoltooide transactie; de kopie opent dan als een corrupt bestand. De pagina is offline terwijl de kopie wordt gemaakt. Herstellen is dezelfde handeling in omgekeerde volgorde:

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

Het dashboard biedt ook een JSON-export, geserveerd vanuit /api/backup als linkbreeze-backup-YYYY-MM-DD.json. Deze bevat het profiel, de links, de instellingen en de opgeslagen thema's. Het bevat geen analytics-geschiedenis, e-mailabonnees of geüploade afbeeldingen. Bij het herstellen hiervan worden de huidige rijen in die vier tabellen verwijderd voordat de gegevens uit het bestand worden ingevoegd. Beschouw dit als een configuratiesnapshot voor het verplaatsen van hosts of het ongedaan maken van een bewerkingsfout. De volume-kopie is de back-up.

Voor opslag gelden hier dezelfde twee regels als overal waar u SQLite in productie op een VPS draait. Houd de database op een lokale schijf, omdat de locking van SQLite onbetrouwbaar is op een netwerkbestandssysteem en u dit pas merkt bij een corrupte pagina. Als u het named volume vervangt door een host bind mount, voer dan eerst een chown uit op de host-directory: de container draait als de non-root gebruiker node, uid 1000 in node:22-alpine. Een directory die door root is aangemaakt, is niet beschrijfbaar voor deze gebruiker, waardoor de applicatie de database niet kan openen en de container bij het opstarten afsluit. Bind mounts versus named volumes in Compose behandelt deze afweging volledig.

De analytics en de cookiemelding die u niet nodig heeft

Dit is de functionaliteit die het zelf hosten van een pagina rechtvaardigt, terwijl u deze elders gratis zou kunnen krijgen.

De analytics zijn cookievrij. Er wordt geen cookie geplaatst bij een bezoeker en er worden geen scripts van derden geladen op de publieke pagina. Een bezoeker wordt geïdentificeerd aan de hand van een SHA-256 hash van het IP-adres, de user agent-string en een salt, afgekapt tot 16 hexadecimale tekens. De salt is zelf een hash van de huidige UTC-datum en uw SECRET_KEY, waardoor deze om middernacht UTC wijzigt en de hashes van gisteren niet kunnen worden gekoppeld aan die van vandaag. Het ruwe IP-adres wordt nooit naar de database geschreven.

Klikken worden op de server geteld. Elke http-link op de publieke pagina wijst naar /go/<id> op uw eigen domein, dat de klik registreert en vervolgens antwoordt met een 302-redirect naar de werkelijke bestemming. Het tellen werkt daarom ook voor lezers met uitgeschakelde JavaScript en in in-app browsers die achtergrondverzoeken blokkeren. Paginaweergaven worden geregistreerd via /api/track.

Twee uitzonderingen zijn het vermelden waard. Een verzoek met een geldige admin-sessie wordt overgeslagen, zodat het bewerken van uw eigen pagina de cijfers niet vertekent. Bekende crawler user agents worden eveneens overgeslagen.

Over toestemming: er wordt niets opgeslagen op het apparaat van de lezer, en een cookie op het apparaat van de lezer is precies waarvoor een cookiemelding toestemming vraagt. Uw verplichtingen hangen nog steeds af van de locatie van uw lezers, dus controleer deze, maar er is hier geen tracking-cookie om te melden en er is geen derde partij die de gegevens ontvangt.

Eén kanttekening die mensen vaak verrast: roteer SECRET_KEY en de dagelijkse salt verandert daarmee, waardoor elke terugkerende bezoeker vanaf dat moment als nieuw wordt geteld.

Waarom is de kolom voor land in de analytics leeg?

Omdat niets in uw stack een land-header instelt. LinkBreeze bepaalt het land op basis van proxy-headers zoals cf-ipcountry en x-vercel-ip-country. Op een VPS achter uw eigen Caddy of Nginx bestaan deze headers niet, waardoor het land als null wordt geregistreerd en het overzicht leeg blijft. Er is geen GeoIP-database aanwezig in de container.

Er zijn twee manieren om dit op te lossen. Plaats Cloudflare voor het domein; dit voegt cf-ipcountry toe aan elk verzoek dat wordt geproxied. Of stel een van deze headers in uw eigen reverse proxy in op basis van een lokale GeoIP-opzoeking.

De gerelateerde valkuil is ernstiger, dus controleer deze goed. De handlers voor clicks en views lezen het clientadres eerst uit X-Forwarded-For, daarna uit X-Real-IP, en vallen terug op 0.0.0.0 wanneer geen van beide headers aanwezig is. Als u poort 3000 direct aan het internet blootstelt zonder proxy ervoor, krijgt elke bezoeker dezelfde hashwaarde. Dit betekent dat het aantal unieke bezoekers altijd op 1 blijft staan en dat de rate limit van 60 events per minuut per IP-adres direct op uw gehele publiek van toepassing is. Achter de reverse_proxy-richtlijn hierboven stelt Caddy de header voor u in en verdwijnen beide problemen.

Importeren vanuit Linktree, en wat niet wordt overgenomen

De migratiewizard in het dashboard accepteert een openbare profiel-URL of een geëxporteerd bestand. Deze herkent linktr.ee, bento.me, lnk.bio, tap.link, hopp.bio, beacons.ai, solo.to, linkfly, mssg.me en LittleLink-pagina's, evenals algemene HTML- en JSON-exports. Voor een Linktree- of Bento-URL leest de wizard de __NEXT_DATA__ JSON die deze pagina's insluiten. Voor een statische pagina leest de wizard de anchor-tags.

Wat wordt overgenomen is de titel, de URL, de beschrijving en de afbeelding van elke link, of de link een sociaal profiel is, en uw weergavenaam, biografie en avatar. U kiest welke van de gevonden links u wilt behouden voordat er gegevens naar de database worden geschreven.

Wat niet wordt overgenomen is de analysegeschiedenis, het thema en de lay-out, e-mailabonnees, geplande publicatiedata en alles wat het oude platform achter zijn eigen inlogscherm houdt. Houd er rekening mee dat u het uiterlijk handmatig opnieuw moet opbouwen en accepteer dat de oude klikgeschiedenis op de oude dienst blijft staan.

De importeur haalt de URL op vanaf uw server in plaats van vanuit uw browser, dus adressen die niet openbaar zijn worden geweigerd. Private/local URLs are not allowed betekent dat u een adres binnen uw eigen netwerk heeft opgegeven, en deze weigering is bewust: zonder deze beperking zou iedereen met dashboardtoegang uw server kunnen gebruiken om machines te scannen die alleen vanaf uw server bereikbaar zijn. De andere meldingen die u kunt zien zijn Only http and https URLs are allowed, Request timed out en Response too large.

Scraping is afhankelijk van de markup van derden. Als de wizard niets vindt op een pagina die duidelijk links bevat, dan heeft dat platform de HTML gewijzigd sinds de parser is geschreven. Voeg de links handmatig toe in plaats van te wachten op een oplossing. Als u in feite meetbare korte links wilt in plaats van een profielpagina, dan doet een zelfgehoste URL-verkorter zoals Shlink dat werk en draait deze probleemloos op dezelfde machine.

Een vastgezette deployment bijwerken

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

Schema-migraties worden automatisch uitgevoerd wanneer de container start. Er is geen gedocumenteerde methode om deze ongedaan te maken, dus maak eerst een kopie van het volume. Een upgrade die u niet kunt terugdraaien is alleen veilig als u de vorige staat kunt herstellen.

Het dashboard toont een melding wanneer er een nieuwere release beschikbaar is. Dit gebeurt door eenmaal per 24 uur een klein versiebestand op te halen uit de GitHub-repository van het project; er wordt geen informatie over uw instantie verzonden. Lees de release notes voordat u de tag wijzigt, aangezien in dit stadium van het project een kleine versie-update standaardinstellingen kan wijzigen waar u op vertrouwt.

Foutmodi en de meldingen die u zult zien

manifest unknown bij het pullen. De tag werd geschreven als :v1.2.7. Registry-tags bevatten geen v, gebruik daarom :1.2.7.

no matching manifest for linux/arm64/v8 in the manifest list entries. De gepubliceerde image is alleen beschikbaar voor amd64. Bouw deze op de ARM-host vanuit de getagde broncode.

De container rapporteert unhealthy terwijl de pagina correct laadt. Een healthcheck in uw compose-bestand roept curl aan, wat niet in de image aanwezig is. Verwijder deze en laat de ingebouwde wget healthcheck van de image draaien.

Caddy geeft een certificaatfout of helemaal niets. Controleer docker compose logs caddy. De gebruikelijke oorzaken zijn een A-record dat nog niet naar deze VPS wijst, of poort 80 die gesloten is op de firewall. Hierdoor wordt de ACME (automatic certificate management environment) HTTP-challenge geblokkeerd die Caddy gebruikt om aan te tonen dat het domein onder uw beheer valt.

Unieke bezoekers blijft op 1 staan. Geen enkele proxy stelt X-Forwarded-For in, waardoor elke bezoeker identiek wordt gehasht.

De container stopt direct na het opstarten, terwijl deze gisteren nog werkte. Als u bent overgestapt van een named volume naar een host bind mount, is de datadirectory eigendom van root terwijl de applicatie draait als uid 1000. Hierdoor kan het databasebestand niet worden geopend. sudo chown -R 1000:1000 de host-directory.

Tracking-verzoeken worden beantwoord met HTTP 429. De per-IP throttle op /api/track en /go/<id> is bereikt. Bezoekers worden nog steeds doorgestuurd naar hun bestemming, de klik wordt simpelweg niet geteld.

FAQ

Het is een jong project. Per augustus 2026 bevat de repository 178 sterren, 17 forks en één beheerder, en de eerste release dateert van 1 juli 2026. Releases verschijnen gemiddeld meer dan twee keer per week, waardoor bugs snel worden verholpen en het gedrag snel verandert. De MIT-licentie en het lokale SQLite-bestand betekenen dat u een werkende pagina behoudt, zelfs als de ontwikkeling stopt. Een openbare webapplicatie zonder beveiligingsupdates vormt echter een risico; beschouw dit daarom als software die u blijft bijwerken in plaats van eenmalig te installeren.

Welke LinkBreeze image-tag moet ik gebruiken?

Gebruik de versie-tag, bijvoorbeeld ghcr.io/manak-hash/linkbreeze:1.2.7, en wijzig deze bewust. De release-workflow pusht alleen latest en het kale versienummer, dus :v1.2.7 met de v bestaat niet en Docker geeft manifest unknown als antwoord. De image is uitsluitend gebouwd voor linux/amd64; op een arm64 VPS moet u de tag klonen en lokaal bouwen.

Waarom blijft het landenoverzicht leeg in de LinkBreeze-analytics?

LinkBreeze leest het land van de bezoeker uit proxy-headers zoals cf-ipcountry of x-vercel-ip-country en bevat zelf geen GeoIP-database. Een VPS achter uw eigen Caddy of Nginx stelt geen van deze headers in, waardoor het land als null wordt opgeslagen. Plaats Cloudflare voor het domein of laat uw reverse proxy een van deze headers instellen op basis van een lokale GeoIP-opzoeking.

Wat moet ik precies back-uppen en hoe herstel ik dit?

Maak een back-up van het volledige linkbreeze-data-volume, niet alleen van het databasebestand. /app/data/linkbreeze.db bevat elke link, pagina, instelling, abonnee en analytics-rij, en /app/data/uploads bevat de avatar- en thumbnail-afbeeldingen waarnaar de pagina verwijst. Stop de container, voer docker compose cp linkbreeze:/app/data ./backup-$(date +%F) uit en start deze vervolgens opnieuw. Herstel de back-up door de map terug te kopiëren naar de gestopte container en deze te starten. De JSON-export vanuit het dashboard is een configuratiesnapshot van het profiel, de links, instellingen en thema's; deze bevat geen analytics en geen afbeeldingen.

Neemt het importeren vanuit Linktree mijn analytics en thema mee?

Nee. De migratiewizard leest de linktitels, URL's, beschrijvingen en afbeeldingen van uw oude openbare profiel, evenals uw weergavenaam, bio en avatar. De analytics-geschiedenis, het thema, e-mailabonnees en geplande publicatiedata blijven achter. Bouw het uiterlijk opnieuw op in de thema-editor na de import en houd er rekening mee dat uw klikgeschiedenis op het oude platform blijft staan.

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