HedgeDoc mit Docker Compose und PostgreSQL selbst hosten
Diese Anleitung zeigt, wie Sie HedgeDoc 1.12.0 mit Docker Compose und PostgreSQL 18 betreiben, anonyme Notizen abschalten, die Registrierung schließen, den Editor hinter Nginx mit TLS veröffentlichen und Datenbank sowie Uploads so sichern, dass der Restore nachweislich funktioniert.
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

HedgeDoc ist ein Markdown-Editor im Browser, in dem mehrere Personen gleichzeitig an derselben Notiz schreiben. Protokolle, Runbooks oder Checklisten bleiben so auf dem eigenen Server. Diese Anleitung zeigt den Betrieb mit Docker Compose und PostgreSQL in der Version 1.12.0: geschlossene Instanz ohne anonyme Notizen, Reverse Proxy mit TLS, Sicherung von Datenbank und Uploads sowie eine Wiederherstellung, die tatsächlich funktioniert.
Voraussetzungen
HedgeDoc ist genügsam. Im Test belegte der Anwendungscontainer nach dem Start rund 180 MB RAM, PostgreSQL etwa 40 MB. Das Image belegt rund 870 MB.
- Linux-Server oder VM mit 1 bis 2 CPU-Kernen, 2 GB RAM und 10 GB freiem Speicher, für ein kleines Team mehr als genug
- x86-64 oder ARM64: das Projekt stellt Images für
amd64undarm64bereit, ein Raspberry Pi 4 oder 5 genügt also ebenfalls - Docker Engine mit Compose-Plugin (
docker compose) - Eine (Sub-)Domain wie
server.example.de, die auf den Server zeigt, und ein Reverse Proxy mit TLS-Zertifikat - Nur im Firmennetz genügen interner DNS-Eintrag und internes Zertifikat
Grenzen sollten Sie kennen: HedgeDoc 1.x hat keine Ordnerstruktur und keine Volltextsuche über alle Notizen. Für ein strukturiertes Firmenwiki ist ein klassisches Wiki besser geeignet. Die Software steht unter AGPL-3.0, es gibt keine kostenpflichtige Edition.
Schritt 1: Projektordner, compose.yaml und .env anlegen
Legen Sie einen Ordner /opt/hedgedoc an. Die offizielle Beispieldatei aus hedgedoc/container ist nur ein Einstieg (Passwort im Klartext, anonyme Notizen, Port 3000 auf allen Schnittstellen). Die folgende Fassung behebt das.
services:
database:
image: postgres:18.6-alpine
environment:
POSTGRES_USER: hedgedoc
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD fehlt}
POSTGRES_DB: hedgedoc
volumes:
# Ab PostgreSQL 18 liegt das Volume auf /var/lib/postgresql
- database:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U hedgedoc -d hedgedoc"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
app:
image: quay.io/hedgedoc/hedgedoc:1.12.0
environment:
CMD_DB_URL: postgres://hedgedoc:${DB_PASSWORD}@database:5432/hedgedoc
CMD_DOMAIN: ${HD_DOMAIN:?HD_DOMAIN fehlt}
CMD_PROTOCOL_USESSL: ${HD_USESSL:-true}
CMD_URL_ADDPORT: ${HD_ADDPORT:-false}
CMD_HSTS_ENABLE: ${HD_USESSL:-true}
CMD_SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET fehlt}
CMD_ALLOW_ANONYMOUS: "false"
CMD_ALLOW_ANONYMOUS_EDITS: "false"
CMD_ALLOW_EMAIL_REGISTER: ${HD_ALLOW_REGISTER:-false}
CMD_DEFAULT_PERMISSION: private
CMD_ALLOW_GRAVATAR: "false"
volumes:
- uploads:/hedgedoc/public/uploads
ports:
- "127.0.0.1:3000:3000"
# Kein eigener Healthcheck: das Image bringt "node healthcheck.mjs" mit
restart: unless-stopped
depends_on:
database:
condition: service_healthy
volumes:
database:
uploads:
Zwei Details sind wichtig. Erstens mountet die Datenbank /var/lib/postgresql und nicht /var/lib/postgresql/data. Das offizielle PostgreSQL-Image hat ab Version 18 sein Datenverzeichnis auf /var/lib/postgresql/18/docker verlegt und das Volume auf /var/lib/postgresql. Die HedgeDoc-Beispieldatei nutzt noch den alten Pfad. Zweitens fehlt bewusst ein eigener Healthcheck für app: Das Image enthält kein curl. Ein naheliegender Test mit curl -f http://localhost:3000/_health lässt den Container dauerhaft unhealthy erscheinen, im Log steht dann /bin/sh: 1: curl: not found. Der eingebaute Check node healthcheck.mjs funktioniert dagegen ohne Zutun.
Die Geheimnisse gehören in die Datei .env im selben Ordner. Erzeugen Sie beide Werte mit openssl rand -hex 24 beziehungsweise -hex 32 und setzen Sie die Rechte auf chmod 600 .env.
# openssl rand -hex 24
DB_PASSWORD=HIER_ZUFALLSWERT_EINTRAGEN
# openssl rand -hex 32
SESSION_SECRET=HIER_ZUFALLSWERT_EINTRAGEN
HD_DOMAIN=server.example.de
HD_USESSL=true
HD_ADDPORT=false
# nur für die Ersteinrichtung auf true
HD_ALLOW_REGISTER=true
Das SESSION_SECRET ist kein Detail: Fehlt es, erzeugt HedgeDoc bei jedem Start ein neues, und alle Benutzer werden nach jedem Neustart oder Update abgemeldet. Ohne gesetzten Wert verweigert Compose den Start:
docker compose config -q
error while interpolating services.database.environment.POSTGRES_PASSWORD:
required variable DB_PASSWORD is missing a value: DB_PASSWORD fehlt
Verifizieren: docker compose config -q läuft ohne Ausgabe durch. Erscheint die Meldung oben, fehlt ein Wert in der .env oder die Datei liegt im falschen Ordner.
Schritt 2: Container starten und Healthcheck prüfen
cd /opt/hedgedoc
docker compose up -d
docker compose ps
docker compose logs app | tail -n 5
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/_health
Beim ersten Start legt HedgeDoc die Tabellen an. Im Log erscheinen All migrations performed successfully und HTTP Server listening at 0.0.0.0:3000. Der Endpunkt /status liefert JSON mit Kennzahlen wie notesCount und registeredUsers; er ist öffentlich erreichbar und sollte am Proxy eingeschränkt werden (siehe Schritt 4).
Verifizieren: docker compose ps zeigt beide Dienste als (healthy), der curl-Aufruf liefert 200. Die laufende Version prüfen Sie mit docker compose exec app node -e 'console.log(require("/hedgedoc/package.json").version)', erwartet wird 1.12.0.
Schritt 3: Erstes Konto anlegen und Instanz schließen
Öffnen Sie https://server.example.de, klicken Sie auf Anmelden und im Dialog auf Registrieren. Weil CMD_ALLOW_ANONYMOUS auf false steht, landen nicht angemeldete Besucher beim Aufruf von /new auf der Startseite statt in einer neuen Notiz.

Sind alle Konten angelegt, setzen Sie HD_ALLOW_REGISTER=false und führen docker compose up -d aus. Ein Registrierungsversuch beantwortet HedgeDoc danach mit HTTP 404. Weitere Konten legen Sie dann auf der Kommandozeile an:
docker compose exec app sh -c 'cd /hedgedoc && NODE_ENV=production \
bin/manage_users --add kollegin@example.de'
Ohne --pass fragt das Werkzeug das Passwort interaktiv ab, so landet es nicht in der Shell-Historie. Mit --reset setzen Sie ein vergessenes Passwort zurück. Alternativ bindet HedgeDoc LDAP, OAuth2 oder SAML an.
CMD_DEFAULT_PERMISSION=private sorgt dafür, dass neue Notizen zunächst nur ihr Besitzer sieht. Freigaben vergeben Sie je Notiz über das Berechtigungsmenü rechts oben in der Vorschau.


Verifizieren: Nach der Anmeldung zeigt /me im Browser "status":"ok" mit Ihrem Namen. Ein Aufruf einer privaten Notiz in einem privaten Browserfenster leitet ohne Anmeldung auf die Startseite um (HTTP 302), ebenso /new.
Schritt 4: Reverse Proxy und TLS
HedgeDoc spricht nur HTTP und baut alle Links aus drei Variablen, der häufigsten Fehlerquelle. Für https://server.example.de gilt: CMD_DOMAIN=server.example.de (ohne Protokoll und Pfad), CMD_PROTOCOL_USESSL=true und CMD_URL_ADDPORT=false. Der Proxy muss WebSockets unter /socket.io/ durchreichen, sonst lädt der Editor endlos. Eine Konfiguration für Nginx nach Vorlage der Projektdokumentation:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
http2 on;
server_name server.example.de;
ssl_certificate /etc/letsencrypt/live/server.example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/server.example.de/privkey.pem;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /socket.io/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location ~ ^/(metrics|status)$ {
allow 192.0.2.0/24;
deny all;
proxy_pass http://127.0.0.1:3000;
}
}
Achten Sie darauf, dass proxy_pass keinen abschließenden Schrägstrich hat; laut Dokumentation bricht sonst die WebSocket-Verbindung. Caddy reicht WebSockets und X-Forwarded-Proto von sich aus weiter, im Nginx Proxy Manager aktivieren Sie die Option für WebSocket-Unterstützung. Durch die Bindung an 127.0.0.1 erreicht niemand HedgeDoc am Proxy vorbei.
Mit CMD_PROTOCOL_USESSL=true setzt HedgeDoc das Sitzungscookie mit dem Attribut Secure. Ein Login direkt über http://127.0.0.1:3000 wird zwar mit einer Weiterleitung beantwortet, aber ohne Set-Cookie, Sie bleiben also abgemeldet. Testen Sie Logins immer über die HTTPS-Adresse.
Verifizieren: curl -sI https://server.example.de/_health liefert 200. Im Quelltext der Startseite steht <base href="https://server.example.de/">, und im Editor zeigt die Leiste oben 1 ONLINE statt eines Ladesymbols.
Schritt 5: Datenbank und Uploads sichern
Notizen, Benutzer und Sitzungen liegen in PostgreSQL, hochgeladene Bilder im Volume uploads. Ohne dieses Volume fehlen nach einem Restore alle Bilder. Das folgende Skript sichert beides; der Volume-Name setzt sich aus Ordnername und Volume zusammen (hedgedoc_uploads), prüfen Sie ihn mit docker volume ls.
#!/bin/bash
# /opt/hedgedoc/backup.sh: Datenbank und Uploads sichern
set -euo pipefail
cd /opt/hedgedoc
mkdir -p backup
D=$(date +%F_%H%M)
docker compose exec -T database pg_dump -U hedgedoc -d hedgedoc --format=custom \
> backup/hedgedoc-db-$D.dump
docker run --rm -v hedgedoc_uploads:/data:ro -v "$PWD/backup":/backup alpine \
tar czf /backup/hedgedoc-uploads-$D.tar.gz -C /data .
find backup -type f -mtime +14 -delete
Starten Sie das Skript täglich per Cron und kopieren Sie backup sowie die .env auf ein anderes System.
Verifizieren: Im Ordner backup liegen eine .dump- und eine .tar.gz-Datei mit aktuellem Datum. tar tzf auf das Archiv listet die hochgeladenen Bilder, pg_restore --list auf den Dump die Tabellen Notes, Users und Revisions.
Schritt 6: Wiederherstellung durchspielen
Im Test wurde eine Notiz mit dem Titel Restore-Marker HD-7741 und einem Bild angelegt, gesichert, dann die Notiz gelöscht, die Datenbank verworfen und das Bild aus dem Volume entfernt. Die Anwendung muss vorher stoppen, sonst blockieren offene Verbindungen das DROP DATABASE.
cd /opt/hedgedoc
docker compose stop app
docker compose exec -T database psql -U hedgedoc -d postgres \
-c 'DROP DATABASE hedgedoc;' -c 'CREATE DATABASE hedgedoc OWNER hedgedoc;'
docker compose exec -T database pg_restore -U hedgedoc -d hedgedoc \
--no-owner --exit-on-error < backup/hedgedoc-db-2026-10-04_0106.dump
docker run --rm -v hedgedoc_uploads:/data -v "$PWD/backup":/backup:ro alpine \
tar xzf /backup/hedgedoc-uploads-2026-10-04_0106.tar.gz -C /data
docker compose start app
Nach dem Start war die Notiz wieder da, mit Besitzer und Berechtigung private, das Bild hatte dieselbe SHA-256-Prüfsumme wie vor der Sicherung und lieferte über den alten Link HTTP 200. Weil auch die Tabelle Sessions im Dump steckt und das SESSION_SECRET unverändert blieb, war die Browsersitzung von vor dem Backup weiterhin angemeldet.
Verifizieren: docker compose exec database psql -U hedgedoc -d hedgedoc -tAc 'select title, permission from "Notes";' zeigt Ihre Marker-Notiz, und das Bild in der Notiz wird angezeigt.
Schritt 7: Updates, Rollback und Entfernen
Lesen Sie vor jedem Update die Release Notes. Version 1.11.1 enthielt eine Sicherheitskorrektur (GHSA-93w7-49m2-cqwg). Auf quay.io zeigen 1.12.0, 1.12.0-debian, debian und latest auf dasselbe Image, eine Alpine-Variante gibt es als 1.12.0-alpine. Schreiben Sie die Version fest, statt latest zu verwenden.
./backup.sh
sed -i 's|hedgedoc:1.12.0|hedgedoc:1.12.1|' compose.yaml # Beispiel für ein Folgerelease
docker compose pull app
docker compose up -d
Weil neue Versionen beim Start die Datenbank migrieren, gehört zum Rollback neben dem alten Tag auch der Dump von vor dem Update (Schritt 6). Eine neue PostgreSQL-Hauptversion erfordert Dump und Restore, ein geändertes Tag allein reicht nicht.
Zum Entfernen genügt docker compose down; Volumes bleiben dabei erhalten. Achtung: docker compose down -v löscht die Volumes database und uploads unwiderruflich, alle Notizen und Bilder sind dann weg. Führen Sie das nur mit geprüftem Backup aus.
Verifizieren: Nach einem Update zeigt die Versionsabfrage aus Schritt 2 die neue Nummer, docker compose ps meldet (healthy), und das Log endet mit All migrations performed successfully.
Troubleshooting
| Symptom | Ursache | Lösung |
|---|---|---|
| Nach Login weiterhin abgemeldet | Aufruf über HTTP, Cookie ist Secure | Über die HTTPS-Adresse anmelden |
Weiterleitung auf https://localhost/ | CMD_DOMAIN steht noch auf localhost | Domain eintragen, docker compose up -d |
Links enthalten :3000 | CMD_URL_ADDPORT=true hinter dem Proxy | Auf false setzen |
| Seite lädt, Editor unvollständig | CMD_PROTOCOL_USESSL=false hinter HTTPS, Skripte werden als http:// geladen und blockiert | Auf true setzen |
| Editor lädt endlos | WebSocket am Proxy nicht durchgereicht | /socket.io/ mit Upgrade-Headern |
Bind for 127.0.0.1:3000 failed: port is already allocated | Port 3000 belegt | Linke Portzahl ändern, Proxy anpassen |
App dauerhaft unhealthy | Eigener Healthcheck mit curl | Eigenen Check entfernen, Image-Check nutzen |
Falsche Domain, Port oder Protokoll erkennen Sie am <base href> im Quelltext der Startseite: Dort steht genau die Adresse, die HedgeDoc für korrekt hält.
Häufige Fragen
Kann ich HedgeDoc mit SQLite statt PostgreSQL betreiben?
Technisch ja, die offizielle Beispieldatei markiert SQLite aber als nicht empfohlen. PostgreSQL macht Backups mit pg_dump konsistent, während die Anwendung läuft.
Wie migriere ich von CodiMD oder HackMD?
Laut Dokumentation lässt sich das Image meist direkt austauschen, vorher sichern Sie Datenbank und Uploads.
Wer darf die Berechtigung einer Notiz ändern?
Nur der Besitzer, also das Konto, das die Notiz angelegt hat. Andere Benutzer können je nach Stufe lesen oder schreiben, die Stufe selbst aber nicht verändern.
Darf ich Gästen Lesezugriff geben?
Ja. Laut Berechtigungstabelle der Dokumentation dürfen Gäste Notizen mit der Stufe Geschlossen oder Bearbeitbar lesen, auch wenn anonyme Notizen abgeschaltet sind. Schreiben dürfen sie in beiden Fällen nicht.
Fazit
HedgeDoc ist schnell aufgesetzt und im Betrieb unauffällig. Die Arbeit steckt in drei Punkten: die URL-Variablen passend zum Proxy setzen, die Instanz nach der Ersteinrichtung schließen und neben der Datenbank auch das Upload-Volume sichern. Wer den Restore einmal mit einer Marker-Notiz durchgespielt hat, kann Updates gelassen einspielen.
| Eckdaten | Wert |
|---|---|
| Image | quay.io/hedgedoc/hedgedoc:1.12.0 (amd64, arm64) |
| Datenbank | postgres:18.6-alpine |
| Port | 3000 (intern), nur an 127.0.0.1 gebunden |
| Volumes | database (/var/lib/postgresql), uploads (/hedgedoc/public/uploads) |
| Wichtige Variablen | CMD_DOMAIN, CMD_PROTOCOL_USESSL, CMD_URL_ADDPORT, CMD_SESSION_SECRET, CMD_ALLOW_ANONYMOUS, CMD_ALLOW_EMAIL_REGISTER |
| Lizenz | AGPL-3.0 |
Weiterführende Anleitungen und Quellen
- PostgreSQL mit pg_dump und pg_restore sichern und migrieren
- Caddy als Reverse Proxy mit automatischem HTTPS
- SilverBullet: Markdown-Wissensdatenbank mit Docker
- Memos mit Docker für Schnellnotizen
- HedgeDoc-Dokumentation: Docker-Installation
- HedgeDoc-Dokumentation: alle Konfigurationsvariablen
- HedgeDoc-Dokumentation: Reverse Proxy
- Versionshinweise zu HedgeDoc 1.12.0 auf GitHub
- Offizielles Container-Repository mit Beispiel-Compose
- PostgreSQL-Image: Hinweise zum Datenpfad ab Version 18


