Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Docker 21.09.2026 · 22 min Lesezeit

Shlink selbst hosten: URL-Kürzer mit Docker Compose und MariaDB

Ein eigener URL-Kürzer unter der eigenen Domain hält Kurzlinks, Zieladressen und Statistiken im Haus. Diese Anleitung baut Shlink 5.1.6 mit Docker Compose und MariaDB 11 auf, prüft den Dienst über den Health-Endpunkt und die REST-API und zeigt Backup, Update, Reverse-Proxy-Betrieb und die typischen Fehler samt Diagnose.

Helle Illustration mit der Überschrift Eigener URL-Kürzer mit Docker, drei Karten für Eigene Domain, Statistiken und REST-API sowie einem abstrakten Dashboard und Terminalfenster KI-generiert

Shlink ist ein quelloffener, selbst gehosteter URL-Kürzer. Er nimmt eine lange Zieladresse entgegen und liefert dafür einen kurzen Link unter einer Domain aus, die Ihnen selbst gehört. Der Dienst ist in PHP geschrieben, steht unter der MIT-Lizenz und bringt eine vollständige REST-API sowie eine Kommandozeile mit. Für den Betrieb im Unternehmen ist das der entscheidende Unterschied zu einem kostenlosen Kürzungsdienst aus dem Netz: Sie entscheiden, welche Kurzlinks existieren, wohin sie zeigen, wie lange sie gültig bleiben und wer die Zugriffsstatistiken sieht. Ein fremder Anbieter kann Ihre Links abschalten, mit Werbung unterlegen, das Preismodell ändern oder komplett verschwinden. Bereits gedruckte Flyer, QR-Codes auf Verpackungen und Links in E-Mail-Signaturen laufen dann ins Leere.

Diese Anleitung führt einen kompletten Shlink-Stack mit Docker Compose und MariaDB von der leeren Maschine bis zum funktionierenden Dienst. Der beschriebene Stack wurde am 21.09.2026 mit den Images shlinkio/shlink:5.1.6 und mariadb:11 tatsächlich aufgebaut und geprüft. Alle Befehlsausgaben, die hier als beobachtet bezeichnet werden, stammen aus diesem Test. Punkte, die nur aus der Herstellerdokumentation stammen und im Test nicht durchgespielt wurden, sind ausdrücklich als solche gekennzeichnet. Sie bekommen damit eine ehrliche Trennung zwischen dem, was nachweislich läuft, und dem, was Sie für den Produktivbetrieb noch selbst verifizieren müssen.

Shlink deckt den klassischen Anwendungsfall eines Unternehmens-Kürzers vollständig ab. Sie betreiben eine kurze Domain oder Subdomain, etwa für Kampagnenlinks, QR-Codes auf Druckerzeugnissen, Links in Support-Tickets oder für schwer teilbare Deep-Links in interne Anwendungen. Jeder Kurzlink lässt sich über die API anlegen, mit Schlagwörtern versehen, zeitlich begrenzen und wieder deaktivieren. Die Besuchszählung findet in Ihrer eigenen Datenbank statt, nicht bei einem Dritten.

Die Stärken im Überblick:

  • Vollständige REST-API, mit der sich die Kurzlinkerzeugung in bestehende Prozesse und Skripte einbauen lässt.
  • Kommandozeilenwerkzeug im Container für Verwaltungsaufgaben wie das Anlegen von API-Schlüsseln.
  • Mehrere Datenbank-Backends: laut Dokumentation werden MySQL, MariaDB, PostgreSQL, Microsoft SQL und SQLite unterstützt.
  • Konfiguration beim Docker-Betrieb vollständig über Umgebungsvariablen, also gut versionierbar und automatisierbar.
  • Feingranulare Rollen für API-Schlüssel, um den Zugriff einzelner Integrationen einzuschränken.

Die Grenzen sollten Sie vor der Einführung kennen:

  • Shlink ist ein Dienst ohne eingebaute Weboberfläche. Die grafische Verwaltung ist ein eigenes Projekt namens shlink-web-client und muss separat betrieben werden. Im Test wurde ausschließlich die API genutzt, die Weboberfläche wurde nicht aufgesetzt.
  • Die Geolokalisierung von Besuchen benötigt laut Dokumentation einen GeoLite2-Lizenzschlüssel. Ohne diesen Schlüssel ist die Standortermittlung vollständig deaktiviert. Das wurde nicht getestet.
  • SQLite ist laut Dokumentation ausdrücklich nur für Test- und Automatisierungszwecke vorgesehen und für den Produktivbetrieb nicht unterstützt. Deshalb verwendet diese Anleitung MariaDB.
  • Die Erzeugung von QR-Codes durch Shlink gilt laut Dokumentation ab Version 4.5.0 als veraltet. Wer QR-Codes braucht, sollte sie außerhalb von Shlink erzeugen.
  • Ein Kürzer ist ein Umleitungsdienst und damit grundsätzlich für Missbrauch attraktiv. Wer die API breit zugänglich macht, sollte damit rechnen, dass Kurzlinks für Phishing zweckentfremdet werden.

Voraussetzungen und Ressourcenbedarf

Für den in dieser Anleitung beschriebenen Weg brauchen Sie einen Linux-Server mit einer aktuellen Docker-Installation samt Compose-Plugin. Der Befehl docker compose version muss eine Version ausgeben. Läuft stattdessen ein Fehler auf, fehlt das Plugin und muss nachinstalliert werden.

Fachlich kommen diese Punkte dazu:

  • Eine eigene Domain oder Subdomain, die möglichst kurz ist. Der Sinn eines Kürzers geht verloren, wenn die Basisdomain selbst lang ist.
  • Ein DNS-A-Eintrag beziehungsweise AAAA-Eintrag, der diese Domain auf den Server zeigt. Ohne funktionierende Namensauflösung können Sie später kein Zertifikat beziehen.
  • Ein TLS-Zertifikat, in der Praxis über einen Reverse Proxy mit automatischer Ausstellung. Shlink selbst terminiert kein TLS.
  • Eine Datenbank. Diese Anleitung nutzt MariaDB im selben Compose-Stack. Für größere Installationen ist eine getrennt betriebene, bereits gesicherte Datenbank die bessere Wahl.
  • Plattenplatz. Die beiden Images belegen zusammen einige hundert Megabyte. Der laufende Verbrauch wird von den gespeicherten Besuchen bestimmt, nicht von den Kurzlinks selbst. Das ist ein Erfahrungswert und keine Herstellerangabe: Planen Sie für den Anfang großzügig einige Gigabyte im Datenbank-Volume ein und beobachten Sie das Wachstum.

Zum Arbeitsspeicher macht die Dokumentation eine konkrete Angabe: Die Umgebungsvariable MEMORY_LIMIT begrenzt den Speicher je Shlink-Prozess und ist mit 512 Megabyte vorbelegt. Wie viele Prozesse laufen, hängt laut Dokumentation von WEB_WORKER_NUM ab, das standardmäßig der Anzahl der verfügbaren CPU-Kerne entspricht. Auf einem Server mit vielen Kernen sollten Sie diesen Wert also bewusst setzen, statt ihn dem Zufall der Hardware zu überlassen.

Die .env-Datei

Legen Sie ein eigenes Verzeichnis für den Stack an und darin eine Datei .env. Sie enthält die Werte, die sich zwischen Umgebungen unterscheiden und die nicht in ein Repository gehören. Alle Werte unten sind ausdrücklich Platzhalter und müssen ersetzt werden.

# Passwort für den Datenbankbenutzer, bitte ersetzen
SHLINK_DB_PASSWORD=BITTE-HIER-EIN-LANGES-ZUFALLSPASSWORT-EINSETZEN

# Erster API-Schluessel, bitte ersetzen
SHLINK_INITIAL_API_KEY=BITTE-HIER-EINEN-ZUFALLSWERT-EINSETZEN

# Die eigene Kurzdomain ohne Schema und ohne Schraegstrich
SHLINK_DEFAULT_DOMAIN=kurz.example.com

Brauchbare Zufallswerte erzeugen Sie direkt auf dem Server, statt sich selbst Zeichenketten auszudenken:

# Zwei unabhaengige Zufallswerte erzeugen
openssl rand -base64 33
openssl rand -hex 32

Zu den Dateirechten: Die .env enthält zwei Geheimnisse, das Datenbankpasswort und den initialen API-Schlüssel. Beschränken Sie den Zugriff und stellen Sie sicher, dass die Datei nie in die Versionsverwaltung gelangt.

# Nur der Eigentuemer darf lesen und schreiben
chmod 600 .env

# Datei dauerhaft aus der Versionsverwaltung heraushalten
echo ".env" >> .gitignore

Ein Hinweis, der in der Praxis oft untergeht: Ein API-Schlüssel in einer Umgebungsvariablen ist für jeden lesbar, der docker inspect auf den Container ausführen darf, und er steht im Klartext in Ihrer Sicherung des Stack-Verzeichnisses. Behandeln Sie ihn wie ein Passwort. Das ist eine begründete Empfehlung dieser Anleitung und keine Herstelleraussage.

Die compose.yaml im Detail

Die folgende Datei beschreibt den kompletten Stack. Genau diese Struktur wurde im Test verwendet.

services:
  shlink_db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${SHLINK_DB_PASSWORD}
      MARIADB_DATABASE: shlink
      MARIADB_USER: shlink
      MARIADB_PASSWORD: ${SHLINK_DB_PASSWORD}
    volumes:
      - shlink_db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  shlink:
    image: shlinkio/shlink:5.1.6
    restart: unless-stopped
    depends_on:
      shlink_db:
        condition: service_healthy
    environment:
      DEFAULT_DOMAIN: ${SHLINK_DEFAULT_DOMAIN}
      IS_HTTPS_ENABLED: "false"
      DB_DRIVER: maria
      DB_HOST: shlink_db
      DB_NAME: shlink
      DB_USER: shlink
      DB_PASSWORD: ${SHLINK_DB_PASSWORD}
      INITIAL_API_KEY: ${SHLINK_INITIAL_API_KEY}
      TIMEZONE: Europe/Berlin
    ports:
      - "127.0.0.1:8085:8080"

volumes:
  shlink_db_data:

Der Datenbankdienst im Einzelnen:

  • image: mariadb:11 bindet die Hauptversion fest. Damit bekommen Sie Sicherheitsaktualisierungen innerhalb der Version 11, aber keinen ungewollten Sprung auf eine neue Hauptversion mit anderem Migrationsverhalten.
  • MARIADB_DATABASE, MARIADB_USER, MARIADB_PASSWORD legen beim allerersten Start Datenbank und Benutzer an. Bei einem späteren Start mit bereits gefülltem Volume werden diese Variablen ignoriert. Wer nachträglich das Passwort ändern will, muss es in der Datenbank selbst ändern.
  • volumes bindet ein benanntes Volume auf /var/lib/mysql. Das ist der Ort, an dem sämtliche Kurzlinks und Besuchsdaten liegen. Ohne dieses Volume wären alle Daten beim Entfernen des Containers weg.
  • healthcheck ruft das im Image mitgelieferte Skript healthcheck.sh mit den Schaltern --connect und --innodb_initialized auf. Der erste prüft, ob eine Verbindung möglich ist, der zweite, ob die Speicher-Engine wirklich fertig initialisiert ist. Im Test erreichte der Container damit zuverlässig den Status healthy.
  • interval, timeout, retries mit 10 Sekunden, 5 Sekunden und 10 Versuchen ergeben ein Zeitfenster von reichlich anderthalb Minuten, in dem die Datenbank bereit werden darf. Im Test war das deutlich mehr als nötig.

Der Shlink-Dienst im Einzelnen:

  • image: shlinkio/shlink:5.1.6 ist ein fester Versions-Tag. Laut Dokumentation entspricht ein voll ausgeschriebener Tag genau dieser Shlink-Version, während stable immer auf die neueste stabile Version zeigt. Für einen kontrollierten Betrieb ist der feste Tag die richtige Wahl, weil ein Neustart dann nicht ungeplant eine neue Hauptversion samt Datenbankmigration zieht.
  • depends_on mit condition: service_healthy ist der wichtigste Punkt der Datei. Shlink startet damit erst, wenn der Healthcheck der Datenbank erfolgreich war. Ohne diese Bedingung würde Compose den Shlink-Container starten, sobald der Datenbankcontainer existiert, nicht wenn er bereit ist.
  • DEFAULT_DOMAIN ist laut Dokumentation die Standard-Kurzdomain dieser Instanz. Dieser Wert bestimmt, wie die von der API zurückgegebenen Kurzlinks aussehen. Details dazu im Fehlerteil, denn genau hier liegt die häufigste Stolperfalle.
  • IS_HTTPS_ENABLED steht in dieser Datei auf false, weil der Test bewusst lokal ohne TLS lief. Für den echten Betrieb muss der Wert auf true, siehe eigener Abschnitt weiter unten.
  • DB_DRIVER: maria ist der dokumentierte Wert für MariaDB. Zulässig sind laut Dokumentation mysql, maria, postgres, mssql und sqlite.
  • DB_HOST: shlink_db ist der Dienstname aus dieser Datei, nicht localhost. Innerhalb des Compose-Netzwerks löst Docker den Dienstnamen auf. localhost würde im Shlink-Container auf den Shlink-Container selbst zeigen und die Verbindung scheitern lassen.
  • INITIAL_API_KEY legt laut Dokumentation beim Start einmalig einen API-Schlüssel mit Administratorrechten an. Die Dokumentation nennt dazu ausdrücklich, dass die Variable ignoriert wird, sobald bereits andere API-Schlüssel existieren, dass ein späteres Entfernen der Variable den Schlüssel nicht löscht und ein späteres Ändern ihn nicht anpasst.
  • TIMEZONE: Europe/Berlin sorgt dafür, dass gespeicherte Zeitstempel in der erwarteten Zone liegen. Laut Dokumentation fällt Shlink sonst auf die PHP-Standardzone zurück, die im Docker-Image UTC ist.
  • ports mit 127.0.0.1:8085:8080 bindet den Dienst ausdrücklich nur an die Loopback-Adresse. Der Dienst ist damit von außen nicht direkt erreichbar, sondern nur über den Reverse Proxy auf demselben Host. Das war im Test so und ist für den Produktivbetrieb die richtige Voreinstellung.

Installation und erster Start

Legen Sie das Verzeichnis an, speichern Sie beide Dateien darin und starten Sie den Stack:

# Arbeitsverzeichnis anlegen und betreten
mkdir -p /opt/shlink
cd /opt/shlink

# Danach compose.yaml und .env in diesem Verzeichnis ablegen

# Images herunterladen und Stack im Hintergrund starten
docker compose up -d

Im Test lief dieser Befehl durch. Danach zeigt die Statusabfrage, ob beide Dienste in Ordnung sind:

# Status der Dienste ansehen
docker compose ps

Beobachtet wurde dabei der Datenbankcontainer mit dem Status Up (healthy) und der Shlink-Container mit dem Status Up. Der Datenbankcontainer erreicht den gesunden Zustand zuerst, der Shlink-Container startet erst danach. Wenn etwas hakt, liefern die Protokolle die Antwort:

# Protokoll beider Dienste laufend mitlesen
docker compose logs -f

# Nur den Shlink-Dienst ansehen, letzte 50 Zeilen
docker compose logs --tail 50 shlink

Funktionsprüfung

Der erste Test ist der Health-Endpunkt. Er braucht keinen API-Schlüssel:

# Gesundheitszustand der Instanz abfragen
curl -s http://127.0.0.1:8085/rest/health

Die im Test tatsächlich zurückgelieferte Antwort lautete:

{"status":"pass","version":"5.1.6","links":{"about":"https://shlink.io","project":"https://github.com/shlinkio/shlink"}}

Damit ist zweierlei belegt: Der Dienst antwortet, und die laufende Version stimmt mit dem im Compose-File gesetzten Tag überein. Diese Gegenprobe lohnt sich, weil ein beweglicher Tag wie stable durchaus eine andere Version liefern kann als erwartet.

Als Nächstes eine erste Kurz-URL über die API. Der API-Schlüssel wird im Header X-Api-Key übergeben, so wie es die Dokumentation zur Authentifizierung vorgibt:

# Erste Kurz-URL anlegen
curl -s -X POST http://127.0.0.1:8085/rest/v3/short-urls   -H "X-Api-Key: IHR-API-SCHLUESSEL"   -H "Content-Type: application/json"   -d '{"longUrl":"https://example.org/eine/lange/adresse","tags":["test"]}'

Der Aufruf war im Test erfolgreich. Die Antwort enthielt unter anderem die Felder shortUrl, shortCode, longUrl, dateCreated, tags, ein domain mit dem Wert null, die Schalter crawlable mit false und forwardQuery mit true sowie ein visitsSummary, dessen Zähler erwartungsgemäß alle auf 0 standen.

Zwei Beobachtungen aus diesem Aufruf sind wichtiger als der Erfolg selbst. Erstens: Die zurückgegebene shortUrl wurde mit dem Wert aus DEFAULT_DOMAIN gebildet, nicht mit der lokalen Testadresse, über die der Aufruf gestellt wurde. Zweitens: Shlink hatte selbstständig den Titel der Zielseite ermittelt und im Datensatz als title hinterlegt. Das ist ein ausgehender Netzwerkzugriff des Dienstes auf die Zieladresse. Laut Dokumentation steuert die Umgebungsvariable AUTO_RESOLVE_TITLES dieses Verhalten und ist mit true vorbelegt. Wer keine ausgehenden Verbindungen zu beliebigen Zielen wünscht, sei es aus Datenschutzgründen oder weil die Firewall das nicht hergibt, sollte diese Variable bewusst auf false setzen.

Zum Abschluss die Gegenprobe über das Auflisten:

# Die letzten drei Kurz-URLs auflisten
curl -s "http://127.0.0.1:8085/rest/v3/short-urls?itemsPerPage=3"   -H "X-Api-Key: IHR-API-SCHLUESSEL"

Auch dieser Aufruf funktionierte im Test mit demselben Schlüssel und lieferte die zuvor angelegte Kurz-URL zurück. Damit ist der Weg vom Anlegen bis zum Lesen vollständig belegt.

Persistente Daten, Datenbank und Rechte

Alle dauerhaften Daten dieses Stacks liegen an genau einer Stelle: im benannten Volume shlink_db_data, eingehängt unter /var/lib/mysql im Datenbankcontainer. Der Shlink-Container selbst hält in dieser Konfiguration keine Nutzdaten, die einen Neustart überleben müssen. Das vereinfacht die Sicherung erheblich, verlagert aber auch das gesamte Risiko auf dieses eine Volume.

# Vorhandene Volumes des Stacks anzeigen
docker volume ls --filter name=shlink

# Details zu einem Volume, unter anderem der Pfad auf dem Host
docker volume inspect shlink_shlink_db_data

Der tatsächliche Volume-Name wird von Compose aus dem Projektnamen und dem Namen aus der Datei gebildet. Der Projektname entspricht standardmäßig dem Verzeichnisnamen. Wer den Stack in einem Verzeichnis namens shlink ablegt, bekommt also shlink_shlink_db_data.

Zu den Rechten: Laut Dokumentation laufen die Shlink-Images ab Version 4.0.0 aus Sicherheitsgründen mit einem nicht privilegierten Benutzer. Der MariaDB-Container verwaltet die Rechte innerhalb des Volumes selbst. Solange Sie ein benanntes Volume und keine Bind-Mount-Einbindung in ein Hostverzeichnis verwenden, müssen Sie an den Dateirechten nichts von Hand einstellen. Wer stattdessen einen Hostpfad einhängt, handelt sich Eigentümerprobleme ein und muss die Rechte selbst passend setzen.

Betrieb unter eigener Domain, Reverse Proxy und TLS

Wichtiger Hinweis vorweg: Dieser Abschnitt beruht auf der Herstellerdokumentation und auf allgemeiner Betriebspraxis. Der Betrieb hinter einem Reverse Proxy mit echtem TLS-Zertifikat und eigener Domain wurde im Rahmen dieser Anleitung nicht durchgespielt. Der Test lief bewusst nur lokal über HTTP.

Shlink terminiert kein TLS. Die Dokumentation beschreibt ausdrücklich den Betrieb hinter einem Reverse Proxy und nennt Nginx, Apache und Caddy als übliche Wege. Der Proxy nimmt die HTTPS-Verbindung an, kümmert sich um das Zertifikat und reicht die Anfrage an den Shlink-Port weiter. Entscheidend ist laut Dokumentation dabei, dass die IP-Adresse des Besuchers und die angefragte Domain vom Proxy an Shlink weitergereicht werden, sonst arbeiten nicht alle Funktionen wie erwartet. In der Praxis heißt das: Host, X-Real-IP, X-Forwarded-For und X-Forwarded-Proto setzen. Stehen mehrere Proxys hintereinander, nennt die Dokumentation die Variable TRUSTED_PROXIES, mit der Sie entweder die Anzahl der vorgelagerten Proxys oder deren Adressen angeben.

Zwei Variablen entscheiden darüber, ob Ihre Kurzlinks brauchbar sind:

  • DEFAULT_DOMAIN muss genau Ihre Kurzdomain enthalten, ohne Schema und ohne abschließenden Schrägstrich, also zum Beispiel kurz.example.com. Laut Dokumentation ist das die Standard-Kurzdomain der Instanz.
  • IS_HTTPS_ENABLED muss im Produktivbetrieb auf true stehen. Laut Dokumentation teilt diese Variable Shlink mit, ob der Dienst über HTTPS ausgeliefert wird, und die Dokumentation stellt ausdrücklich klar, dass es weiterhin Ihre Aufgabe ist, ihn auch tatsächlich über HTTPS auszuliefern. Bleibt der Wert auf false, während der Proxy HTTPS spricht, erzeugt Shlink die Kurzlinks mit dem Schema http. Sie verteilen dann Links, die zunächst unverschlüsselt aufgerufen werden.

Nach jeder Änderung an diesen Variablen muss der Container neu erzeugt werden, ein einfacher Neustart reicht nicht:

# Container mit geaenderten Umgebungsvariablen neu erzeugen
docker compose up -d

# Danach pruefen, welche Werte im Container wirklich ankommen
docker compose exec shlink env | grep -E 'DEFAULT_DOMAIN|IS_HTTPS_ENABLED'

Wichtig für das Verständnis: Bereits angelegte Kurzlinks behalten ihren Kurzcode. Die Korrektur dieser Variablen ändert also nicht rückwirkend die Codes, wohl aber die Adresse, unter der Shlink sie fortan ausgibt.

API-Schlüssel verwalten und absichern

Laut Dokumentation werden API-Schlüssel über die Kommandozeile im Container erzeugt und verwaltet. Der Zugriff erfolgt über docker compose exec:

# Alle vorhandenen API-Schluessel auflisten
docker compose exec shlink shlink api-key:list

# Einen neuen benannten Schluessel erzeugen
docker compose exec shlink shlink api-key:generate --name=monitoring

# Einen Schluessel ueber seinen Namen deaktivieren
docker compose exec shlink shlink api-key:disable monitoring --by-name

# Alle verfuegbaren Befehle anzeigen
docker compose exec shlink shlink

Drei Punkte aus der Dokumentation, die den Betrieb prägen:

  • Ab Shlink 4.3 werden API-Schlüssel vor dem Speichern gehasht. Der Klartext ist nach der Erzeugung nicht mehr abrufbar. Sie müssen den Wert also sofort an einer sicheren Stelle ablegen, sonst ist er verloren.
  • Schlüssel lassen sich mit Rollen einschränken. Genannt werden AUTHORED_SHORT_URLS für den Zugriff nur auf selbst erzeugte Kurzlinks, DOMAIN_SPECIFIC für die Beschränkung auf eine bestimmte Domain und NO_ORPHAN_VISITS, mit dem verwaiste Besuche für diesen Schlüssel nicht sichtbar sind. Ohne Rollen gilt ein Schlüssel laut Dokumentation als Administratorschlüssel und darf alles.
  • Rollen lassen sich an einem bestehenden Schlüssel nicht nachträglich ändern. Wer Rechte einschränken will, muss laut Dokumentation den alten Schlüssel deaktivieren und einen neuen mit den gewünschten Rollen erzeugen.

Daraus folgt eine klare Empfehlung dieser Anleitung, die über die Dokumentation hinausgeht: Verwenden Sie den über INITIAL_API_KEY erzeugten Administratorschlüssel nur für die Einrichtung. Legen Sie für jede Integration, die Kurzlinks anlegt, einen eigenen benannten Schlüssel mit möglichst enger Rolle an. Dann lässt sich später auch nachvollziehen, welche Anwendung welche Links erzeugt hat, und ein kompromittierter Schlüssel kostet Sie nur diese eine Integration. Die Dokumentation nennt außerdem die Möglichkeit, beim Erzeugen ein Ablaufdatum zu setzen, was für befristete Projekte sinnvoll ist.

Backup und Wiederherstellung

Ehrlichkeitshalber vorweg: Ein vollständiger Wiederherstellungsdurchlauf wurde im Rahmen dieser Anleitung nicht durchgeführt. Die folgenden Befehle beschreiben den üblichen und für MariaDB dokumentierten Weg. Prüfen Sie ihn in Ihrer Umgebung selbst, bevor Sie sich darauf verlassen. Ein Backup, dessen Rückweg nie getestet wurde, ist kein Backup.

Zu sichern sind drei Dinge:

  • Der Inhalt der Datenbank, also alle Kurzlinks, Schlagwörter und Besuchsdaten.
  • Die Konfiguration, also compose.yaml und .env. Ohne die .env nützt Ihnen der Datenbank-Dump wenig, weil Passwort und Domain fehlen.
  • Optional das Volume als Ganzes, als zusätzliche Absicherung neben dem logischen Dump.

Ein logischer Dump aus dem laufenden Container:

# Datenbank in eine komprimierte Datei mit Zeitstempel sichern
docker compose exec -T shlink_db   mariadb-dump --single-transaction -u shlink -p"$SHLINK_DB_PASSWORD" shlink   | gzip > shlink-$(date +%F).sql.gz

# Ergebnis auf plausible Groesse pruefen
ls -lh shlink-*.sql.gz

Der Schalter --single-transaction sorgt bei InnoDB für einen in sich konsistenten Stand, ohne die Tabellen für die Dauer des Dumps zu sperren. Die letzte Zeile ist kein Beiwerk: Ein Dump von wenigen hundert Byte ist fast immer ein Fehlerfall, typischerweise ein falsches Passwort.

Der Weg zurück, wenn die Datenbank leer oder beschädigt ist:

# Den Shlink-Dienst anhalten, damit keine Schreibzugriffe stoeren
docker compose stop shlink

# Dump zurueckspielen
gunzip -c shlink-2026-09-21.sql.gz   | docker compose exec -T shlink_db     mariadb -u shlink -p"$SHLINK_DB_PASSWORD" shlink

# Dienst wieder starten
docker compose start shlink

# Gegenprobe ueber die API
curl -s "http://127.0.0.1:8085/rest/v3/short-urls?itemsPerPage=3"   -H "X-Api-Key: IHR-API-SCHLUESSEL"

Das Anhalten des Shlink-Dienstes vor dem Zurückspielen ist kein optionaler Schritt. Offene Verbindungen und gleichzeitige Schreibzugriffe während einer Wiederherstellung führen zu einem uneinheitlichen Datenstand.

Updates und die Grenzen eines Rollbacks

Laut Dokumentation ist ein Update des Docker-Images der einfachste Fall: Zu jeder neuen Shlink-Version erscheint ein neuer Image-Tag, Sie setzen den neuen Tag ein, und das Image kümmert sich beim Start selbst um nötige Datenbankanpassungen, ohne Daten zu verlieren. Die Dokumentation stellt außerdem in Aussicht, dass Umgebungsvariablen abwärtskompatibel bleiben und nur Hauptversionen Unterstützung für bestehende Variablen fallen lassen können, dann aber mit dokumentiertem Migrationsweg.

Zwei Hinweise aus der Dokumentation sind für die Planung entscheidend. Erstens wird empfohlen, beim Aktualisieren nicht zu viele Versionen auf einmal zu überspringen, sondern in Schritten über die jeweils letzte Patch-Version einer Nebenversion zu gehen. Zweitens rät die Dokumentation ausdrücklich zu einer Datenbanksicherung vorab, falls der Vorgang zurückgerollt werden muss.

Der praktische Ablauf:

# 1. Datenbank sichern, siehe vorheriger Abschnitt

# 2. Den Versions-Tag in der compose.yaml auf die neue Version aendern

# 3. Neues Image laden
docker compose pull

# 4. Container mit neuem Image neu erzeugen
docker compose up -d

# 5. Protokoll auf Migrationsmeldungen und Fehler pruefen
docker compose logs --tail 100 shlink

# 6. Laufende Version gegenpruefen
curl -s http://127.0.0.1:8085/rest/health

Zum Thema Rollback gehört Klarheit: Wenn beim Start einer neuen Version eine Datenbankmigration gelaufen ist, hat sich das Schema geändert. Ein Zurücksetzen des Image-Tags auf die alte Version trifft dann auf eine Datenbank, die diese Version nicht kennt. Ein Rollback ist in diesem Fall nicht garantiert. Der verlässliche Rückweg ist deshalb immer der Weg über die vorher erstellte Sicherung: alten Tag einsetzen, Datenbank aus dem Dump von vor dem Update wiederherstellen. Genau deshalb ist die Sicherung vor dem Update kein optionaler Schritt. Das Update auf eine neue Hauptversion und der Rollback wurden im Rahmen dieser Anleitung nicht getestet.

Ein weiteres Argument für feste Versions-Tags: Mit einem festen Tag entscheiden Sie, wann eine Migration stattfindet. Mit stable oder latest entscheidet das ein beliebiger Neustart, womöglich nachts nach einem Stromausfall und ohne frische Sicherung.

Typische Fehler mit Diagnose und Lösung

Die Kurzlinks zeigen auf die falsche Adresse. Das ist die Falle, die im Test tatsächlich aufgetreten ist und die am leichtesten übersehen wird. Der API-Aufruf zum Anlegen war erfolgreich, die Antwort sah vollkommen unauffällig aus, und trotzdem war die zurückgegebene shortUrl nicht über die Adresse erreichbar, unter der der Aufruf gestellt wurde. Der Grund: Shlink bildet die Kurz-URL aus dem Wert von DEFAULT_DOMAIN, nicht aus dem Host der Anfrage. Wer hier einen Platzhalter stehen lässt oder die falsche Domain einträgt, bekommt also mit HTTP 200 quittierte, aber unbrauchbare Kurzlinks zurück, und merkt es unter Umständen erst, wenn die Links bereits verteilt sind. Diagnose: docker compose exec shlink env | grep DEFAULT_DOMAIN und den Wert mit der Domain vergleichen, die Ihr Proxy ausliefert. Lösung: Wert korrigieren und docker compose up -d ausführen, damit der Container neu erzeugt wird.

Shlink startet, bevor die Datenbank bereit ist. Ohne die Bedingung condition: service_healthy startet Compose den Shlink-Container, sobald der Datenbankcontainer existiert. Ein frisch initialisierter MariaDB-Container braucht aber noch einige Sekunden, bis er Verbindungen annimmt. Shlink findet dann keine Datenbank und beendet sich. Im Test löste genau die Kombination aus Healthcheck und service_healthy dieses Problem: Der Datenbankcontainer erreichte den Status healthy, erst danach startete Shlink, und der Start lief durch. Wer den Healthcheck weglässt, baut sich ein Startverhalten, das von der Geschwindigkeit der Maschine abhängt und deshalb sporadisch fehlschlägt.

Falsches Datenbankpasswort. Dieser Fall wurde nicht selbst provoziert, er ist aber der klassische Nachfolgefehler. Typische Ursache ist, dass das Passwort in der .env geändert wurde, während das Datenbank-Volume aus einem früheren Lauf noch existiert. Die MariaDB-Variablen für Benutzer und Passwort wirken nur bei der Erstinitialisierung eines leeren Volumes. Diagnose über docker compose logs shlink_db und docker compose logs shlink. Lösung entweder durch Ändern des Passworts in der Datenbank selbst oder, sofern es sich um eine Testinstallation ohne schützenswerte Daten handelt, durch vollständiges Neuaufsetzen des Volumes. Letzteres löscht alle Daten.

Der Port ist bereits belegt. Wenn auf dem Host schon ein Dienst auf dem gewählten Port lauscht, scheitert der Start des Shlink-Containers mit einer Meldung über eine bereits belegte Portzuordnung. Auch dieser Fall wurde nicht selbst provoziert. Diagnose mit ss -tlnp | grep 8085. Lösung: einen freien Port wählen und die Portzeile in der compose.yaml anpassen, anschließend die Weiterleitung im Reverse Proxy entsprechend nachziehen.

Kurzlinks werden mit http statt https ausgegeben. Ursache ist ein IS_HTTPS_ENABLED auf false, während der Reverse Proxy längst HTTPS ausliefert. Der Dienst funktioniert, die Links funktionieren auch, aber sie starten unverschlüsselt. Diagnose über docker compose exec shlink env | grep IS_HTTPS_ENABLED. Lösung: Wert auf true setzen und den Container neu erzeugen.

Der API-Schlüssel wird beim zweiten Start nicht neu angelegt. Das ist kein Fehler, sondern dokumentiertes Verhalten. Laut Dokumentation wird INITIAL_API_KEY ignoriert, sobald bereits API-Schlüssel existieren. Das Ändern des Wertes ändert keinen bestehenden Schlüssel, das Entfernen der Variablen löscht keinen. Wer einen weiteren Schlüssel braucht, erzeugt ihn über die Kommandozeile im Container.

Ein Kurzlink liefert eine Fehlermeldung zur Authentifizierung. Bei einem fehlenden, ungültigen, deaktivierten oder abgelaufenen Schlüssel antwortet die API laut Dokumentation mit dem Status 401 und einer Meldung, dass der angegebene API-Schlüssel nicht existiert oder ungültig ist. Prüfen Sie dann zuerst, ob der Header wirklich X-Api-Key heißt und ob der Schlüssel in der Ausgabe von api-key:list als aktiv geführt wird.

Saubere Deinstallation

Achtung, hier ist der Unterschied zwischen zwei Befehlen der Unterschied zwischen einer Pause und einem endgültigen Datenverlust. Lesen Sie beide Varianten, bevor Sie eine davon ausführen.

# Variante 1: Container stoppen und entfernen, Daten bleiben erhalten
docker compose down

# Variante 2: ACHTUNG, entfernt zusaetzlich die Volumes
# Damit werden die Datenbank und alle Kurzlinks unwiederbringlich geloescht
docker compose down -v

Der Befehl docker compose down entfernt die Container und das Netzwerk, lässt das benannte Volume aber stehen. Ein späteres docker compose up -d im selben Verzeichnis findet die Datenbank unverändert vor. Der Befehl docker compose down -v entfernt zusätzlich die Volumes. Damit ist die Datenbank weg, und mit ihr sämtliche Kurzlinks, Schlagwörter und Besuchsstatistiken. Es gibt keinen Rückweg außer einer vorher erstellten Sicherung.

Prüfen Sie im Zweifel, ob das Volume noch existiert:

# Volumes des Stacks auflisten
docker volume ls --filter name=shlink

# Nicht mehr benoetigte Images entfernen
docker image rm shlinkio/shlink:5.1.6 mariadb:11

Ein Punkt, der bei einem URL-Kürzer schwerer wiegt als bei fast jedem anderen Dienst: Ihre Kurzlinks sind nach außen verteilt. Sie stehen in E-Mails, in QR-Codes auf gedruckten Materialien, in Präsentationen und auf fremden Webseiten. Sobald Sie den Dienst abschalten oder die Domain aufgeben, laufen all diese Links ins Leere, und zwar dauerhaft und ohne dass Sie es merken. Planen Sie eine Abschaltung deshalb nie als reine Serverarbeit. Klären Sie vorher, welche Links im Umlauf sind, und richten Sie nach Möglichkeit eine Weiterleitung der Domain auf eine Ersatzseite ein, statt sie einfach verfallen zu lassen. Die Dokumentation nennt für den laufenden Betrieb dazu passend die Variablen DEFAULT_INVALID_SHORT_URL_REDIRECT und DEFAULT_REGULAR_404_REDIRECT, mit denen sich unbekannte Kurzcodes auf eine eigene Seite statt auf eine allgemeine Fehlerseite leiten lassen.

Passende Anleitungen auf S-EDV

Quellen

ShlinkURL-KürzerDocker ComposeMariaDBSelfhostingREST-APIReverse Proxy