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

Kutt mit Docker Compose: eigener URL-Shortener mit API, Statistik und Backup

Kutt ist ein schlanker, quelloffener URL-Shortener mit Weboberfläche, REST-API und Klickstatistik. Diese Anleitung richtet Kutt 3.2.6 mit Docker Compose und SQLite ein, legt Admin und API-Key an, prüft Redirects und Statistik und zeigt ein getestetes Backup und Restore der Datenbank.

Mit KI erstellt – redaktionelle Prüfung ausstehend

Illustration zur Anleitung: Kutt URL-Shortener mit Docker Compose, Kurzlinks per API, Klickstatistik sowie Backup und Restore

Wer im Unternehmen Links für Newsletter, Drucksachen, QR-Codes oder Support-Anleitungen verkürzen will, landet schnell bei Cloud-Diensten, die jeden Klick mitschneiden und Daten außerhalb der eigenen Infrastruktur speichern. Kutt ist ein quelloffener URL-Shortener unter MIT-Lizenz, der eigene Kurzlinks, Klickstatistiken, eine REST-API und eine Admin-Oberfläche mitbringt und als einzelner Docker-Container mit SQLite läuft.

Diese Anleitung zeigt den kompletten Weg: compose.yaml mit festgelegtem Image-Tag, .env mit Pflicht-Secret, erster Admin, API-Key, Kurzlinks per API, Redirect- und Statistikprüfung, gesperrte Selbstregistrierung, Backup und Restore der SQLite-Datenbank sowie Update und Deinstallation. Der wichtigste Praxisfund vorweg: Die offiziellen Compose-Dateien im Repository bauen das Image aus dem Quellcode. Für den Betrieb auf einem Server ist das fertige Image kutt/kutt von Docker Hub der bessere Weg.

Was Kutt ist und wo die Grenzen liegen

Kutt stammt vom Projekt thedevs-network auf GitHub. Laut GitHub-API (Abruf 29.09.2026) hat das Repository 11.128 Sterne, ist nicht archiviert, der letzte Push war am 03.09.2026, und das aktuelle Release ist v3.2.6 vom 05.07.2026. Seit Version 3 läuft Kutt ohne Pflicht-Datenbankserver: Standard ist SQLite, optional werden PostgreSQL, MySQL oder MariaDB sowie Redis als Cache unterstützt.

Das bringt Kutt mit:

  • Kurzlinks mit zufälliger oder eigener Adresse, optional mit Passwort, Ablaufzeit und Beschreibung
  • Klickstatistik pro Link nach Zeitraum, Browser, Betriebssystem, Land und Referrer
  • REST-API unter /api/v2 mit Authentifizierung per X-API-Key
  • Admin-Seite zur Verwaltung von Nutzern, Links und Domains
  • Eigene Domains für Kurzlinks sowie Anmeldung per OIDC
  • Anpassbares Aussehen über den Ordner /kutt/custom

Die Grenzen sollten Sie vorher kennen. Selbstregistrierung, Passwort-Reset und E-Mail-Änderung funktionieren nur mit konfiguriertem SMTP-Versand (MAIL_ENABLED=true). Ohne Mail legt der Admin alle Konten selbst an. TLS-Zertifikate für zusätzliche eigene Link-Domains erzeugt Kutt nicht selbst, das übernimmt ein Reverse Proxy. Und Kutt ist bewusst schlank: Wer mehrere API-Keys oder Tags an Links braucht, ist mit Shlink besser bedient. Eine entsprechende Anleitung mit MariaDB finden Sie weiter unten bei den passenden Anleitungen. Kutt punktet dagegen mit eingebauter Weboberfläche für Endnutzer, die bei Shlink als separater Client dazukommt.

Voraussetzungen und Ressourcen

  • Linux-Server mit Docker Engine und Docker Compose v2 (docker compose)
  • Architektur amd64 oder arm64, beide sind für kutt/kutt:v3.2.6 auf Docker Hub veröffentlicht
  • Rund 110 MB Speicherplatz für das Image, dazu wenige hundert KB für die leere Datenbank
  • Arbeitsspeicher: Im Test belegte der Container 107 MiB direkt nach dem Start und 157 MiB nach allen Tests
  • Für den Produktivbetrieb eine eigene Subdomain, etwa go.firma.de, und ein Reverse Proxy mit TLS
  • Optional ein SMTP-Konto, falls Nutzer sich selbst registrieren oder Passwörter zurücksetzen sollen

Die getestete Version ist Kutt 3.2.6 (Image kutt/kutt:v3.2.6, Node.js 22 auf Alpine). Die Versionsnummer lässt sich im Container prüfen:

# Version im laufenden Container anzeigen
docker exec kutt grep '"version"' package.json

Offizielle Compose-Dateien und warum diese Anleitung abweicht

Im Repository liegen im Wurzelverzeichnis docker-compose.yml (SQLite), docker-compose.sqlite-redis.yml, docker-compose.postgres.yml und docker-compose.mariadb.yml. Alle enthalten beim Dienst server die Zeile build: context: . und kein image:. Sie sind also dafür gedacht, nach einem git clone lokal zu bauen. Außerdem veröffentlichen sie Port 3000 auf allen Schnittstellen und nutzen benannte Volumes.

Für einen Server ist ein festgelegtes, vorgebautes Image sinnvoller: kein Build-Werkzeug, reproduzierbare Version und ein klarer Rollback-Punkt. Die folgende Datei übernimmt deshalb Pfade und Variablen aus der offiziellen SQLite-Variante, ersetzt aber build durch image, bindet den Port nur an localhost und legt die Daten als Bind-Mount ab, damit das Backup eine gewöhnliche Datei ist.

Vollständige compose.yaml und .env

Legen Sie ein Verzeichnis an, etwa /opt/kutt, und darin die Unterordner für Daten und Anpassungen:

# Projektverzeichnis und Datenordner anlegen
sudo mkdir -p /opt/kutt/data /opt/kutt/custom
cd /opt/kutt

Die compose.yaml:

services:
  kutt:
    image: kutt/kutt:v3.2.6
    container_name: kutt
    restart: unless-stopped
    env_file: .env
    environment:
      DB_FILENAME: /var/lib/kutt/data.sqlite
    volumes:
      - ./data:/var/lib/kutt
      - ./custom:/kutt/custom
    ports:
      - "127.0.0.1:3000:3000"

Die .env enthält nur Platzhalter. Erzeugen Sie das Secret mit openssl rand -hex 32 und tragen Sie es ein:

# Pflicht: langes zufälliges Secret zum Signieren der Login-Tokens
JWT_SECRET=HIER_64_ZEICHEN_AUS_OPENSSL_EINTRAGEN
# Name in der Oberfläche
SITE_NAME=Firmen-Kurzlinks
# Domain, unter der die Kurzlinks erreichbar sind (ohne https://)
DEFAULT_DOMAIN=go.firma.de
# true nur hinter einem Reverse Proxy, sonst false
TRUST_PROXY=true
# Selbstregistrierung und anonyme Links aus
DISALLOW_REGISTRATION=true
DISALLOW_ANONYMOUS_LINKS=true
# Ohne SMTP bleibt Mail aus
MAIL_ENABLED=false
# Secret erzeugen und .env schützen
openssl rand -hex 32
chmod 600 /opt/kutt/.env

Die wichtigsten Parameter im Überblick:

ParameterWirkung
image: kutt/kutt:v3.2.6Festgelegte Version statt latest, damit Updates bewusst erfolgen.
DB_FILENAMEPfad der SQLite-Datei im Container. Sie liegt im gemounteten Ordner ./data.
./custom:/kutt/customEigene CSS-Dateien, Bilder und Vorlagen. Leer lassen, wenn nichts angepasst wird.
127.0.0.1:3000:3000Kutt ist nur lokal erreichbar, öffentlich antwortet ausschließlich der Reverse Proxy.
JWT_SECRETPflicht. Fehlt es, beendet sich der Container mit Missing environment variables: JWT_SECRET. Laut README ist auch JWT_SECRET_FILE für Docker-Secrets möglich.
DEFAULT_DOMAINDomain, aus der Kutt die Kurzlinks im Feld link der API-Antwort baut.
TRUST_PROXYLiest die Client-IP aus Proxy-Headern. Ohne Proxy auf false setzen, sonst können Clients ihre IP fälschen.
DISALLOW_REGISTRATIONSperrt die Selbstregistrierung. Voreinstellung ist true.
DISALLOW_ANONYMOUS_LINKSVerhindert Kurzlinks ohne Anmeldung. Voreinstellung ist true.

Installation und Start

# Konfiguration prüfen und starten
cd /opt/kutt
docker compose config -q
docker compose up -d
# Start im Log verfolgen
docker compose logs -f kutt

Beim Start führt der Container zuerst die Datenbankmigrationen aus und startet dann den Server. Im Test sah das so aus:

> kutt@3.2.6 migrate
> knex migrate:latest
Batch 1 run: 10 migrations
> kutt@3.2.6 start
> node server/server.js --production
> Ready on http://localhost:3000

Bis Ready vergingen im Test zwischen 8 und 24 Sekunden. Eine Verbindung vor diesem Zeitpunkt liefert bei curl den Code 000, das ist kein Fehler.

Funktionsprüfung und Healthcheck

Kutt bringt einen eigenen Health-Endpunkt mit, der schlicht OK zurückgibt:

# Health-Endpunkt abfragen, erwartet: OK 200
curl -s -w ' %{http_code}\n' http://127.0.0.1:3000/api/v2/health

Für externes Monitoring, etwa mit Uptime Kuma, eignet sich genau diese URL. Solange noch kein Konto existiert, zeigt die Startseite das Formular zum Anlegen des ersten Admins, erkennbar an create-admin im HTML.

Ersten Admin anlegen

Beim ersten Aufruf der Oberfläche fordert Kutt dazu auf, ein Admin-Konto anzulegen. Das geht im Browser über einen SSH-Tunnel (ssh -L 3000:127.0.0.1:3000 server) oder direkt per API:

# Ersten Admin anlegen (Passwort 8 bis 64 Zeichen)
curl -s -X POST http://127.0.0.1:3000/api/v2/auth/create-admin \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@firma.de","password":"LangesPasswort-2026"}'

Die Antwort ist HTTP 201 mit einem Token. Ein zweiter Aufruf wird abgewiesen, sobald irgendein Konto existiert: {"error":"Can not create the admin user because a user already exists."} mit HTTP 400. Legen Sie den Admin trotzdem sofort nach dem ersten Start an, denn bis dahin könnte jeder, der Port 3000 erreicht, dieses Konto für sich beanspruchen. Das ist ein weiterer Grund für die Bindung an localhost.

Den API-Key finden Sie in der Oberfläche unter Einstellungen. Headless klappt es mit dem Login-Token, das Kutt als Cookie token erwartet:

# Anmelden und Token merken
TOKEN=$(curl -s -X POST http://127.0.0.1:3000/api/v2/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@firma.de","password":"LangesPasswort-2026"}' | jq -r .token)
# API-Key erzeugen (40 Zeichen)
curl -s -X POST http://127.0.0.1:3000/api/v2/auth/apikey \
  -H 'Accept: application/json' -b "token=$TOKEN"

Jeder Aufruf erzeugt einen neuen Key und macht den alten ungültig, pro Nutzer gibt es genau einen. Mit dem Key legen Sie Links an:

# Kurzlink mit eigener Adresse anlegen
curl -s -X POST http://127.0.0.1:3000/api/v2/links \
  -H "X-API-Key: $KUTT_KEY" -H 'Content-Type: application/json' \
  -d '{"target":"https://s-edv.com/","customurl":"sedv","description":"Testlink"}'

Die Antwort kommt mit HTTP 201 und enthält unter anderem "address":"sedv", "visit_count":0 und "link":"https://go.firma.de/sedv". Ohne customurl vergibt Kutt eine sechsstellige Zufallsadresse wie vsTD6a. Die Länge steuert LINK_LENGTH.

# Redirect prüfen, erwartet: HTTP 302 und Location auf das Ziel
curl -s -o /dev/null -w 'HTTP %{http_code} Location: %{redirect_url}\n' http://127.0.0.1:3000/sedv

Kutt antwortet mit HTTP 302 Location: https://s-edv.com/. Ein unbekannter Kurzlink leitet per 302 auf /404 weiter. Wichtig für eigene Tests: Kutt filtert Bots anhand des User-Agents. Ein Aufruf mit dem Standard-User-Agent von curl leitet zwar weiter, zählt aber nicht als Klick. Erst ein Aufruf mit Browser-Kennung erhöht den Zähler:

# Klick mit Browser-Kennung auslösen
curl -s -o /dev/null -A 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0' \
  http://127.0.0.1:3000/sedv
# Statistik des Links abrufen
curl -s http://127.0.0.1:3000/api/v2/links/LINK-ID/stats -H "X-API-Key: $KUTT_KEY"

Die Klicks werden über eine interne Warteschlange verbucht, daher kann der Zähler ein bis zwei Sekunden nachlaufen. Die Statistik liefert Blöcke für lastDay, lastWeek, lastMonth und lastYear, jeweils mit Aufschlüsselung nach Browser, Betriebssystem, Land und Referrer. Im Test stand nach einem Klick visit_count: 1 mit firefox 1, linux 1 und referrer direct 1. Das Land bleibt bei lokalen Adressen unknown.

Registrierung sperren und Zugriffe absichern

Mit DISALLOW_REGISTRATION=true ist die Selbstregistrierung aus: POST /api/v2/auth/signup antwortet mit {"error":"Request is not allowed."} und HTTP 400, die Seite /signup mit 404. Bemerkenswert: Auch mit DISALLOW_REGISTRATION=false bleibt die Registrierung gesperrt, solange MAIL_ENABLED=false gesetzt ist, weil Kutt neue Konten per E-Mail bestätigt. Wer Registrierung wirklich öffnen will, braucht also beides.

Weitere Nutzer legen Sie als Admin in der Oberfläche an. Die Admin-Endpunkte sind auch per API erreichbar, etwa GET /api/v2/users/admin, das Konten mit Rolle und Linkanzahl auflistet. Zugriffe ohne oder mit falschem Key werden abgewiesen:

# Falscher Key, erwartet: {"error":"Unauthorized."} HTTP 401
curl -s -w ' HTTP %{http_code}\n' http://127.0.0.1:3000/api/v2/links -H 'X-API-Key: falscher-key-123'

Auch ein anonymer POST auf /api/v2/links ergibt 401. Kurzlinks auf die eigene Domain lehnt Kutt ab (URLs are not allowed, HTTP 400), damit keine Umleitungsschleifen entstehen.

Persistente Daten und Rechte

Der gesamte Zustand steckt in einer Datei: ./data/data.sqlite. Darin liegen Nutzer, gehashte Passwörter, API-Keys, Links, Besuche und Domains. Der Ordner ./custom enthält nur optionale Anpassungen. Der Container läuft als root, deshalb gehört die neu angelegte Datenbank auf dem Host dem Benutzer root. Das hat zwei Folgen: Ein normaler Benutzer kann sie beim Restore nicht einfach überschreiben (cp: cannot create regular file 'data/data.sqlite': Permission denied), und beim Aufräumen braucht es sudo. Die SQLite-Datei läuft im Journal-Modus delete, es gibt also keine zusätzlichen WAL-Dateien, die beim Kopieren vergessen werden könnten.

Wichtig für die Sicherheit: JWT_SECRET steht nur in der .env, nicht in der Datenbank. Sichern Sie die .env deshalb getrennt und verschlüsselt mit. Geht das Secret verloren, werden lediglich bestehende Browser-Sitzungen ungültig, API-Keys liegen in der Datenbank.

Netzwerkfreigabe, Reverse Proxy und TLS

Kutt terminiert selbst kein TLS. Setzen Sie einen Reverse Proxy davor, der für go.firma.de ein Zertifikat bezieht und auf 127.0.0.1:3000 weiterleitet. Mit Caddy genügt ein Block:

# /etc/caddy/Caddyfile
go.firma.de {
    reverse_proxy 127.0.0.1:3000
}

Laut README gehört dann TRUST_PROXY=true in die .env, damit Kutt die echte Client-IP aus den Proxy-Headern liest. Läuft Kutt ausnahmsweise ohne Proxy, stellen Sie den Wert auf false. DEFAULT_DOMAIN muss exakt der öffentlichen Domain entsprechen. Für zusätzliche Kundendomains richten Sie laut README einen DNS-Eintrag auf den Server ein, tragen die Domain in den Einstellungen ein und kümmern sich selbst um das Zertifikat. CUSTOM_DOMAIN_USE_HTTPS=true sorgt dann dafür, dass Kutt für diese Domains https-Links erzeugt. Die Werte SERVER_IP_ADDRESS und SERVER_CNAME_ADDRESS dienen nur der Anzeige in den Einstellungen.

Backup und Restore

Die sicherste Variante für kleine Instanzen ist ein kaltes Backup: Container kurz stoppen, Datei kopieren, wieder starten. Kurzlinks sind in diesen wenigen Sekunden nicht erreichbar, planen Sie das in ein ruhiges Zeitfenster.

# Kaltes Backup der SQLite-Datenbank
cd /opt/kutt
docker compose stop kutt
sudo cp -a data/data.sqlite /backup/kutt-$(date +%F).sqlite
docker compose start kutt
sha256sum data/data.sqlite /backup/kutt-$(date +%F).sqlite

Ohne Unterbrechung geht es mit der SQLite-Backup-API, die das mitgelieferte Modul better-sqlite3 bereitstellt. Das Ergebnis lässt sich direkt auf Integrität prüfen:

# Online-Backup im laufenden Container
docker exec kutt node -e "require('better-sqlite3')('/var/lib/kutt/data.sqlite').backup('/var/lib/kutt/backup-online.sqlite').then(p=>console.log('pages',p.totalPages))"
# Integrität prüfen, erwartet: ok
docker exec kutt node -e "console.log(require('better-sqlite3')('/var/lib/kutt/backup-online.sqlite',{readonly:true}).pragma('integrity_check',{simple:true}))"
# Sicherung aus dem Datenordner wegbewegen
sudo mv /opt/kutt/data/backup-online.sqlite /backup/kutt-online-$(date +%F).sqlite

Der Restore wurde so geprüft: Backup erstellt, den Link sedv per DELETE /api/v2/links/LINK-ID gelöscht (danach leitete /sedv auf /404 um), Datenbank zurückgespielt, Container gestartet. Danach leitete /sedv wieder mit 302 auf das Ziel, der Link hatte seinen alten Zähler visit_count 1, und der API-Key war weiterhin gültig. Weil die Datei root gehört, spielen Sie sie am einfachsten per docker compose cp in den gestoppten Container:

# Restore in den gestoppten Container
cd /opt/kutt
docker compose stop kutt
docker compose cp /backup/kutt-2026-09-29.sqlite kutt:/var/lib/kutt/data.sqlite
docker compose start kutt
# Kontrolle: Redirect wieder da
curl -s -o /dev/null -w 'HTTP %{http_code} Location: %{redirect_url}\n' http://127.0.0.1:3000/sedv

Updates und Rollback-Grenzen

Ein Update besteht aus Backup, neuem Tag in der compose.yaml und Neustart. Die Datenbankmigrationen laufen beim Start automatisch mit knex migrate:latest.

# Update auf eine neue Version
cd /opt/kutt
docker compose stop kutt
sudo cp -a data/data.sqlite /backup/kutt-vor-update-$(date +%F).sqlite
# Tag in compose.yaml anpassen, dann:
docker compose pull
docker compose up -d
docker compose logs --tail 20 kutt

Die Grenze beim Rollback: Die Migrationen passen das Datenbankschema beim Start an, einen Downgrade-Weg dokumentiert das Projekt nicht. Ein altes Image auf eine bereits migrierte Datenbank loszulassen, ist daher riskant. Ein sauberer Rollback heißt deshalb immer altes Image plus die Datenbanksicherung von vor dem Update. Links, die nach dem Update angelegt wurden, gehen dabei verloren. Auf Docker Hub gibt es neben latest und den Versions-Tags auch main, das den aktuellen Entwicklungsstand abbildet und für den Betrieb nicht gedacht ist.

Typische Fehler mit Diagnose und Lösung

SymptomUrsacheLösung
Container beendet sich sofort, Log zeigt Missing environment variables: JWT_SECRETSecret fehlt oder ist leerWert mit openssl rand -hex 32 erzeugen und in .env eintragen
curl meldet Code 000 direkt nach dem StartMigrationen laufen nochAuf Ready on http://localhost:3000 im Log warten
{"error":"Unauthorized."} mit HTTP 401Key falsch, neu erzeugt oder Header fehltHeader X-API-Key prüfen, nach Neuerzeugung alte Skripte anpassen
Klickzähler bleibt bei 0Aufrufe mit Bot-User-Agent werden nicht gezähltMit Browser testen oder curl mit -A und Browser-Kennung
Custom URL is already in use. mit HTTP 500Adresse existiert bereitsAndere customurl wählen oder bestehenden Link per PATCH ändern
Request is not allowed. bei der RegistrierungDISALLOW_REGISTRATION=true oder MAIL_ENABLED=falseKonten als Admin anlegen oder SMTP konfigurieren
Permission denied beim Zurückkopieren der DatenbankDatei gehört root, weil der Container als root läuftdocker compose cp oder sudo cp verwenden
Falsche Client-IPs in der StatistikTRUST_PROXY passt nicht zum AufbauMit Proxy true, ohne Proxy false

Saubere Deinstallation

Achtung: Die folgenden Schritte löschen alle Kurzlinks, Statistiken, Konten und API-Keys endgültig. Bereits verteilte Kurzlinks, etwa in gedruckten QR-Codes, funktionieren danach nicht mehr. Erstellen Sie vorher ein Backup, falls Sie die Daten später noch brauchen.

# Container und Netzwerk entfernen
cd /opt/kutt
docker compose down --remove-orphans
# Image entfernen
docker rmi kutt/kutt:v3.2.6
# Daten und Konfiguration löschen (unwiderruflich)
sudo rm -rf /opt/kutt

Vergessen Sie nicht, den Block im Reverse Proxy und den DNS-Eintrag für die Kurzlink-Domain zu entfernen.

Testumfang

Am 29.09.2026 wurden mit kutt/kutt:v3.2.6 auf einem Linux-Host (amd64) Start, Health-Endpunkt, Admin-Anlage, API-Key, Kurzlinks per API, 302-Redirect, Klickzählung mit Statistik, 401 bei falschem Key, gesperrte Registrierung, Persistenz nach Neustart sowie Backup (kalt und online) und Restore nach gelöschtem Link praktisch geprüft. Reverse Proxy mit TLS, eigene Domains, SMTP, OIDC und die Varianten mit PostgreSQL, MariaDB oder Redis stammen aus der Projektdokumentation und wurden nicht selbst betrieben.

Passende Anleitungen auf S-EDV

Quellen

KuttURL-ShortenerDocker ComposeSQLiteSelfhostingREST-APIBackup