Zum Inhalt springen

Produktionsbetrieb

Dieses Dokument beschreibt, wie Katalon auf einem Linux-Server in Produktion betrieben wird.

Empfohlener Weg für neue Instanzen: katalon-cli (uv tool install katalon-cli) installiert und aktualisiert Produktionsinstanzen über gepinnte Release-Images, ohne Repository-Checkout — siehe katalon-cli Repository. Der manuelle Weg unten (Repository klonen, Compose-Dateien selbst pflegen) bleibt für Sonderfälle und zum Verständnis der zugrundeliegenden Compose-Topologie relevant, wird aber nicht mehr als primärer Installationsweg empfohlen.

Für Wartungsaufgaben im API-Container steht katalon-manage bereit:

Terminal-Fenster
docker compose exec api katalon-manage --help

katalon-manage db-reset löscht Bestandsdaten und legt vorher standardmäßig ein PostgreSQL-Backup an. Ohne --all bleiben Konfiguration, Benutzerkonten, Schemata, Vokabulare sowie Rollen- und Funktionsrechte erhalten. --all löscht auch diese Konfiguration und ist nur für eine bewusst vollständig zurückzusetzende Instanz geeignet.

Optionen: --backup-dir PFAD wählt das Sicherungsverzeichnis, --no-backup überspringt die Sicherung und --yes unterdrückt die interaktive Bestätigung.

katalon-manage import-csv DATEI --type TYP --mapping MAPPING.json importiert Objekte, Entitäten, Orte oder Occurrences anhand eines JSON-Mappings. --dry-run validiert und zeigt die Vorschau, ohne Daten zu ändern. Optional steuern --subtype, --idno-strategy (auto, column, skip), --upsert-strategy (skip, merge, replace) und --auto-publish den Import; --media-selector ordnet beim Objektimport Dateinamen zu.

katalon-manage import-xml DATEI --type TYP --mapping MAPPING.json --record-xpath XPATH importiert dieselben Datensatztypen aus XML. --record-xpath wählt die einzelnen Datensätze aus; für XML-Namensräume verwendet der Ausdruck die Clark-Notation. Die Optionen entsprechen dem CSV-Import; --media-selector erwartet hier einen XPath.

katalon-manage reset-admin erstellt interaktiv ein neues zufälliges Passwort für ein Admin- oder Superuser-Konto. Bei mehreren solchen Konten wird das Zielkonto abgefragt. Das Passwort wird nur ausgegeben und zusätzlich mit Dateirechten 0600 unter /var/lib/katalon/reset-credentials.txt abgelegt. Wie jedes Zugangsdatum muss es anschließend sicher behandelt und nach der Anmeldung geändert werden.

[~/Coding/Katalon Collections/katalon-docs/src/content/docs/en/administration/production.md#D648]

katalon-manage is available inside the API container for maintenance tasks. Show its full help with:

Terminal-Fenster
docker compose exec api katalon-manage --help

katalon-manage db-reset deletes collection data and creates a PostgreSQL backup by default. Without --all, it retains configuration, user accounts, schemas, vocabularies, and role and feature permissions. --all also deletes that configuration and is only suitable when intentionally resetting an entire instance.

  • Linux-Server (Debian/Ubuntu empfohlen), min. 4 GB RAM, 20 GB Disk
  • Docker ≥ 24 und Docker Compose v2 installiert
  • Öffentliche IP-Adresse, DNS-Einträge für deine Domains gesetzt
  • TLS-Zertifikate (Let’s Encrypt empfohlen)
  • Domainname(n) entschieden und DNS-Einträge gesetzt
  • URL-Layout gewählt (Subdomain oder Subpfad, → Abschnitt 4)
  • TLS-Zertifikate ausgestellt
  • .env vollständig ausgefüllt — insbesondere SECRET_KEY, Datenbankpasswort, KATALON_BASE_URL, CORS_ORIGINS
  • MEDIA_ROOT-Host-Verzeichnis existiert und gehört UID/GID 1000 (install -d -o 1000 -g 1000 -m 755 /srv/katalon/media, nicht mkdir -p) — api- und worker-Container laufen als nicht-root User app (UID 1000). Fehlt das, schlagen Uploads still mit Permission denied fehl, ohne Health-Check-Alarm — siehe Abschnitt “Medien-Upload schlägt fehl” unten.
  • docker/nginx.prod.conf auf eigene Domain(en) angepasst (enthält bereits /robots.txt//llms.txt-Routing für das Portal sowie ein statisches Disallow: / für die Admin-Subdomain)
  • Rate-Limits für öffentliche Endpunkte geprüft (RATE_LIMIT_*, Defaults meist ausreichend) — siehe Zugriffsschutz für öffentliche Endpunkte unten
  • .env VITE-Build-Argumente für Admin/Portal gesetzt
  • Wikidata-Adapter: WIKIDATA_USER_AGENT setzen oder KATALON_BASE_URL + OAI_ADMIN_EMAIL vollständig pflegen (Wikidata-Policy erfordert identifizierbaren User-Agent)
  • Backup-Strategie eingerichtet (Cron für DB-Dump, Media-Volume gesichert)
  • Automatische Zertifikatserneuerung (certbot-Cron) eingerichtet
  • Für transaktionale E-Mails: SMTP-Relay, Absender-Domain und SPF/DKIM/DMARC eingerichtet
  • Nach erstem Start: alembic upgrade head ausgeführt
  • Nach erstem Start: First-Run-superuser-Passwort geändert
Terminal-Fenster
git clone https://github.com/katalon-collections/katalon.git
cd Katalon
Terminal-Fenster
cp .env.example .env
nano .env

Mindestens diese Werte anpassen:

Variable Beschreibung
POSTGRES_PASSWORD Starkes Datenbankpasswort
DATABASE_URL Muss dasselbe Passwort enthalten
SECRET_KEY JWT-Schlüssel — generieren mit openssl rand -hex 32
KATALON_BASE_URL Öffentliche Basis-URL der Instanz (z.B. https://katalon.example.org)
FIRST_RUN_CREDENTIALS_PATH Pfad im API-Container für die einmalig erzeugte Credentials-Datei (bei Bedarf auf ein persistentes Volume legen)
DEFAULT_ADMIN_EMAIL Fallback-E-Mail für lokale Entwicklung ohne KATALON_BASE_URL
DEFAULT_ADMIN_PASSWORD Fallback-Passwort für lokale Entwicklung ohne KATALON_BASE_URL
CORS_ORIGINS Komma-separierte Liste erlaubter Frontends
OAI_ADMIN_EMAIL Erscheint im OAI-PMH Identify-Response
WIKIDATA_USER_AGENT Optionaler User-Agent für Wikidata. Leer = automatisch aus KATALON_BASE_URL + OAI_ADMIN_EMAIL.
SMTP_* Optionaler externer SMTP-Relay für transaktionale E-Mails. Passwort nur als Betreiber-Secret setzen.
RATE_LIMIT_* Rate-Limits für öffentliche Endpunkte (Portal-Suche, OAI-PMH, JSON-LD/Turtle-Export, Authority-Proxy, globaler Default) — siehe Zugriffsschutz für öffentliche Endpunkte unten.
ROBOTS_DISALLOW_PATHS / LLMS_TXT_* Steuerung von /robots.txt und /llms.txt — siehe unten.

ARKs werden in Katalon lokal geprägt, aber erst über einen eigenen, bei der ARK Alliance registrierten NAAN weltweit auflösbar. Vor der Aktivierung:

  1. Einen dauerhaften öffentlichen Domainnamen und KATALON_BASE_URL festlegen.
  2. Einen NAAN über das NAAN-Antragsformular der ARK Alliance beantragen.
  3. Im NAAN-Register den lokalen Resolver https://katalog.example.org/ark:/<NAAN>/ hinterlegen. N2T leitet dann vollständige ARKs an die Instanz weiter.

Danach in .env setzen:

Terminal-Fenster
ARK_ENABLED=true
ARK_NAAN=12345
ARK_RESOLVER_URL=https://n2t.net/
ARK_SUFFIX_LENGTH=10

Katalon beantwortet https://katalog.example.org/ark:/12345/<Suffix> mit einem Redirect auf die aktuelle öffentliche Portal-Detailseite. ARK_ENABLED schaltet nur neue Vergaben ab; die Auflösung bereits vergebener ARKs bleibt aktiv. Der Test-NAAN 99999 ist nicht für Produktionsdaten geeignet und wird nicht aufgelöst.

Katalon betreibt keinen eigenen Mailserver. Für Passwort-Reset und spätere Benachrichtigungen wird ein externer SMTP-Relay verwendet. Versand bleibt mit SMTP_ENABLED=false deaktiviert, bis Relay und Absender vollständig konfiguriert sind.

Terminal-Fenster
SMTP_ENABLED=true
SMTP_HOST=smtp.example.org
SMTP_PORT=587
SMTP_USERNAME=noreply@example.org
SMTP_PASSWORD=BETREIBER_SECRET
SMTP_FROM=Katalon <noreply@example.org>
SMTP_STARTTLS=true
SMTP_SSL_TLS=false
KATALON_BASE_URL=https://katalon.example.org

Port 587 verwendet üblicherweise STARTTLS. Für implizites TLS (meist Port 465) SMTP_STARTTLS=false und SMTP_SSL_TLS=true setzen. Bei aktiviertem SMTP muss genau ein TLS-Modus aktiv sein und KATALON_BASE_URL gesetzt sein, damit sichere Reset-Links erzeugt werden können. Die Absender-Domain benötigt SPF, DKIM und DMARC; außerdem muss der Server den SMTP-Host erreichen können. SMTP_PASSWORD gehört ausschließlich in die nicht versionierte Produktionsumgebung oder die Secret-Verwaltung.

Wenn du eigene Ports, Volume-Pfade oder zusätzliche Umgebungsvariablen brauchst, ändere dafür nicht die zentrale docker-compose.yml.

Stattdessen:

Terminal-Fenster
cp docker-compose.override.yml.example docker-compose.override.yml

docker-compose.override.yml wird automatisch von Docker Compose mitgeladen und bleibt bei Updates unangetastet.

Terminal-Fenster
# certbot installieren (Debian/Ubuntu)
apt install certbot
# Zertifikat ausstellen (DNS muss auf den Server zeigen)
certbot certonly --standalone -d example.org -d admin.example.org
# Zertifikate in den Docker-Pfad kopieren
mkdir -p docker/certs
cp /etc/letsencrypt/live/example.org/fullchain.pem docker/certs/
cp /etc/letsencrypt/live/example.org/privkey.pem docker/certs/
chmod 644 docker/certs/*.pem

Automatische Erneuerung (crontab):

0 3 * * * certbot renew --quiet && cp /etc/letsencrypt/live/example.org/fullchain.pem /pfad/zu/katalon/docker/certs/ && cp /etc/letsencrypt/live/example.org/privkey.pem /pfad/zu/katalon/docker/certs/ && docker compose -f docker-compose.yml -f docker-compose.prod.yml exec nginx nginx -s reload

Lege fullchain.pem und privkey.pem in docker/certs/.

Option C: TLS wird extern terminiert (z. B. Traefik, nginx-proxy)

Abschnitt betitelt „Option C: TLS wird extern terminiert (z. B. Traefik, nginx-proxy)“

Läuft vor dem Stack bereits ein Reverse Proxy, der TLS terminiert (Traefik-Label-Setup, externer nginx, …), braucht der nginx-Service selbst kein TLS mehr — er wird dann nur noch intern per Docker-Netzwerk auf Port 80 angesprochen, Port 443 wird meist gar nicht mehr veröffentlicht.

docker/nginx.conf und docker/nginx.prod.conf enthalten beide einen listen 443 ssl-Block, der beim Start ladbare Zertifikate unter docker/certs/ voraussetzt — auch wenn Port 443 nie extern erreichbar ist (cannot load certificate ... BIO_new_file() failed sonst). Ein Self-signed-Zertifikat nur zum Booten in Produktion zu erzeugen ist kein guter Fix (make certs ist explizit für lokale Entwicklung gedacht, nicht für Prod-Container).

Sauberer: eine eigene, schlanke nginx-Config ohne den 443-Block einbinden (identisch zu docker/nginx.conf, nur listen 443 ssl + die beiden ssl_certificate*-Zeilen entfernt) und im jeweiligen Compose-Overlay statt docker/nginx.conf mounten. Diese Reverse-Proxy-spezifische Config ist Teil der Instanz-Konfiguration, nicht des Repos — sie gehört (wie ein eigenes docker-compose.traefik.yml-Overlay) lokal zur Instanz.

Es gibt zwei unterstützte Layouts. Einmal entscheiden, dann konsequent durchziehen.


https://meineurl.de → Public-Portal
https://admin.meineurl.de → Admin-UI

docker/nginx.prod.conf ist für dieses Layout vorbereitet. Domains ersetzen:

Terminal-Fenster
sed -i 's/example\.org/meineurl.de/g; s/admin\.example\.org/admin.meineurl.de/g' docker/nginx.prod.conf

TLS-Zertifikate für beide Domains ausstellen:

Terminal-Fenster
certbot certonly --standalone -d meineurl.de -d admin.meineurl.de

Frontend-Build-Argumente in .env setzen (werden über docker-compose.prod.yml an die Build-Stages weitergereicht):

Terminal-Fenster
ADMIN_VITE_API_URL=https://admin.meineurl.de
PORTAL_URL=https://meineurl.de
PORTAL_VITE_API_URL=https://meineurl.de

https://meineurl.de → Public-Portal
https://meineurl.de/cataloging → Admin-UI

Vite muss die Asset-Pfade beim Build einbetten. Dafür in frontend/admin/vite.config.ts ergänzen:

export default defineConfig({
base: '/cataloging/', // ← neu
// … Rest unverändert
})

docker/nginx.prod.conf — den separaten server-Block für admin.example.org ersetzen durch einen location-Block im Portal-Server:

# Im Portal-Server-Block ergänzen (nach dem /v1/-Block):
location /cataloging/ {
proxy_pass http://admin/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}

Den server-Block für admin.example.org sowie den zugehörigen HTTP-Redirect-Eintrag komplett entfernen.

Nur ein Zertifikat nötig:

Terminal-Fenster
certbot certonly --standalone -d meineurl.de

Frontend-Build-Argumente in .env:

Terminal-Fenster
ADMIN_VITE_API_URL=https://meineurl.de
PORTAL_URL=https://meineurl.de
PORTAL_VITE_API_URL=https://meineurl.de

TLS-Volume in docker-compose.prod.yml (beide Optionen)

Abschnitt betitelt „TLS-Volume in docker-compose.prod.yml (beide Optionen)“
nginx:
volumes:
- ./docker/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro
- ./docker/certs:/etc/nginx/certs:ro
ports:
- "80:80"
- "443:443"

docker/nginx.prod.conf erwartet fullchain.pem und privkey.pem direkt unter /etc/nginx/certs/ (also docker/certs/fullchain.pem und docker/certs/privkey.pem).

Terminal-Fenster
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Logs beobachten:

Terminal-Fenster
docker compose logs -f api worker

Beim ersten Start führt der API-Container automatisch keine Migrationen aus — das muss manuell angestoßen werden:

Terminal-Fenster
docker compose exec api alembic upgrade head

Nach jedem Update ebenfalls ausführen.

Nach dem ersten Start den Index und die Mappings aufbauen:

Terminal-Fenster
# Index anlegen (passiert automatisch beim ersten API-Start via ensure_index())
# Alle bestehenden Datensätze indexieren:
curl -X POST https://deine-domain.de/v1/search/reindex

Nach dem Vorgänge-Update (0.2.x) muss für bestehende Daten mindestens der Objektindex neu aufgebaut werden, weil collection_status in die öffentlichen Suchfilter aufgenommen wurde:

Terminal-Fenster
curl -X POST https://deine-domain.de/v1/search/reindex/object

Beim ersten API-Start ohne vorhandenen Admin/Superuser erzeugt Katalon automatisch:

  • E-Mail: admin@<domain-aus-KATALON_BASE_URL>
  • Passwort: kryptografisch zufällig (einmalig)

Die Zugangsdaten werden im API-Log mit dem Block ====== KATALON FIRST RUN ====== ausgegeben und zusätzlich in ./first-run-credentials.txt abgelegt (über install.sh via docker cp).

  1. Browser: https://admin.deine-domain.de
  2. Login mit den First-Run-Zugangsdaten
  3. Sofort das Passwort ändern (Admin → Benutzer → eigenes Konto)

Portal-Suche, OAI-PMH und die JSON-LD/Turtle-Export-Endpunkte sind bewusst ohne API-Key erreichbar — jeder Datensatz mit Status public ist darüber offen abrufbar (siehe REST API: Authentifizierung). Schutz vor Massenzugriff/Scraping läuft deshalb über Rate-Limiting und Crawler-Konventionen, nicht über Zugriffsbeschränkung.

Alle über .env konfigurierbar (slowapi-Syntax "N/unit", z. B. 30/minute, 500/hour):

Variable Default Betrifft
RATE_LIMIT_DEFAULT 200/minute Globaler Fallback für alle Endpunkte ohne eigenes Limit
RATE_LIMIT_PUBLIC_EXPORT 30/minute JSON-LD/Turtle-Export je Datensatz (/{typ}/{id}/export) — der wahrscheinlichste Ziel-Endpunkt für Bulk-Scraping
RATE_LIMIT_PUBLIC_SEARCH 100/minute Portal-Suche (/portal/v1/search, /portal/v1/search/advanced)
RATE_LIMIT_OAI 100/minute OAI-PMH (/oai)
RATE_LIMIT_AUTHORITY_PROXY 60/minute Normdaten-Proxy (GND/Geonames, erfordert ohnehin einen eingeloggten Benutzer)
RATE_LIMIT_SPARQL 60/minute SPARQL-Endpoint (/sparql, Abfragen gegen den Triple Store)

Login und Passwort-Reset haben eigene, fest codierte Brute-Force-Limits und sind nicht über .env steuerbar — anderes Bedrohungsmodell als Crawler-/Scraper-Traffic.

Katalon stellt /robots.txt und /llms.txt auf der Portal-Domain bereit — beides sind reine Konventionen für wohlerzogene Bots/Agenten, kein technischer Zugriffsschutz:

Terminal-Fenster
curl https://deine-domain.de/robots.txt
curl https://deine-domain.de/llms.txt
  • ROBOTS_DISALLOW_PATHS — Pfade, die /robots.txt für alle Bots sperrt (Default ["/v1/"]; Portal-HTML-Seiten bleiben crawlbar).
  • LLMS_TXT_ENABLED / LLMS_TXT_EXTRA_NOTES — /llms.txt weist LLM-Agenten explizit auf die JSON-LD/Turtle-Export-Endpunkte und OAI-PMH als strukturierte Datenquelle hin, statt HTML zu scrapen. Ab-/anschaltbar, mit optionalem Freitext-Zusatz.

docker/nginx.prod.conf routet beide Pfade auf der Portal-Domain zur API; die Admin-Subdomain bekommt stattdessen ein statisches Disallow: /, damit die Admin-UI nie indexiert wird. Bei einer eigenen nginx-Config diese Routen entsprechend nachziehen.

Manche Institutionen wollen Katalon nur als interne Erfassungsoberfläche betreiben — ohne öffentliches Portal-Frontend, teils auch ohne öffentliche REST-API/OAI-PMH. Das ist kein eigenständig gepflegtes Installationsprofil, sondern ein Custom-Rezept auf Basis der Standard-Compose-/nginx-Dateien:

  1. Portal-Container weglassen — den portal-Service in einem Compose-Override nicht starten (oder aus einer lokalen Kopie von docker-compose.prod.yml entfernen).
  2. Öffentliche location-Blöcke aus docker/nginx.prod.conf entfernen, je nachdem wie weit die Schließung gehen soll: /portal/v1/, /oai, /sparql, ^/(objects|entities|...) (kanonische Record-URIs), /robots.txt, /llms.txt. Wer stattdessen eine eigene Konsumenten-Anwendung gegen die REST-API bauen will, lässt /portal/v1/ bewusst offen und entfernt nur den Rest.
  3. Admin-UI von den jetzt toten Links befreien — beim Admin-Build VITE_PORTAL_ENABLED=false als Build-Argument setzen. Blendet den Button „Im Portal ansehen“ sowie die angezeigte OAI-Endpunkt-URL in den Einstellungen aus.
  4. IIIF (cantaloupe) bleibt in jedem Fall Pflicht-Container — wird auch für interne Bildvorschauen bei der Erfassung gebraucht, unabhängig vom Portal.

Das ist bewusst kein per Installer abfragbares Profil und wird nicht als eigener katalon-cli-Installationspfad angeboten — nur ein dokumentierter, manuell gepflegter Ausgangspunkt für Selbst-Hoster mit entsprechendem Bedarf.


Optionale RDF-Projektion & SPARQL-Schnittstelle (Oxigraph)

Abschnitt betitelt „Optionale RDF-Projektion & SPARQL-Schnittstelle (Oxigraph)“

Katalon enthält einen optionalen Triple Store (Oxigraph) für semantische Graphabfragen via SPARQL 1.1. Um den Standard-Betrieb ressourcenschonend zu halten, ist der Oxigraph-Dienst als Docker Compose-Profil rdf konzipiert und standardmäßig inaktiv (Zero Clutter).

Füge folgende Variablen zu deiner .env-Datei hinzu:

Terminal-Fenster
# Docker Compose Profile aktivieren
COMPOSE_PROFILES=rdf
# Oxigraph Triple Store Integration
OXIGRAPH_ENABLED=true
OXIGRAPH_URL=http://oxigraph:7878
# SPARQL 1.1 Protocol Endpoint
SPARQL_ENDPOINT_ENABLED=true
SPARQL_REQUIRE_AUTH=true # 'false' setzen für offenen, anonymen Lesezugriff
SPARQL_QUERY_TIMEOUT=30.0 # Maximaldauer einer SPARQL-Abfrage in Sekunden
SPARQL_MAX_QUERY_LENGTH=65536 # Maximale Query-Größe in Bytes (64 KB)
RATE_LIMIT_SPARQL=60/minute # Ratenbegrenzung für SPARQL-Anfragen

Starte den Stack mit dem Profil rdf:

Terminal-Fenster
docker compose --profile rdf up -d

Docker startet nun zusätzlich den Container oxigraph mit dem persistenten Volume oxigraph_data.

Nach dem ersten Start ist der Triple Store noch leer. Die Daten werden nicht automatisch beim Booten, sondern über einen Celery-Task synchronisiert:

  1. Über die Admin-Oberfläche:
    • Gehe zu Einstellungen (Zahnrad-Symbol).
    • In der Sektion Linked Data & SPARQL siehst du den Status und den Triple-Zähler.
    • Klicke auf „RDF-Index neu aufbauen“.
  2. Oder per API / curl:
    Terminal-Fenster
    curl -X POST "https://admin.deine-domain.de/sparql/rebuild" \
    -H "Authorization: Bearer <dein-admin-token>"

Der Worker verarbeitet alle publizierten Datensätze im Hintergrund und erzeugt die entsprechenden Named Graphs (urn:katalon:graph:<type>:<uuid>).

Sobald OXIGRAPH_ENABLED=true aktiv ist, synchronisiert Katalon bei jeder Veröffentlichung, Aktualisierung oder Löschung eines Datensatzes den zugehörigen Named Graph automatisch in Oxigraph. Nicht-öffentliche Entwürfe verbleiben isoliert in PostgreSQL.

Da Oxigraph eine reine, abgeleitete Projektion aus PostgreSQL darstellt, kann der gesamte Triple Store im Katastrophenfall jederzeit verlustfrei über „RDF-Index neu aufbauen“ aus der Datenbank rekonstruiert werden. Ein separates Backup des Volumes oxigraph_data ist daher im Normalbetrieb nicht zwingend erforderlich.

Standardmäßig liegen Mediendateien unter MEDIA_ROOT auf dem lokalen Dateisystem (unverändertes Verhalten, keine Aktion nötig). Alternativ lassen sich Mediendateien in einem S3-kompatiblen Objektspeicher ablegen — Ceph RADOSGW, MinIO, Hetzner Object Storage, Garage oder AWS S3. Strikt opt-in über STORAGE_BACKEND=s3; Logos und Portal-Themes bleiben unabhängig vom gewählten Backend immer lokal unter MEDIA_ROOT, nur die eigentlichen Objekt-Mediendateien wandern nach S3.

Der Bucket muss vor der Aktivierung existieren; Katalon legt ihn nicht selbst an. Die konfigurierten Zugangsdaten benötigen Put-, Get- und Delete-Rechte auf dem Bucket.

Terminal-Fenster
STORAGE_BACKEND=s3
S3_ENDPOINT_URL=https://s3.example-provider.com
S3_BUCKET=katalon-media
S3_REGION=us-east-1
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_FORCE_PATH_STYLE=true
S3_VERIFY_TLS=true
#S3_CA_BUNDLE=/pfad/zu/ca-bundle.pem # nur bei privater CA nötig
Variable Default Bedeutung
STORAGE_BACKEND local s3 aktiviert das Objektspeicher-Backend
S3_ENDPOINT_URL leer Leer = AWS-Default-Endpoint. Bei Ceph RADOSGW/MinIO/Hetzner/Garage die jeweilige Endpoint-URL setzen
S3_BUCKET leer Pflichtfeld bei STORAGE_BACKEND=s3
S3_REGION us-east-1 RADOSGW/MinIO ignorieren die Region inhaltlich, SigV4 braucht aber einen Wert
S3_ACCESS_KEY / S3_SECRET_KEY leer Pflichtfelder bei STORAGE_BACKEND=s3
S3_FORCE_PATH_STYLE true Path-Style-Adressierung (endpoint/bucket/key) funktioniert überall ohne DNS-Wildcard-Setup
S3_VERIFY_TLS true false nur für Test-Umgebungen mit selbstsigniertem Zertifikat
S3_CA_BUNDLE leer Pfad zu einem CA-Bundle, wenn der Endpoint ein Zertifikat einer privaten CA verwendet

Fehlen S3_BUCKET, S3_ACCESS_KEY oder S3_SECRET_KEY bei STORAGE_BACKEND=s3, verweigert der API-Start mit einer entsprechenden Fehlermeldung.

docker-compose.s3.yml reicht die S3_*-Variablen an api, worker und cantaloupe durch und stellt Cantaloupe auf S3Source um. Als letztes Overlay nach den übrigen anhängen:

Terminal-Fenster
docker compose -f docker-compose.yml -f docker-compose.cantaloupe.yml \
-f docker-compose.prod.yml -f docker-compose.s3.yml up -d

Wichtig — Cantaloupe braucht DNS für den Bucket: Cantaloupes S3Source (Version 5.0.x) kennt keine Path-Style-Adressierung, sondern spricht ausschließlich virtual-hosted (<bucket>.<endpoint-host>). <bucket>.<endpoint-host> muss also auflösbar sein — entweder per Wildcard-DNS auf den Objektspeicher-Endpoint oder per explizitem DNS-Eintrag für genau diesen Bucket-Namen. Das Backend selbst (boto3, für Upload/Download über die API) spricht dagegen per Default Path-Style (S3_FORCE_PATH_STYLE=true) und braucht dieses DNS-Setup nicht.

Für den Dev-Stack steht docker-compose.minio.yml bereit — startet einen lokalen MinIO-Container samt Konsole (http://localhost:9001, minioadmin/minioadmin) und verdrahtet api, worker und cantaloupe automatisch dagegen:

Terminal-Fenster
docker compose -f docker-compose.yml -f docker-compose.dev.yml \
-f docker-compose.cantaloupe.yml -f docker-compose.minio.yml up -d

Der Bucket muss auch hier einmalig angelegt werden (MinIO-Konsole oder S3-API).

  • Keine Live-Migration: Der Wechsel zwischen local und s3 verschiebt bestehende Mediendateien nicht automatisch. Ein Backend-Wechsel ist nur für neue Instanzen bzw. vor dem ersten Produktionsstart sinnvoll; bereits gespeicherte Dateien müssten manuell auf den neuen Bucket bzw. zurück nach MEDIA_ROOT übertragen werden, inklusive Anpassung der storage_key-Werte in der Datenbank.
  • Keine presigned URLs: Ausgelieferte Mediendateien (/objects/{id}/media/{media_id}/file) streamen bei S3 immer durch die API, nicht per Redirect auf eine presigned URL. So bleibt die Sichtbarkeitsprüfung (öffentlich/privat) wirksam — private Medien können nicht über eine direkt aufrufbare S3-URL an der API vorbeigeleitet werden. Der lokale Backend-Pfad nutzt dagegen weiterhin die effiziente FileResponse-Sendfile-Auslieferung.
Terminal-Fenster
git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker compose exec api alembic upgrade head
# Falls Schema-Änderungen: Reindex anstoßen
curl -X POST https://deine-domain.de/v1/search/reindex

Für Updates, die nur Objekt-Suchfelder ändern, reicht:

Terminal-Fenster
curl -X POST https://deine-domain.de/v1/search/reindex/object

Für den Wechsel auf einen neuen Host siehe Katalon auf einen neuen Server umziehen. Die Anleitung sichert Datenbank, Medien, Instanzkonfiguration und TLS-Dateien; Elasticsearch wird auf dem Zielsystem neu indexiert.

Der Compose-Stack enthält einen backup-Service (docker/backup.sh), der täglich läuft und beides sichert:

  • Datenbank: pg_dump → db_<timestamp>.sql.gz
  • Mediendateien (MEDIA_ROOT): media_<timestamp>.tar.gz

Dumps landen im Host-Verzeichnis BACKUP_ROOT (Default /srv/katalon/backups) und werden nach BACKUP_RETENTION_DAYS (Default 14) automatisch gelöscht.

Konfiguration über .env:

Variable Default Bedeutung
BACKUP_ENABLED true false = Service läuft, macht aber keine Backups
BACKUP_ROOT /srv/katalon/backups Host-Zielverzeichnis
BACKUP_RETENTION_DAYS 14 ältere Dumps werden gelöscht
BACKUP_AT 03:00 feste Uhrzeit HH:MM (Container-Zeitzone, s. u.)
BACKUP_INTERVAL_SECONDS 86400 nur wirksam wenn BACKUP_AT leer ist

Zwei Zeitplan-Modi: Ist BACKUP_AT gesetzt, läuft das Backup täglich zur festen Uhrzeit. Ist es leer, greift der Intervall-Modus (BACKUP_INTERVAL_SECONDS, Backup sofort beim Start + dann alle N Sekunden).

Zeitzone: BACKUP_AT wird in der Container-Zeitzone interpretiert (Standard UTC). Für lokale Zeit TZ=Europe/Berlin im environment des backup-Service setzen.

Terminal-Fenster
# Einmaliges Backup sofort auslösen (z. B. vor einem Deploy)
docker compose run --rm backup once
# Vorhandene Backups ansehen
ls -lh /srv/katalon/backups

Off-site empfohlen: BACKUP_ROOT zusätzlich per rsync/S3 auf einen zweiten Standort spiegeln — ein Backup auf demselben Host schützt nicht vor Host-Verlust.

Datenbank:

Terminal-Fenster
gunzip -c /srv/katalon/backups/db_20260101_030000.sql.gz \
| docker compose exec -T db psql -U katalon katalon

Mediendateien (. = Inhalt von MEDIA_ROOT):

Terminal-Fenster
tar xzf /srv/katalon/backups/media_20260101_030000.tar.gz -C "$MEDIA_ROOT"

Nach dem DB-Restore Elasticsearch neu aufbauen:

Terminal-Fenster
curl -X POST https://deine-domain.de/v1/search/reindex/object # je Typ

Ein Restore ist nur so viel wert wie sein letzter Test. Verifizierter Ablauf auf frischer Umgebung:

  1. Frisches Verzeichnis + .env mit anderem POSTGRES_DB (z. B. katalon_restore).
  2. Nur DB starten: docker compose up -d db.
  3. Neueste db_*.sql.gz per obigem psql-Befehl einspielen.
  4. Zeilenzahl gegen Quelle prüfen: docker compose exec -T db psql -U katalon -d katalon_restore -c "SELECT count(*) FROM objects;"
  5. Media-tar in ein Testverzeichnis entpacken, Dateizahl vergleichen.
  6. Voll starten, curl .../health → ok, Stichprobe im Admin-UI.

Ergebnis: Dump lässt sich sauber einspielen (PostGIS-Extension inklusive), Media-tar entpackt vollständig. Drill mindestens halbjährlich wiederholen.

Elasticsearch-Daten können jederzeit aus der Datenbank neu indexiert werden (POST /v1/search/reindex). Ein eigenes ES-Backup ist für den normalen Betrieb nicht zwingend nötig.

Terminal-Fenster
curl https://deine-domain.de/health
# → {
# "status": "ok",
# "checks": {
# "database": "ok",
# "elasticsearch": "ok",
# "redis": "ok",
# "beat_heartbeat": "ok",
# "cantaloupe": "ok"
# },
# "queue_backlog": 0,
# "backup_age_hours": 6.2
# }

Der Endpoint prüft aktiv Datenbank, Elasticsearch, Redis, Cantaloupe (IIIF-Bildserver) und den Celery-Beat/Worker-Herzschlag; ist Oxigraph aktiviert (OXIGRAPH_ENABLED=true), erscheint zusätzlich ein oxigraph-Eintrag. Ist eine dieser Abhängigkeiten nicht erreichbar, liefert er HTTP 503 mit {"status": "degraded", ...} — so kann ein Load Balancer einen ausgefallenen Backend-Zustand erkennen. queue_backlog (Länge der Standard-Task-Queue) und backup_age_hours (Alter des letzten erfolgreichen Backups) sind rein informativ und wirken sich nicht auf den HTTP-Status aus — als Grundlage für eigene Monitoring-Schwellenwerte geeignet.

Terminal-Fenster
docker compose logs api # API-Logs
docker compose logs worker # Celery-Worker-Logs
docker compose logs nginx # Zugriffslog
Terminal-Fenster
# Identify
curl "https://deine-domain.de/oai?verb=Identify"
# Alle Sets
curl "https://deine-domain.de/oai?verb=ListSets"
# Alle Records (paginiert)
curl "https://deine-domain.de/oai?verb=ListRecords&metadataPrefix=oai_dc"
Service Minimum Empfohlen
db (PostgreSQL) 256 MB 512 MB
redis 64 MB 128 MB
elasticsearch 1 GB 2 GB
api 256 MB 512 MB
worker (Celery) 256 MB 512 MB
cantaloupe 256 MB 512 MB
nginx 32 MB 64 MB
Gesamt ~2,1 GB ~4 GB

nginx startet nicht: “cannot load certificate … BIO_new_file() failed”

Abschnitt betitelt „nginx startet nicht: “cannot load certificate … BIO_new_file() failed”“

docker/certs/ enthält keine Zertifikatsdateien. Betrifft typischerweise Deployments hinter einem externen Reverse Proxy (Traefik, …), die weiterhin docker/nginx.conf einbinden, obwohl der 443-Block dort nie gebraucht wird — siehe Option C oben. Kein Zertifikat für Prod erzeugen, sondern eine eigene Config ohne listen 443 ssl mounten.

API startet nicht (Datenbankverbindung schlägt fehl)

Abschnitt betitelt „API startet nicht (Datenbankverbindung schlägt fehl)“
Terminal-Fenster
docker compose logs db | tail -20
# Healthcheck abwarten: depends_on mit condition: service_healthy ist gesetzt

Katalon startet auch ohne ES (API-Endpunkte funktionieren, Suche gibt leere Ergebnisse zurück). ES braucht beim ersten Start 30–60 Sekunden.

Prüfe, ob das Volume media_data vom API-Container schreibbar ist:

Terminal-Fenster
docker compose exec api ls -la /var/lib/katalon/media/

api und worker laufen als nicht-root User app (UID 1000). Ist das Host-Verzeichnis hinter MEDIA_ROOT nicht für UID 1000 schreibbar, schlagen Uploads mit PermissionError fehl. Ein Startup-Check loggt in diesem Fall eine Warnung (MEDIA_ROOT nicht beschreibbar für UID ...) — in den api-Logs nach dieser Meldung suchen, statt erst beim ersten Upload-Fehler zu bemerken.

Fix (nur das Verzeichnis selbst, nicht rekursiv — bei großem Medienbestand wäre chown -R potenziell sehr langsam):

Terminal-Fenster
sudo chown 1000:1000 "${MEDIA_ROOT:-/srv/katalon/media}"

Neue Unterordner (_batch_imports, logos, themes) erben die Rechte des Elternverzeichnisses bei Neuanlage durch den app-User automatisch korrekt; bereits bestehende Unterordner mit falschem Owner müssen einzeln behandelt werden (chown 1000:1000 <verzeichnis>, ebenfalls nicht rekursiv nötig, solange nur neue Dateien hinzukommen).

Große IIIF-Bilder bleiben leer, obwohl Thumbnails funktionieren

Abschnitt betitelt „Große IIIF-Bilder bleiben leer, obwohl Thumbnails funktionieren“

Cantaloupe kann info.json und kleine Thumbnails ausliefern, obwohl der beschreibbare Derivat-Cache nicht korrekt angelegt wurde. Große Bildanforderungen liefern dann unter Umständen HTTP 200 mit leerem Inhalt. Der Compose-Stack verwendet für den wegwerfbaren Derivat-Cache inzwischen ein tmpfs; bestehende Installationen mit dem früheren Named Volume müssen den Dienst einmal neu erstellen.

Terminal-Fenster
docker compose up -d --force-recreate cantaloupe

Dadurch wird kein Datenbank- oder Medien-Volume gelöscht.

Mehrere Instanzen auf einem Host: tmpfs-Größe des Cantaloupe-Caches

Abschnitt betitelt „Mehrere Instanzen auf einem Host: tmpfs-Größe des Cantaloupe-Caches“

Der Cantaloupe-Derivat-Cache läuft als tmpfs, begrenzt über CANTALOUPE_CACHE_TMPFS_SIZE (Default 512m, siehe .env.example). Ohne dieses Limit würde Docker pro Mount bis zu 50 % des Host-RAM erlauben. Läuft mehr als eine Katalon-artige Instanz auf demselben Host, CANTALOUPE_CACHE_TMPFS_SIZE je Instanz bewusst so wählen, dass die Summe aller Instanzen zusammen mit Elasticsearch- und Postgres-Speicherbedarf den verfügbaren Host-RAM nicht übersteigt.

Bei einem alten Stack, der noch das frühere Named Volume verwendet, kann der Cache alternativ repariert werden:

Terminal-Fenster
docker compose logs cantaloupe --tail=100 | grep -E "AccessDeniedException|FilesystemCache"
docker compose exec cantaloupe ls -ld /var/lib/cantaloupe/cache
docker compose exec cantaloupe sh -c 'chown -R cantaloupe:cantaloupe /var/lib/cantaloupe/cache && chmod -R u+rwX /var/lib/cantaloupe/cache'
curl -s -o /tmp/iiif-check.jpg -w 'HTTP %{http_code}, bytes %{size_download}\n' \
"https://deine-domain.de/iiif/3/<media-id>.<endung>/full/max/0/default.jpg"

Die Daten im Medien-Volume werden dabei nicht gelöscht. Der Test muss eine positive Byte-Anzahl liefern; anschließend die betroffene Portalseite hart neu laden.

Terminal-Fenster
docker compose -f docker-compose.yml -f docker-compose.prod.yml build --no-cache