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.
API-Schlüssel erstellen
Ohne Schlüssel kann kein Client mit der API kommunizieren. Erstellen Sie den 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 und später nicht erneut angezeigt werden kann. shlink api-key:list zeigt die Namen und an, ob die einzelnen Schlüssel aktiviert sind, aber niemals den Schlüssel selbst. Widerrufen Sie einen Schlüssel mit shlink api-key:disable und dem Namen.
Jeder REST-Aufruf übergibt 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, dessen type den Wert https://shlink.io/api/error/invalid-api-key hat und dessen Titel Invalid API key lautet, 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 Laden fordert er eine Server-URL und einen API-Schlüssel an. Geben Sie https://s.example.com und den erstellten Schlüssel ein. Der Client speichert beides im Browserspeicher und ruft Ihre API direkt auf. Dadurch werden keine Daten über andere Dienste übertragen. Die Trennung der Oberfläche von der API ist ein beachtenswertes Muster, weil es auch Halcyon ermöglicht, eine Jellyfin-Bibliothek wie eine Videothek aus den 1990er-Jahren darzustellen, ohne den dahinterliegenden Medienserver zu ändern.
QR-Codes werden vom Webclient und nicht von der API erzeugt. Shlink 5 hat den alten Trick entfernt, /qr-code an eine Kurz-URL anzuhängen. Diese Adresse liefert daher kein Bild mehr. Stattdessen zeichnet der Webclient den Code in Ihrem Browser, ohne eine Anfrage an Shlink zu senden. Öffnen Sie bei einem beliebigen Link in der Liste der Kurz-URLs die QR-Code-Option, legen Sie Größe, Format und Rand fest und laden Sie das Ergebnis herunter. Erhöhen Sie die Fehlerkorrekturstufe, wenn der Code auch bei kleinem Ausdruck oder teilweiser Abdeckung noch gescannt werden soll. Im selben Dialog können Sie auch Farben und ein Logo festlegen.
Den Dienst dauerhaft ausführen
Ein URL-Verkürzer fällt unbemerkt aus. Die Links leiten nicht mehr weiter, und niemand informiert Sie darüber, weil die Person, die den Link angeklickt hat, von einem defekten Link ausgeht. Überwachen Sie eine echte Kurz-URL statt der Startseite und 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 überwachen.
Sichern Sie die Datenbank und 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 zusammen mit Ihrer Compose-Datei reicht aus, um den gesamten Dienst auf einem neuen Server wiederherzustellen. Jede Anwendung auf dem Server benötigt eine eigene Version dieses Paars. Eine Fotobibliothek ist dabei der Sonderfall, weil PhotoPrism und Immich die Originale sowohl auf der Festplatte als auch als Datensätze in einer Datenbank speichern. Ein Datenbank-Dump stellt daher Konten und Einstellungen wieder her, aber kein einziges Foto. Upgrades bestehen aus sudo docker compose pull gefolgt von sudo docker compose up -d. Shlink führt beim Start alle neuen Migrationen aus. Erstellen Sie den Dump vor dem Abruf der neuen Version, 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.