Shlink selbst hosten: URL-Shortener mit Docker
Betreiben Sie Shlink 5.1 auf einem VPS: Kurzdomain, HTTPS, Postgres, API-Keys, Webclient, QR-Codes und Klickstatistiken mit Docker Compose einrichten.
Was Sie erstellen
Ein selbst gehosteter URL-Shortener ist ein kleiner Server, der einen langen Link in einen kurzen Link unter Ihrer Kontrolle umwandelt und jeden Klick darauf zählt. Shlink ist dafür die passende Wahl: Die Software ist Open Source, wird als Docker-Image veröffentlicht und erledigt die gesamte Aufgabe in einem Container plus Datenbank. In dieser Anleitung richten Sie Shlink auf einem VPS hinter einer echten Kurzdomain ein – mit HTTPS, einem API-Key, QR-Codes und Klickstatistiken.
Zwei Komponenten sorgen für das typische Verhalten eines kommerziellen Shorteners. Der API-Server beantwortet Weiterleitungen und verwaltet die Daten. Der Webclient ist eine separate statische Anwendung, die aus Ihrem Browser mit dieser API kommuniziert. Sie können beide Komponenten betreiben oder nur die API ausführen und sie über die Befehlszeile steuern.
Die hier verwendeten Versionsnummern entsprachen dem Stand von Juli 2026: Shlink 5.1 und shlink-web-client 4.8.
Zunächst eine kurze Domain auf den Server zeigen lassen
Die Domain ist der zentrale Bestandteil. s.example.com/abc123 ist der Link, den andere sehen. Wählen Sie daher einen kurzen Namen und legen Sie ihn fest, bevor Sie etwas installieren. Shlink speichert die Domain mit jeder Kurz-URL. Wenn Sie sie später ändern, funktionieren alle bereits ausgegebenen Links nicht mehr.
Erstellen Sie für die kurze Domain einen DNS-A-Record, der auf die öffentliche IPv4-Adresse Ihres VPS zeigt. Fügen Sie außerdem einen AAAA-Record hinzu, wenn der Server IPv6 verwendet. Prüfen Sie anschließend, ob die Domain aufgelöst wird, bevor Sie fortfahren.
dig +short s.example.com ADie Ausgabe muss die Adresse Ihres Servers sein. Ist sie leer, wurde der Record noch nicht propagiert. Dann schlagen alle späteren Schritte auf schwer nachvollziehbare Weise fehl, weil für einen Namen, der nicht aufgelöst wird, kein TLS-Zertifikat (Transport Layer Security) ausgestellt werden kann.
Die Compose-Datei
Shlink benötigt eine Datenbank. SQLite eignet sich für einen Test. Für alles, was Sie dauerhaft betreiben möchten, ist Postgres jedoch die richtige Wahl, weil sich die Besuchsdatensätze ansammeln und Postgres Indizes sowie gleichzeitige Schreibvorgänge besser verarbeitet. Legen Sie Folgendes in /opt/shlink/compose.yaml ab.
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 veröffentlichten Ports werden an 127.0.0.1 gebunden. Bis der Reverse Proxy im nächsten Abschnitt eingerichtet ist, ist daher nichts aus dem Internet erreichbar. Docker schreibt seine eigenen Weiterleitungsregeln vor der Host-Firewall. Eine einfache 8080:8080-Zeile würde die Anwendung daher auch auf einem System veröffentlichen, dessen Firewall scheinbar geschlossen ist. Die Bindung an die Loopback-Adresse verhindert das. Dasselbe Muster gilt für jede Anwendung, die Sie auf diese Weise betreiben. Weitere Informationen finden Sie im Leitfaden zu Docker Compose auf einem VPS.
Das Datenbankpasswort stammt aus einer .env-Datei neben der Compose-Datei. Es gelangt dadurch nicht in die YAML-Datei.
sudo mkdir -p /opt/shlink
printf 'DB_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee /opt/shlink/.env
sudo chmod 600 /opt/shlink/.envStarten Sie den Stack und überwachen Sie, wie die API verfügbar wird.
cd /opt/shlink
sudo docker compose up -d
sudo docker compose logs -f shlinkBeim ersten Start werden die Datenbankmigrationen ausgeführt. Dieser Start dauert daher länger als spätere Starts. Sobald der Dienst stabil läuft, prüfen Sie, ob er lokal antwortet.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/rest/healthEin 200 bedeutet, dass die API aktiv ist und die Datenbankverbindung funktioniert. Ein 500 an dieser Stelle betrifft fast immer die Datenbank: Der DB_PASSWORD in .env stimmt nicht mit dem Wert überein, mit dem Postgres erstellt wurde. Das Postgres-Image liest POSTGRES_PASSWORD nur, wenn es ein leeres Datenverzeichnis initialisiert. Eine spätere Änderung des Passworts hat keine Wirkung, bis Sie das Volume entfernen und den Stack erneut starten.
HTTPS davor terminieren
Shlink stellt unverschlüsseltes HTTP auf Port 8080 bereit. TLS gehört in einen Reverse Proxy. Entscheidend ist, dass der ursprüngliche Hostname weitergegeben wird. Shlink ermittelt anhand des Headers Host, zu welcher Domain ein Kurzcode gehört. Wenn ein Proxy diesen Header umschreibt, liefern vorhandene Links eine 404-Antwort. Außerdem werden die Besuchsstatistiken der falschen Domain zugeordnet.
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;
}
}Stellen Sie anschließend das Zertifikat aus. Die vollständige Anleitung einschließlich des Timers für die Erneuerung finden Sie im Certbot-Leitfaden für nginx unter Ubuntu 24.04.
sudo certbot --nginx -d s.example.comIS_HTTPS_ENABLED: "true" in der Compose-Datei bewirkt, dass Shlink in den zurückgegebenen Kurz-URLs https:// ausgibt. Dadurch wird TLS nicht automatisch aktiviert. Lassen Sie false hinter einem HTTPS-Proxy. Dann ist jeder Link, den die API zurückgibt, ein http://-Link, der anschließend weiterleitet. Das verursacht einen zusätzlichen Roundtrip und wirkt im Webclient fehlerhaft.
Einen API-Schlüssel erstellen
Ohne Schlüssel kann kein Client mit der API kommunizieren. Erstellen Sie einen Schlüssel über die CLI im Container.
sudo docker compose exec shlink shlink api-key:generate --name "web client"Der Befehl gibt den Schlüssel nur einmal aus. Kopieren Sie ihn jetzt, weil er gehasht gespeichert wird und nicht erneut angezeigt werden kann. shlink api-key:list zeigt die Namen und an, ob die einzelnen Schlüssel aktiviert sind, niemals den Schlüssel selbst. Widerrufen Sie einen Schlüssel mit shlink api-key:disable und dem Namen.
Jeder REST-Aufruf übermittelt den Schlüssel in einem X-Api-Key-Header.
curl -H "X-Api-Key: YOUR_KEY" https://s.example.com/rest/v3/short-urlsEin JSON-Objekt mit einem shortUrls-Schlüssel bedeutet, dass der Schlüssel funktioniert. Ein 401 mit INVALID_API_KEY bedeutet, dass der Schlüssel falsch, deaktiviert oder abgelaufen ist.
Kurzlinks über die Befehlszeile erstellen
Die CLI ist der schnellste Weg, Links zu erstellen. Außerdem lässt sie sich gut in Skripte integrieren.
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 erstellt einen lesbaren Link statt eines generierten Codes. Slugs sind pro Domain eindeutig. Ein zweiter Versuch mit einem bereits vergebenen Slug schlägt daher fehl, statt den ersten Link stillschweigend zu überschreiben. --tag kann wiederholt werden. Über Tags gruppieren Sie Links, für die Sie später gemeinsame Statistiken abrufen möchten.
Listen Sie zunächst die vorhandenen Links auf. Sehen Sie sich anschließend den Netzwerkverkehr eines Links an.
sudo docker compose exec shlink shlink short-url:list
sudo docker compose exec shlink shlink short-url:visits docsshort-url:visits gibt für jeden Klick eine Zeile mit Datum, Referrer und User-Agent aus. Die Spalten für Land und Stadt bleiben leer, sofern Sie keine Umgebungsvariable GEOLITE_LICENSE_KEY setzen. Dabei handelt es sich um einen kostenlosen MaxMind-Schlüssel, den Shlink zum Herunterladen der GeoLite2-Datenbank verwendet. Ohne diesen Schlüssel werden Besuche weiterhin erfasst, aber nicht geografisch zugeordnet.
Der Webclient und QR-Codes
Der Webclient ist jetzt unter 127.0.0.1:8081 erreichbar und benötigt einen eigenen Proxy-Eintrag oder einen SSH-Tunnel, wenn Sie ihn nicht veröffentlichen möchten. Beim ersten Aufruf werden eine Server-URL und ein API-Schlüssel abgefragt. Geben Sie https://s.example.com und den von Ihnen erzeugten Schlüssel ein. Der Client speichert beides im Browserspeicher und ruft Ihre API direkt auf. Dadurch werden keine Daten über Dritte übertragen. Die Trennung von Oberfläche und API ist ein beachtenswertes Muster. Es ermöglicht auch, dass Halcyon eine Jellyfin-Mediathek wie eine Videothek aus den 1990er-Jahren aussehen lässt, ohne den dahinterliegenden Medienserver zu ändern.
QR-Codes benötigen keine Konfiguration. Hängen Sie /qr-code an eine beliebige kurze URL an. Die API gibt das Bild zurück.
https://s.example.com/docs/qr-code?size=500&format=svg&margin=20size ist die Breite in Pixeln und akzeptiert Werte von 50 bis 1000. Der Standardwert ist 300. format ist png oder svg. margin ist der freie Rand um den Code in Pixeln. Das fertige Bild ist so groß wie die Größe plus zweimal der Rand. Fügen Sie errorCorrection=Q für einen Code hinzu, der auch bei kleinem Ausdruck oder teilweiser Abdeckung noch gescannt werden kann.
Lassen Sie den Dienst in Betrieb
Ein URL-Shortener fällt möglicherweise unbemerkt aus. Die Links leiten nicht mehr weiter, und niemand informiert Sie darüber, weil die Person, die auf den Link geklickt hat, davon ausgeht, dass der Link defekt ist. Richten Sie die Uptime-Prüfung auf eine tatsächlich verkürzte URL und nicht auf die Startseite. Lösen Sie einen Alarm aus, wenn die Antwort keine Weiterleitung ist. Eine selbst gehostete Uptime-Kuma-Instanz eignet sich dafür gut und kann auf einen bestimmten Statuscode prüfen.
Sichern Sie die Datenbank, nicht den Container. Ein Befehl erstellt einen Dump.
sudo docker compose exec -T database pg_dump -U shlink shlink | gzip > shlink-$(date +%F).sql.gzDiese Datei und Ihre Compose-Datei reichen aus, um den gesamten Dienst auf einem neuen Server wiederherzustellen. Jede Anwendung auf dem Server benötigt eine eigene Version dieses Dateipaars. Eine Fotobibliothek ist dabei ein Sonderfall, weil PhotoPrism und Immich die Originaldateien sowohl auf der Festplatte als auch als Datensätze in einer Datenbank speichern. Ein Datenbank-Dump allein stellt daher nichts wieder her. Upgrades werden mit sudo docker compose pull eingeleitet und mit sudo docker compose up -d abgeschlossen. Shlink führt beim Start alle neuen Migrationen aus. Erstellen Sie den Dump vor dem Pull, weil eine Migration nicht zurückgerollt werden kann.
FAQ
Warum liefern meine Kurzlinks nach dem Hinzufügen eines Reverse Proxy 404?
Shlink ordnet einen Kurzcode der Domain im Header Host zu. Ein Proxy, der seinen eigenen Namen oder eine interne Adresse sendet, veranlasst Shlink, den Code unter einer Domain ohne Links zu suchen. Daher wird 404 zurückgegeben. Setzen Sie proxy_set_header Host $host; im nginx-Location-Block und laden Sie den Proxy neu. Die Links funktionieren sofort wieder, ohne dass der Container neu gestartet werden muss.
Benötige ich Postgres, oder reicht SQLite aus?
SQLite eignet sich für erste Tests mit Shlink und benötigt keinen zweiten Container. Wechseln Sie zu Postgres, bevor Sie wichtige Links veröffentlichen, weil die Besuchsdatensätze mit jedem Klick wachsen und SQLite Schreibvorgänge serialisiert. Ein späterer Wechsel erfordert den Export und Reimport Ihrer Links. Wenn Sie sich von Anfang an für Postgres entscheiden, sparen Sie sich diese Migration.
Kann ich einen API-Schlüssel wiederherstellen, den ich nicht kopiert habe?
Nein. Shlink speichert einen Hash des Schlüssels. api-key:list zeigt daher Namen und Status, aber niemals den Wert. Erzeugen Sie mit shlink api-key:generate einen Ersatzschlüssel, fügen Sie ihn in den Webclient ein und deaktivieren Sie den alten Schlüssel mit shlink api-key:disable, damit er nicht mehr funktioniert.
Warum sind die Spalten für Länder in meinen Besuchsstatistiken leer?
Die Geolokalisierung benötigt die GeoLite2-Datenbank. Shlink lädt sie nur herunter, wenn Sie einen GEOLITE_LICENSE_KEY angeben. Den Schlüssel erhalten Sie kostenlos von MaxMind. Fügen Sie ihn zum Environment-Abschnitt hinzu und erstellen Sie den Container neu. Neue Besuche werden dann geolokalisiert. Bei Besuchen, die zuvor erfasst wurden, bleiben die Felder leer, bis Sie shlink visit:locate ausführen.
Wie verschiebe ich Shlink auf einen anderen Server?
Behalten Sie die Domain bei und verschieben Sie die Daten. Sichern Sie die Datenbank mit pg_dump, kopieren Sie das Dumpfile und die Compose-Datei auf den neuen Server und starten Sie den Stack. Stellen Sie das Dumpfile anschließend in der leeren Datenbank wieder her, bevor echter Netzwerkverkehr eintrifft. Ändern Sie den DNS-Eintrag zuletzt. Die Kurzcodes und ihre Besuchshistorie bleiben erhalten, weil alle Daten in der Datenbank gespeichert sind.