Zelfgehoste URL-verkorter met Shlink en Docker
Bouw een eigen URL-verkorter met Shlink 5.1, Docker Compose en Postgres. Configureer DNS, API-sleutels, QR-codes en klikstatistieken op uw VPS.
Wat u bouwt
Een zelfgehoste URL-verkorter is een kleine server die een lange link omzet in een korte link die u zelf beheert. De server telt ook elke klik. Shlink is hiervoor de juiste keuze: het is open source, wordt als Docker-image uitgebracht en voert de volledige taak uit in één container met een database. In deze handleiding installeert u Shlink op een VPS achter een echt kort domein, met HTTPS, een API-sleutel, QR-codes en klikstatistieken.
Twee onderdelen zorgen ervoor dat de oplossing aanvoelt als een commerciële URL-verkorter. De API-server verwerkt omleidingen en beheert de gegevens. De webclient is een afzonderlijke statische applicatie die vanuit uw browser met die API communiceert. U kunt beide uitvoeren, of alleen de API uitvoeren en deze vanaf de opdrachtregel gebruiken.
De versienummers in deze handleiding waren actueel in juli 2026: Shlink 5.1 en shlink-web-client 4.8.
Wijs eerst een kort domein aan de server toe
Het domein is het product. s.example.com/abc123 is de link die mensen zien. Kies daarom een korte naam voordat u iets installeert. Shlink slaat het domein bij elke korte URL op. Als u het later wijzigt, werken alle links die u al hebt gedeeld niet meer.
Maak één DNS A-record voor het korte domein. Laat dit record verwijzen naar het openbare IPv4-adres van uw VPS. Voeg ook een AAAA-record toe als de server IPv6 heeft. Controleer daarna of het domein wordt omgezet voordat u verdergaat.
dig +short s.example.com ADe uitvoer moet het adres van uw server zijn. Als de uitvoer leeg is, is het record nog niet gepropageerd. Elke volgende stap mislukt dan op een onduidelijke manier, omdat er geen TLS-certificaat (transport layer security) kan worden uitgegeven voor een naam die niet wordt omgezet.
Het compose-bestand
Shlink heeft een database nodig. SQLite is geschikt voor een test, maar Postgres is de juiste keuze voor alles wat u wilt behouden, omdat het aantal bezoekrecords toeneemt en Postgres indexen en gelijktijdige schrijfbewerkingen beter verwerkt. 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 zijn gebonden aan 127.0.0.1. Daardoor is er niets vanaf internet bereikbaar totdat de reverse proxy in de volgende sectie actief is. Docker schrijft zijn eigen forwardingregels vóór de firewall van de host. Een gewone regel met 8080:8080 zou de applicatie daardoor beschikbaar maken, zelfs op een systeem waarvan de firewall gesloten lijkt. Door aan het loopbackadres te binden voorkomt u dat. Hetzelfde patroon geldt voor elke applicatie die u op deze manier uitvoert. Dit wordt uitgebreider behandeld in de handleiding voor Docker Compose op een VPS.
Het databasewachtwoord komt uit een bestand .env naast het compose-bestand. Daardoor komt het niet in de YAML terecht.
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.envStart de service en monitor het opkomen van de API.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkBij de eerste start worden de databasemigraties uitgevoerd. Daardoor duurt deze start langer dan latere starts. Controleer na het opstarten of de service lokaal antwoord geeft.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthEen 200 betekent dat de API actief is en dat de databaseverbinding werkt. Een 500 hier wordt vrijwel altijd door de database veroorzaakt. De DB_PASSWORD in .env komt niet overeen met de waarde waarmee Postgres is aangemaakt, omdat de Postgres-image POSTGRES_PASSWORD alleen leest wanneer deze een lege gegevensdirectory initialiseert. Het later wijzigen van het wachtwoord heeft geen effect totdat u het volume verwijdert en opnieuw start.
HTTPS beëindigen vóór de applicatie
Shlink biedt onversleutelde HTTP aan op poort 8080. TLS hoort in een reverse proxy te worden afgehandeld. De instelling die van belang is, is het doorgeven van de oorspronkelijke hostnaam. Shlink bepaalt bij welk domein een korte code hoort door de header Host te lezen. Als een proxy deze header herschrijft, retourneert Shlink 404-responses voor bestaande links. Ook worden bezoekstatistieken dan aan het verkeerde domein 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;
}
}Geef daarna het certificaat uit. De volledige procedure, inclusief de timer voor vernieuwing, staat in de Certbot-handleiding voor nginx op Ubuntu 24.04.
sudo certbot --nginx -d s.example.comIS_HTTPS_ENABLED: "true" in het compose-bestand zorgt ervoor dat Shlink https:// opneemt in de korte URL's die het retourneert. Hiermee wordt TLS niet automatisch ingeschakeld. Laat false achter een HTTPS-proxy staan. Elke link die de API retourneert, is dan een http://-link die vervolgens wordt doorgestuurd. Dat kost een extra retourronde en ziet er verkeerd uit in de webclient.
Een API-sleutel maken
Zonder sleutel kan niets met de API communiceren. Genereer een sleutel met de CLI in de container.
sudo docker compose exec shlink shlink api-key:generate --name "web client"De opdracht geeft de sleutel eenmalig weer. Kopieer deze nu, omdat de sleutel gehasht wordt opgeslagen en niet opnieuw kan worden weergegeven. Met shlink api-key:list ziet u 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-urlsEen JSON-object met een shortUrls-sleutel betekent dat de sleutel werkt. Een 401 met INVALID_API_KEY betekent dat de sleutel onjuist of uitgeschakeld is, of dat de vervaldatum is verstreken.
Korte links maken vanaf de opdrachtregel
De CLI is de snelste manier om links te maken en werkt goed met 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 geeft u een leesbare link in plaats van een gegenereerde code. Slugs zijn uniek per domein. Daarom mislukt een tweede poging met een slug die al in gebruik is, in plaats van dat de eerste link stilzwijgend wordt overschreven. --tag kan worden herhaald. Met tags groepeert u links waarvoor u later gecombineerde statistieken wilt bekijken.
Geef eerst weer wat er bestaat en bekijk daarna het verkeer van één link.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits geeft één regel per klik weer, met de datum, de verwijzer en de user agent. De kolommen voor land en plaats blijven leeg tenzij u een omgevingsvariabele GEOLITE_LICENSE_KEY 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 wordt hun locatie niet bepaald.
De webclient en QR-codes
De webclient is nu beschikbaar op 127.0.0.1:8081 en heeft een eigen proxyvermelding nodig. U kunt ook een SSH-tunnel gebruiken als u deze liever niet openbaar maakt. Bij de eerste keer laden vraagt de client om een server-URL en een API-sleutel. Voer https://s.example.com en de sleutel die u hebt gegenereerd in. De client bewaart beide in de browseropslag en maakt rechtstreeks verbinding met uw API. Er gaat dus geen data via een andere partij.
Voor QR-codes is geen configuratie nodig. Voeg /qr-code toe aan een willekeurige korte URL. De API retourneert dan de afbeelding.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size is de breedte in pixels en accepteert waarden van 50 tot 1000. De standaardwaarde is 300. format is png of svg. margin is de vrije ruimte rond de code in pixels. De uiteindelijke afbeelding is gelijk aan de grootte plus tweemaal de marge. Voeg errorCorrection=Q toe voor een code die ook wordt gescand wanneer deze klein wordt afgedrukt of gedeeltelijk wordt afgedekt.
Houd de service actief
Een URL-shortener kan ongemerkt uitvallen. De links verwijzen dan niet meer door en niemand meldt dit, omdat de persoon die op de link klikte ervan uitging dat de link niet meer werkte. Richt een uptimecontrole op een echte verkorte URL in plaats van op de startpagina en geef een waarschuwing voor elke respons die geen doorverwijzing 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. Met één opdracht maakt u hiervan een dump.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzMet dat bestand en uw compose-bestand kunt u de volledige service op een nieuwe server opnieuw opbouwen. Upgrades bestaan uit sudo docker compose pull, gevolgd door sudo docker compose up -d. Shlink voert bij het opstarten nieuwe migraties uit. Maak de dump voordat u de nieuwe versie ophaalt, omdat een migratie niet kan worden teruggedraaid.
FAQ
Waarom geven mijn korte links 404 terug nadat ik een reverse proxy toevoeg?
Shlink vergelijkt een verkorte code met het domein in de header Host. Als een proxy zijn eigen naam of een intern adres doorstuurt, zoekt Shlink die code onder een domein zonder links. Daarom geeft Shlink 404 terug. Stel proxy_set_header Host $host; in het nginx-locationblok in en laad de proxy opnieuw. De links werken meteen weer, zonder de container opnieuw te starten.
Heb ik Postgres nodig, of is SQLite voldoende?
SQLite is geschikt om Shlink uit te proberen en vereist geen tweede container. Stap over op Postgres voordat u belangrijke links publiceert, omdat het aantal bezoekrecords bij elke klik groeit en SQLite schrijfbewerkingen serialiseert. Als u later overstapt, moet u uw links exporteren en opnieuw importeren. Door vanaf het begin Postgres te kiezen, voorkomt 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. Daarom toont api-key:list wel namen en de status, maar nooit de waarde. Genereer met shlink api-key:generate een vervangende sleutel, plak deze in de webclient en schakel de oude sleutel uit met shlink api-key:disable. Daarna werkt de oude sleutel niet meer.
Waarom zijn de landkolommen leeg in mijn bezoekstatistieken?
Geolocatie vereist de GeoLite2-database. Shlink downloadt deze alleen als u een GEOLITE_LICENSE_KEY opgeeft. De sleutel is gratis verkrijgbaar bij MaxMind. Voeg deze toe aan de omgevingssectie en maak de container opnieuw aan. Nieuwe bezoeken krijgen dan een locatie. Bezoeken die daarvoor zijn geregistreerd, blijven leeg totdat u shlink visit:locate uitvoert.
Hoe verplaats ik Shlink naar een andere server?
Behoud het domein en verplaats de gegevens. Maak met pg_dump een database-dump, kopieer de dump en het compose-bestand naar de nieuwe server, start de stack en herstel de dump vervolgens in de lege database voordat er echt verkeer binnenkomt. Wijzig het DNS-record als laatste. De verkorte codes en hun bezoekgeschiedenis blijven behouden, omdat alles in de database staat.