SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-28

Zelf een URL-verkorter hosten met Shlink en Docker

Host uw eigen URL-verkorter op een VPS met Shlink en Docker Compose. Leer hoe u een kort domein koppelt, Postgres configureert en klikstatistieken beheert voor Shlink 5.1.

Wat u bouwt

Een zelfgehoste URL-verkorter is een kleine server die een lange link omzet in een korte link die uw eigendom is, en die elke klik daarop telt. Shlink is de juiste keuze: het is open source, wordt geleverd als Docker image en voert de volledige taak uit in één container plus een database. Deze handleiding plaatst de software op een VPS achter een eigen kort domein, inclusief HTTPS, een API key, QR-codes en klikstatistieken.

Twee onderdelen zorgen ervoor dat het aanvoelt als een commerciële URL-verkorter. De API-server verwerkt redirects en beheert de gegevens. De webclient is een afzonderlijke statische applicatie die vanuit uw browser met die API communiceert. U kunt beide draaien, of alleen de API gebruiken en deze aansturen via de command line.

De versienummers in deze handleiding waren actueel per juli 2026: Shlink 5.1 en shlink-web-client 4.8.

Wijs eerst een kort domein toe aan de server

Het domein is het product. s.example.com/abc123 is de link die gebruikers zien, dus kies een korte naam en leg deze vast voordat u met de installatie begint. Shlink slaat het domein op bij elke verkorte URL; als u dit later wijzigt, werken alle reeds verspreide links niet meer.

Maak één DNS A-record aan voor het korte domein dat verwijst naar het publieke IPv4-adres van uw VPS. Voeg ook een AAAA-record toe als de server over IPv6 beschikt. Controleer vervolgens of het domein correct naar het IP-adres verwijst voordat u verdergaat.

dig +short s.example.com A

De uitvoer moet het adres van uw server zijn. Als de uitvoer leeg is, is het record nog niet verspreid. Elke volgende stap zal dan op een onduidelijke manier mislukken, omdat er geen TLS (transport layer security)-certificaat kan worden uitgegeven voor een naam die niet naar een IP-adres verwijst.

Het compose-bestand

Shlink heeft een database nodig. SQLite volstaat voor een test, maar Postgres is de juiste keuze voor alles wat u wilt behouden. Het aantal bezoekersrijen loopt namelijk op en Postgres verwerkt de indexen en gelijktijdige schrijfacties beter. Plaats dit in /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:

Beide gepubliceerde poorten binden aan 127.0.0.1, waardoor er niets bereikbaar is vanaf het internet totdat de reverse proxy in de volgende sectie is geplaatst. Docker schrijft zijn eigen doorstuurregels vóór de firewall van de host, wat betekent dat een eenvoudige 8080:8080-regel de applicatie zou blootstellen, zelfs op een server waarvan de firewall gesloten lijkt. Binden aan het loopback-adres voorkomt dit. Hetzelfde patroon is van toepassing op elke applicatie die u op deze manier uitvoert; dit wordt in meer detail behandeld in de handleiding voor Docker Compose op een VPS.

Het databasewachtwoord komt uit een .env-bestand naast het compose-bestand, zodat het nooit in de YAML terechtkomt.

sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.env

Start het geheel en bekijk of de API opkomt.

cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlink

De eerste start voert de databasemigraties uit, waardoor dit langer duurt dan bij latere starts. Controleer zodra het proces stabiel is of de service lokaal antwoordt.

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/health

Een 200 betekent dat de API actief is en de databaseverbinding werkt. Een 500 op dit punt wijst bijna altijd naar de database: het DB_PASSWORD in .env komt niet overeen met de gegevens waarmee Postgres is aangemaakt. De Postgres-image leest POSTGRES_PASSWORD namelijk alleen wanneer deze een lege datamap initialiseert. Het later wijzigen van het wachtwoord heeft geen effect totdat u het volume verwijdert en opnieuw begint.

HTTPS-termination ervoor plaatsen

Shlink serveert standaard HTTP op poort 8080. TLS hoort thuis in een reverse proxy, waarbij de enige instelling die ertoe doet het doorgeven van de oorspronkelijke hostnaam is. Shlink bepaalt bij welk domein een short code hoort door de Host-header te lezen. Een proxy die deze header herschrijft, resulteert in 404-fouten bij bestaande links en bezoekersstatistieken die aan het verkeerde domein worden gekoppeld.

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

Vraag vervolgens het certificaat aan. De volledige handleiding, inclusief de timer voor vernieuwing, staat in de Certbot-gids voor nginx op Ubuntu 24.04.

sudo certbot --nginx -d s.example.com

IS_HTTPS_ENABLED: "true" in het compose-bestand zorgt ervoor dat Shlink https:// afdrukt in de korte URL's die het teruggeeft. Het activeert TLS niet uit zichzelf. Laat u dit false achter een HTTPS-proxy, dan is elke link die de API teruggeeft een http://-link die vervolgens doorverwijst. Dit kost een extra round-trip en ziet er in de webclient onjuist uit.

Een API-sleutel aanmaken

Niets kan communiceren met de API zonder een sleutel. Genereer er een via de CLI in de container.

sudo docker compose exec shlink shlink api-key:generate --name "web client"

Het commando toont de sleutel slechts één keer. Kopieer deze direct, omdat de sleutel gehasht wordt opgeslagen en niet opnieuw kan worden weergegeven. shlink api-key:list toont de namen en of elke sleutel is ingeschakeld, maar nooit de sleutel zelf. Trek een sleutel in met shlink api-key:disable en de naam.

Elke REST-aanroep bevat de sleutel in een X-Api-Key-header.

curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urls

Een JSON-object met een shortUrls-sleutel betekent dat de sleutel werkt. Een 401 met INVALID_API_KEY betekent dat de sleutel onjuist is, is uitgeschakeld of de vervaldatum is verstreken.

De CLI is de snelste manier om links aan te maken en is uitermate geschikt voor scripting.

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 geeft u een leesbare link in plaats van een gegenereerde code. Slugs zijn uniek per domein; een tweede poging met een reeds gebruikte slug mislukt daarom in plaats van de eerste link stilletjes te overschrijven. --tag kan worden herhaald, en met tags groepeert u links waarvan u later gecombineerde statistieken wilt inzien.

Bekijk welke links bestaan en analyseer vervolgens het verkeer van één specifieke link.

sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docs

short-url:visits toont één rij per klik met de datum, de referrer en de user agent. De kolommen voor land en stad blijven leeg tenzij u een GEOLITE_LICENSE_KEY omgevingsvariabele instelt; dit is een gratis MaxMind-sleutel die Shlink gebruikt om de GeoLite2-database te downloaden. Zonder deze sleutel worden bezoeken nog steeds geregistreerd, maar ze worden simpelweg niet geografisch gelokaliseerd.

De webclient en QR-codes

De webclient bevindt zich nu op 127.0.0.1:8081 en vereist een eigen proxy-entry, of een SSH-tunnel als u deze liever niet publiceert. Bij het eerste laden wordt er gevraagd om een server-URL en een API-sleutel. Voer https://s.example.com in en de sleutel die u heeft gegenereerd. De client slaat beide op in de browseropslag en roept uw API rechtstreeks aan, waardoor er geen gegevens via derden verlopen. Het scheiden van de interface van de API is een patroon dat het vermelden waard is, omdat dit hetzelfde patroon is waarmee Halcyon een Jellyfin-bibliotheek presenteert als een videotheek uit de jaren 90 zonder de mediaserver erachter te wijzigen.

QR-codes vereisen geen configuratie. Voeg /qr-code toe aan een willekeurige korte URL en de API retourneert de afbeelding.

https://s.example.com/docs/qr-code?size=500&format=svg&margin=20

size is de breedte in pixels en accepteert waarden van 50 tot 1000, met 300 als standaardwaarde. format is png of svg. margin is de witruimte rondom de code in pixels; de uiteindelijke afbeelding heeft de afmeting plus tweemaal de marge. Voeg errorCorrection=Q toe voor een code die nog steeds scanbaar is wanneer deze klein wordt afgedrukt of gedeeltelijk is bedekt.

Operationeel houden

Een verkorter faalt geruisloos. De links stoppen met doorverwijzen en niemand informeert u, omdat de gebruiker die klikte aannam dat de link niet meer werkte. Richt een uptime-controle op een echte korte URL in plaats van op de startpagina en stel een waarschuwing in voor alles wat geen redirect is. Een zelfgehoste Uptime Kuma-instantie doet dit goed en kan controleren op een specifieke statuscode.

Maak een back-up van de database, niet van de container. Eén commando exporteert de data.

sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gz

Dat bestand in combinatie met uw compose-bestand bouwt de volledige service opnieuw op een nieuwe server. Elke app op de server heeft zijn eigen versie van dat paar nodig. Een fotobibliotheek is een lastig geval, omdat PhotoPrism en Immich beide de originelen op schijf bewaren naast rijen in een database; een dump alleen herstelt dus niets. Upgrades worden uitgevoerd via sudo docker compose pull gevolgd door sudo docker compose up -d, en Shlink voert bij het opstarten automatisch nieuwe migraties uit. Maak de dump voordat u de nieuwe versie ophaalt, aangezien een migratie niet ongedaan kan worden gemaakt.

FAQ

Shlink vergelijkt een korte code met het domein in de Host-header. Een proxy die zijn eigen naam of een intern adres meestuurt, zorgt ervoor dat Shlink naar die code zoekt onder een domein waar geen links aan gekoppeld zijn; daarom antwoordt het met een 404. Stel proxy_set_header Host $host; in het nginx location-blok in en herlaad de proxy. De links werken direct, zonder dat de container opnieuw hoeft te worden opgestart.

Heb ik Postgres nodig, of is SQLite voldoende?

SQLite is prima om Shlink uit te proberen en vereist geen tweede container. Stap over op Postgres voordat u belangrijke links publiceert, omdat het aantal bezoekersrijen bij elke klik groeit en SQLite schrijfacties serialiseert. Later overstappen betekent dat u uw links moet exporteren en opnieuw importeren, dus kiezen voor Postgres vanaf het begin bespaart u die migratie.

Kan ik een API-sleutel herstellen die ik vergeten ben te kopiëren?

Nee. Shlink slaat een hash van de sleutel op, dus api-key:list toont namen en statussen, maar nooit de waarde zelf. Genereer een vervangende sleutel met shlink api-key:generate, plak deze in de webclient en schakel vervolgens de oude uit met shlink api-key:disable zodat deze niet meer werkt.

Waarom zijn de landkolommen leeg in mijn bezoekersstatistieken?

Geolocatie vereist de GeoLite2-database, die Shlink alleen downloadt wanneer u een GEOLITE_LICENSE_KEY opgeeft. De sleutel is gratis verkrijgbaar bij MaxMind. Voeg deze toe aan de environment-sectie, maak de container opnieuw aan en nieuwe bezoeken worden gelokaliseerd. Bezoeken die vóór dat moment zijn geregistreerd, blijven leeg totdat u shlink visit:locate uitvoert.

Behoud het domein en verplaats de data. Maak een dump van de database met pg_dump, kopieer de dump en het compose-bestand naar de nieuwe server, start de stack en herstel vervolgens de dump in de lege database voordat er echt verkeer binnenkomt. Wijzig het DNS-record als laatste. De korte codes en hun bezoekgeschiedenis blijven behouden, omdat alles in de database staat.

#shlink#url-shortener#self-hosting#docker#postgres