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

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
amd64undarm64 - Docker Engine mit Docker Compose v2
- eine Subdomain wie
lebenslauf.example.demit 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
| Eckdatum | Wert |
|---|---|
| Image | amruthpillai/reactive-resume:v6.0.3, Node.js 24, läuft als Benutzer node (UID 1000) |
| Port | 3000 (Weboberfläche, API, Uploads) |
| Volumes | /app/data (Uploads), /var/lib/postgresql (PostgreSQL 18) |
| Pflicht-Env | APP_URL, DATABASE_URL, AUTH_SECRET |
| Healthcheck | eingebaut, 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.

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.

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/.

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.

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 variablesmitpath: [ 'AUTH_SECRET' ]: Eine Pflichtvariable fehlt oder ist leer; ein leerer Wert gilt als nicht gesetzt. password authentication failed for user "postgres", Code28P01, Container im ZustandRestarting: Das Passwort inDATABASE_URLpasst nicht zu dem, mit dem das Volume angelegt wurde. Eine spätere Änderung vonPOSTGRES_PASSWORDwirkt nicht auf ein bestehendes Volume. Sonderzeichen müssen URL-kodiert sein.Error: connect ECONNREFUSED 127.0.0.1:5432: InDATABASE_URLstehtlocalhost. Im App-Container ist das der Container selbst; richtig ist der Dienstnamepostgres.Local storage path is not writable: /app/datamitEACCES: permission denied: Ein Bind-Mount gehört root. Mitchown -R 1000:1000auf den Ordner oder einem benannten Volume ist das behoben.- Anmeldung schlägt fehl, API antwortet 403:
APP_URLpasst nicht zur aufgerufenen Adresse. Im Test lehnte die Instanz mitAPP_URL="http://localhost:3000"eine Anmeldung über die HTTPS-Adresse mit 403 ab; Anfragen mit fremdem Origin beantwortet sie mitINVALID_ORIGIN. Bei einerhttps://-Adresse setzt die App das Cookie mit dem Präfix__Secure-. /api/healthliefert 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
- PostgreSQL mit pg_dump und pg_restore sichern und migrieren
- Caddy als Reverse Proxy mit automatischem HTTPS
- Docker-Container-Updates im Blick behalten mit Diun und WUD
- Mailpit mit Docker Compose als SMTP-Testserver einrichten
- Pocket ID mit Docker Compose als OIDC-Anbieter
- Reactive Resume Docs: Self-hosting with Docker
- Reactive Resume Docs: Environment variables
- reactive-resume/reactive-resume auf GitHub
- Release Notes Reactive Resume v6.0.3
- Image-Tags amruthpillai/reactive-resume auf Docker Hub


