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

Reactive Resume mit Docker Compose: Lebenslauf-Editor selbst hosten

Reactive Resume ist ein quelloffener, datenschutzfreundlicher Lebenslauf-Editor. Die Anleitung zeigt den getesteten Betrieb von Version 6.0.3 mit Docker Compose und PostgreSQL 18, PDF-Export ohne Zusatzdienst, das Sperren der Registrierung, Backup mit pg_dump und Upload-Archiv samt erprobtem Restore, Updates und typische Fehlermeldungen.

Beschrieben für reactive-resume 6.0.3

Mit KI erstellt – redaktionelle Prüfung ausstehend

WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Titelbild zur Anleitung: Reactive Resume als selbst gehosteter Lebenslauf-Editor mit PDF-Export, eigener Datenhaltung sowie Backup und Restore

Bewerbungsunterlagen enthalten mehr persönliche Daten als fast jedes andere Dokument: Anschrift, Telefonnummer, Werdegang, oft ein Foto. Wer solche Daten für Mitarbeitende, Auszubildende oder Kunden in einem Online-Baukasten pflegt, gibt sie an einen Dienstleister weiter. Reactive Resume ist ein quelloffener Lebenslauf-Editor unter MIT-Lizenz, den Sie komplett auf dem eigenen Server betreiben. Mit Version 6 hat sich der Aufbau stark vereinfacht: Ein Node.js-Container erledigt Weboberfläche, API und PDF-Erzeugung, PostgreSQL speichert die Daten. Diese Anleitung zeigt Installation, Erstkonfiguration, Betrieb, Backup samt tatsächlich durchgespieltem Restore, Updates und die typischen Fehlermeldungen. Alle Befehle und Ausgaben stammen aus einem eigenen Testlauf mit Version 6.0.3.

Voraussetzungen

Im Test belegte der App-Container nach mehreren PDF-Exporten rund 300 MiB RAM, PostgreSQL etwa 60 MiB. Die Projektdoku nennt 1 GB RAM für die App und 2 GB, wenn die Datenbank auf demselben Host läuft.

  • Linux-Server oder VM mit 1 bis 2 CPU-Kernen, 2 GB RAM und 10 GB freiem Speicher; das Image gibt es für amd64 und arm64
  • Docker Engine mit Docker Compose v2
  • eine Subdomain wie lebenslauf.example.de mit DNS-Eintrag und ein Reverse Proxy mit TLS-Zertifikat, etwa Caddy oder Nginx
  • optional ein SMTP-Konto für Bestätigungs- und Passwort-Mails
  • ein Speicherziel außerhalb des Servers für Dumps und Upload-Archive

Schritt 1: Image-Tag und Eckdaten festlegen

Das Image liegt mit identischen Tags auf Docker Hub (amruthpillai/reactive-resume) und in der GitHub Container Registry (ghcr.io/reactive-resume/reactive-resume). Die offizielle Doku verwendet latest. Beim Test am 15.10.2026 zeigten latest, v6, v6.0 und v6.0.3 auf denselben Digest; v6.0.3 erschien am 14.10.2026. Schreiben Sie die exakte Version fest, damit ein Neustart nicht unbemerkt eine neue Version mit eigenen Migrationen zieht. Wichtig ist das v: Der Tag 6.0.3 ohne Präfix existiert nicht.

$ docker pull amruthpillai/reactive-resume:6.0.3
Error response from daemon: failed to resolve reference "docker.io/amruthpillai/reactive-resume:6.0.3": ... not found
EckdatumWert
Imageamruthpillai/reactive-resume:v6.0.3, Node.js 24, läuft als Benutzer node (UID 1000)
Port3000 (Weboberfläche, API, Uploads)
Volumes/app/data (Uploads), /var/lib/postgresql (PostgreSQL 18)
Pflicht-EnvAPP_URL, DATABASE_URL, AUTH_SECRET
Healthcheckeingebaut, ruft /api/health alle 30 Sekunden ab

Wer Version 4 oder frühe 5er-Versionen kennt: Die Variablen PRINTER_*, BROWSERLESS_* und CHROME_* liest Version 6 laut Doku nicht mehr. MinIO oder SeaweedFS sind optional; ohne S3-Variablen speichert die App Uploads lokal.

Verifizieren: docker pull amruthpillai/reactive-resume:v6.0.3 lädt das Image ohne Fehler.

Schritt 2: compose.yml und .env anlegen

Legen Sie einen Projektordner an und darin die Datei .env. Das Passwort und das Geheimnis erzeugen Sie jeweils mit openssl rand -hex 32; das Passwort muss in POSTGRES_PASSWORD und DATABASE_URL identisch sein.

mkdir -p /opt/reactive-resume && cd /opt/reactive-resume

# .env
APP_URL="https://lebenslauf.example.de"
POSTGRES_PASSWORD="hier-das-erzeugte-passwort"
DATABASE_URL="postgresql://postgres:hier-das-erzeugte-passwort@postgres:5432/postgres"
AUTH_SECRET="hier-der-zweite-zufallswert"

Die compose.yml folgt der offiziellen Doku, nur mit festem Tag:

services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_DB: postgres
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes: ["postgres_data:/var/lib/postgresql"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 10s
      timeout: 5s
      retries: 10

  reactive-resume:
    image: amruthpillai/reactive-resume:v6.0.3
    restart: unless-stopped
    ports: ["127.0.0.1:3000:3000"]
    env_file: .env
    volumes: ["reactive_resume_data:/app/data"]
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
  reactive_resume_data:

Zwei Details sind bewusst gewählt. PostgreSQL 18 legt seine Daten unter /var/lib/postgresql ab, ältere Beispiele mit /var/lib/postgresql/data passen nicht mehr. Und der Port ist an 127.0.0.1 gebunden, damit nur Ihr Reverse Proxy die App erreicht. APP_URL muss exakt der Adresse in der Browserzeile entsprechen, inklusive https://; warum, zeigt das Troubleshooting.

Verifizieren: docker compose config gibt die aufgelöste Konfiguration ohne Fehlermeldung aus.

Schritt 3: Starten und erstes Konto anlegen

docker compose up -d --wait --wait-timeout 180
curl -s http://127.0.0.1:3000/api/health

Beim ersten Start legt die App ihre Tabellen an. Im Test war der Container nach etwa 30 Sekunden healthy, das Log zeigte:

Running database migrations...
Database migrations completed
Validating local storage path: /app/data
🚀 Up and running on http://localhost:3000

Der Health-Endpunkt antwortet mit "version":"6.0.3","status":"healthy" und meldet Datenbank und Speicher einzeln. Öffnen Sie nun Ihre Adresse im Browser und legen Sie über „Create one now" Ihr Konto mit Name, Benutzername, E-Mail und Passwort an. Sie sind sofort angemeldet. Ohne SMTP verschickt die App keine Bestätigungsmail, das Log meldet dann SMTP not configured; skipping email send.

Anmeldeseite von Reactive Resume mit Feldern für E-Mail oder Benutzername und Passwort sowie Passkey-Anmeldung
Die Anmeldeseite der eigenen Instanz; der Link „Create one now" führt zur Registrierung.

Jetzt kommt der wichtigste Konfigurationsschritt: Im Test ließ sich direkt danach ein zweites Konto ohne Einladung anlegen (HTTP 200). Sperren Sie die Registrierung, sobald alle benötigten Konten existieren:

echo 'FLAG_DISABLE_SIGNUPS="true"' >> .env
docker compose up -d

Danach lehnte die Instanz neue Registrierungen mit EMAIL_PASSWORD_SIGN_UP_DISABLED ab, bestehende Sitzungen liefen weiter. Weitere Schalter wie Gastzugang oder reine SSO-Anmeldung stehen in der Variablenreferenz.

Verifizieren: curl -s http://127.0.0.1:3000/api/openapi/flags enthält "disableSignups":true.

Schritt 4: Lebenslauf anlegen und als PDF exportieren

Über „New" legen Sie einen Lebenslauf an, auf Wunsch mit Beispieldaten. Der Editor zeigt links die Felder und rechts die Seitenvorschau, gespeichert wird beim Tippen. Unter „Design" stehen 17 Vorlagen bereit, mehrere davon als ATS-tauglich markiert, also gut lesbar für Bewerbermanagementsysteme.

Editor von Reactive Resume mit Eingabefeldern links und Live-Vorschau eines Lebenslaufs rechts, oben die Schaltfläche Download PDF
Editor mit Live-Vorschau; oben rechts erzeugt „Download PDF" die Datei direkt.

Der PDF-Export braucht in Version 6 keinen Zusatzdienst. Im Test lieferte „Download PDF" im Browser eine Datei mit 146 KB, und auch serverseitig ließ sich jedes Format abrufen. Für Skripte sieht die API neben Sitzungs-Cookies auch API-Schlüssel vor; im Test diente das Sitzungs-Cookie:

curl -s -b cookies.txt -o cv.pdf -w "%{http_code} %{size_download}\n" \
  http://127.0.0.1:3000/api/openapi/resumes/<ID>/exports/pdf
200 143522

Die Datei begann mit %PDF-1.7, hatte drei A4-Seiten und enthielt die eingetragenen Texte. DOCX, Markdown und JSON lieferte derselbe Endpunkt mit docx, md und json. Hochgeladene Fotos speichert die App als JPEG unter /app/data/uploads/<Benutzer-ID>/pictures/.

Design-Ansicht von Reactive Resume mit Vorlagengalerie, Filtern für ein- und zweispaltige sowie ATS-taugliche Vorlagen und Vorschau
Design-Bereich mit 17 Vorlagen und Filter für ATS-taugliche Layouts.

Verifizieren: Der Download beginnt mit %PDF (head -c 8 cv.pdf) und lässt sich in jedem PDF-Betrachter öffnen.

Schritt 5: Backup erstellen

Die Daten liegen an drei Stellen: in der Datenbank, im Upload-Volume und in der .env. Ohne dasselbe AUTH_SECRET meldet eine wiederhergestellte Instanz laut Doku alle Benutzer ab und kann Zwei-Faktor-Geheimnisse nicht mehr entschlüsseln.

mkdir -p backup
docker compose exec -T postgres pg_dump -U postgres -d postgres --format=custom > backup/reactive-resume.dump
docker run --rm -v reactive-resume_reactive_resume_data:/data:ro -v "$PWD/backup":/backup \
  alpine tar czf /backup/uploads.tar.gz -C /data .
cp .env backup/env.backup

Der Volumename setzt sich aus Projektordner und Volume zusammen; docker volume ls zeigt ihn. Im Test war der Dump 133 KB groß und enthielt 40 Tabellen mit Daten, das Archiv das hochgeladene Foto. Kopieren Sie den Ordner anschließend auf ein anderes System; beachten Sie dabei, dass die .env Geheimnisse enthält.

Verifizieren: docker compose exec -T postgres pg_restore -l < backup/reactive-resume.dump | grep -c "TABLE DATA" zählt die Tabellen im Dump, tar tzf backup/uploads.tar.gz listet die Uploads.

Schritt 6: Restore durchführen und prüfen

Ein Backup ist erst etwas wert, wenn die Wiederherstellung klappt. Im Test diente ein Lebenslauf namens „MARKER-RESTORE-4711" als Markierung. Das Löschen in der Oberfläche reicht als Test nicht: Der Lebenslauf wandert in den Papierkorb, die Zeile bleibt 30 Tage in der Tabelle resume. Deshalb wurde ein Totalverlust simuliert, also Datenbank und Upload-Volume entfernt:

docker compose stop reactive-resume
docker compose exec -T postgres dropdb -U postgres --force --maintenance-db=template1 postgres
docker compose exec -T postgres createdb -U postgres --maintenance-db=template1 postgres
docker compose exec -T postgres pg_restore -U postgres -d postgres --clean --if-exists --exit-on-error < backup/reactive-resume.dump
docker run --rm -v reactive-resume_reactive_resume_data:/data -v "$PWD/backup":/backup:ro \
  alpine sh -c "tar xzf /backup/uploads.tar.gz -C /data && chown -R 1000:1000 /data"
docker compose up -d

Weil die Datenbank selbst postgres heißt, brauchen dropdb und createdb eine andere Wartungsdatenbank, hier template1. Das chown ist nötig, weil die App als UID 1000 schreibt. Nach dem Drop zählte die neue Datenbank 0 Tabellen, pg_restore endete mit Exit-Code 0. Danach meldete /api/health Datenbank und Speicher als gesund, der Marker-Lebenslauf stand wieder in der Liste, das Foto lieferte HTTP 200 und der PDF-Export funktionierte. Sogar die alte Browsersitzung blieb gültig, weil Sitzungen in der Datenbank liegen und AUTH_SECRET unverändert war.

Dokumentenübersicht von Reactive Resume nach dem Restore mit dem wiederhergestellten Lebenslauf MARKER-RESTORE-4711
Nach dem Restore steht der Marker-Lebenslauf wieder in der Dokumentenübersicht.

Verifizieren: docker compose exec -T postgres psql -U postgres -At -c "select name from resume" zeigt Ihre Lebensläufe, curl -s http://127.0.0.1:3000/api/health meldet healthy.

Schritt 7: Updates einspielen

Erstellen Sie vor jedem Update ein Backup wie in Schritt 5. Danach tragen Sie den neuen Tag in die compose.yml ein und erneuern nur die App:

docker compose pull reactive-resume
docker compose up -d reactive-resume
docker compose logs -f reactive-resume

Im Test lief das Update von v6.0.2 auf v6.0.3 so durch: Die App wendete beim Start ihre Migrationen an, /api/health meldete danach "version":"6.0.3", Konto und Lebenslauf blieben erhalten. PostgreSQL läuft dabei weiter. Ein Wechsel der PostgreSQL-Hauptversion ist ein eigenes Projekt: Dump erstellen, neue Version mit leerem Volume starten, Dump einspielen. Wer noch Version 5 betreibt, liest vorher die Migrationsseite der Doku. Neue Releases kündigen Diun oder WUD an, ohne automatisch zu aktualisieren.

Verifizieren: docker compose ps zeigt den neuen Tag mit Status healthy, und /api/health nennt die neue Version.

Troubleshooting

Die meisten der folgenden Meldungen wurden im Test absichtlich provoziert. Erste Anlaufstelle ist immer docker compose logs --tail=50 reactive-resume.

  • Container startet nicht, Log zeigt ❌ Invalid environment variables mit path: [ 'AUTH_SECRET' ]: Eine Pflichtvariable fehlt oder ist leer; ein leerer Wert gilt als nicht gesetzt.
  • password authentication failed for user "postgres", Code 28P01, Container im Zustand Restarting: Das Passwort in DATABASE_URL passt nicht zu dem, mit dem das Volume angelegt wurde. Eine spätere Änderung von POSTGRES_PASSWORD wirkt nicht auf ein bestehendes Volume. Sonderzeichen müssen URL-kodiert sein.
  • Error: connect ECONNREFUSED 127.0.0.1:5432: In DATABASE_URL steht localhost. Im App-Container ist das der Container selbst; richtig ist der Dienstname postgres.
  • Local storage path is not writable: /app/data mit EACCES: permission denied: Ein Bind-Mount gehört root. Mit chown -R 1000:1000 auf den Ordner oder einem benannten Volume ist das behoben.
  • Anmeldung schlägt fehl, API antwortet 403: APP_URL passt nicht zur aufgerufenen Adresse. Im Test lehnte die Instanz mit APP_URL="http://localhost:3000" eine Anmeldung über die HTTPS-Adresse mit 403 ab; Anfragen mit fremdem Origin beantwortet sie mit INVALID_ORIGIN. Bei einer https://-Adresse setzt die App das Cookie mit dem Präfix __Secure-.
  • /api/health liefert 503: Der Eintrag mit "status":"unhealthy" zeigt, ob Datenbank, Speicher oder Redis klemmt.

Häufige Fragen

Brauche ich Browserless, Chrome oder MinIO?

Nein. Version 6 erzeugt PDFs selbst, im Test ohne jeden Zusatzcontainer. S3-kompatibler Speicher ist optional und wird über S3_*-Variablen aktiviert; MinIO und SeaweedFS brauchen dabei S3_FORCE_PATH_STYLE="true".

Ist SMTP nötig?

Für den Betrieb nicht. Ohne vollständige Angaben zu SMTP_HOST, SMTP_USER, SMTP_PASS und SMTP_FROM verschickt die App nichts und vermerkt das im Log. Für „Passwort vergessen" ist ein Mailserver aber sinnvoll.

Werden Daten an Dritte übertragen?

Lebensläufe, Uploads und Sitzungen liegen in Ihrer Datenbank und Ihrem Volume. KI-Funktionen und Jobsuche sind optional und nutzen nur Anbieter, die Sie selbst konfigurieren.

Wie binde ich eine zentrale Anmeldung an?

Reactive Resume unterstützt OpenID Connect über die OAUTH_*-Variablen, etwa mit Authentik, Keycloak oder Pocket ID. Mit FLAG_DISABLE_EMAIL_AUTH="true" lässt sich die Passwort-Anmeldung danach abschalten.

Fazit

Reactive Resume 6 ist schnell betriebsbereit: zwei Container, drei Pflichtvariablen, PDF-Export ohne Zusatzdienst. Die wichtigsten Handgriffe sind das Festschreiben des Tags mit v-Präfix, eine exakt passende APP_URL und das Sperren der Registrierung nach dem eigenen Konto. Für die Sicherung genügen pg_dump, ein Archiv des Upload-Volumes und eine Kopie der .env; genau diese Kombination stellte im Test nach einem simulierten Totalverlust alles wieder her.

Weiterführende Anleitungen und Quellen

Reactive ResumeLebenslaufDocker ComposePostgreSQLSelfhostingBackupDatenschutz