Zum Inhalt springen
Serverküche
Suche

Die Suche wird geladen … (nur in der veröffentlichten Seite verfügbar).

Anwendungen Schwierigkeit: Fortgeschritten

HedgeDoc: gemeinsame Markdown-Notizen in Echtzeit

HedgeDoc 1.12 mit Postgres 18 hinter Traefik: Notizen zu zweit gleichzeitig tippen, Links teilen – und die Freigaben so einstellen, dass sie dicht sind.

· 11 Min. Lesezeit ·Dauer: ca. 50 Minuten
Inhaltsverzeichnis

Ein Etherpad, das Markdown kann: Zwei Leute tippen gleichzeitig in dieselbe Notiz, rechts entsteht live die formatierte Fassung, und am Ende verschickst du einen Link. HedgeDoc macht genau das – und liefert dabei ein paar Voreinstellungen mit, die du kennen solltest, bevor die Notiz im Netz steht.

Was bauen wir?

Wir betreiben HedgeDoc 1.12.0 mit PostgreSQL 18.6 hinter Traefik. Am Ende hast du einen Editor, in dem mehrere Leute gleichzeitig an derselben Markdown-Notiz arbeiten, mit Tabellen, Aufgabenlisten und einer Live-Vorschau. Notizen lassen sich als Link weitergeben – lesend oder schreibend, ganz wie du es einstellst.

Der zweite Teil ist wichtiger als der erste: HedgeDoc kommt offen aus der Kiste. Jeder darf sich registrieren, und jeder Besucher darf Notizen anlegen – auch ohne Konto. Wir drehen das um – geschlossen als Grundzustand, geöffnet pro Notiz – und schauen dabei genau hin, weil zwei der Einstellungen nicht das tun, was ihr Name verspricht.

Voraussetzungen

  • Traefik als Reverse Proxy mit funktionierendem Let’s-Encrypt-Resolver und einer Domain, die auf den Server zeigt
  • Die Domain musst du vorher kennen: HedgeDoc baut alle Links aus einer fest konfigurierten Domain, nachträgliches Umziehen ist unangenehm
  • Etwa 1 GB freier Arbeitsspeicher für Anwendung und Datenbank zusammen
🍳 Empfehlung Anzeige

VPS 1000 G12.5

4 vCore · 8 GB RAM · 128 GB SSD

ab 14,50 €/Monat

HedgeDoc und Postgres laufen zusammen bequem auf dem kleinen VPS.

Zu netcup →

💶 1 Monat gratis:

Einmal einlösbar, gilt für VPS 1000 G12.5. Im Warenkorb einlösen →

💶 5,95 € Gutschein für netcup-Neukunden: immer gültig · nicht für Domains und VPS Lite

Alle netcup-Gutscheine →

Schritt für Schritt

Schritt 1: Der Stack – mit einer aktuellen Postgres-Falle

Terminal
mkdir -p /opt/hedgedoc && cd /opt/hedgedoc

Zuerst die Geheimnisse. HedgeDoc signiert damit die Sitzungs-Cookies; ohne eigenen Wert nimmt es einen zufälligen, und alle Anmeldungen fliegen bei jedem Neustart raus:

Terminal
umask 077
{
  echo "POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '=+/' | cut -c1-24)"
  echo "CMD_SESSION_SECRET=$(openssl rand -hex 32)"
} > .env
YAML
# /opt/hedgedoc/compose.yaml
services:
  database:
    image: postgres:18.6-alpine
    container_name: hedgedoc-db
    restart: unless-stopped
    environment:
      POSTGRES_USER: hedgedoc
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: hedgedoc
    volumes:
      - pgdata:/var/lib/postgresql
    networks: [hedgedoc-intern]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U hedgedoc"]
      interval: 10s
      timeout: 5s
      retries: 5

  app:
    image: quay.io/hedgedoc/hedgedoc:1.12.0
    container_name: hedgedoc
    restart: unless-stopped
    depends_on:
      database:
        condition: service_healthy
    environment:
      CMD_DB_URL: postgres://hedgedoc:${POSTGRES_PASSWORD}@database:5432/hedgedoc
      CMD_SESSION_SECRET: ${CMD_SESSION_SECRET}
      CMD_DOMAIN: DEINE_DOMAIN
      CMD_PROTOCOL_USESSL: "true"
      CMD_URL_ADDPORT: "false"
      CMD_ALLOW_EMAIL_REGISTER: "false"
      CMD_ALLOW_ANONYMOUS: "false"
      CMD_DEFAULT_PERMISSION: "limited"
      CMD_ALLOW_FREEURL: "true"
    volumes:
      - ./uploads:/hedgedoc/public/uploads
    networks: [hedgedoc-intern, proxy]
    labels:
      - traefik.enable=true
      - traefik.http.routers.hedgedoc.rule=Host(`DEINE_DOMAIN`)
      - traefik.http.routers.hedgedoc.entrypoints=websecure
      - traefik.http.routers.hedgedoc.tls.certresolver=le
      - traefik.http.services.hedgedoc.loadbalancer.server.port=3000

volumes:
  pgdata:

networks:
  hedgedoc-intern:
    internal: true
  proxy:
    external: true

Zwei Dinge an diesem Stack sind kein Zufall. Die Datenbank hängt in einem eigenen, internen Netz (internal: true) und ist damit weder von außen noch von anderen Containern im proxy-Netz erreichbar – nur HedgeDoc, das in beiden Netzen steckt, kommt heran. Und der healthcheck samt depends_on: condition: service_healthy verhindert, dass HedgeDoc startet, bevor Postgres bereit ist; sonst bricht es beim ersten Verbindungsversuch ab.

PostgreSQL 18 hat den Datenpfad geändert

Ältere Anleitungen – auch die offizielle Compose-Datei des HedgeDoc-Projekts – hängen die Datenbank unter /var/lib/postgresql/**data** ein. Mit PostgreSQL 18 startet der Container so gar nicht mehr, selbst mit leerem Volume:

Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster" … The suggested container configuration for 18+ is to place a single mount at /var/lib/postgresql

Der Mount muss also eine Ebene höher, auf /var/lib/postgresql. Das gilt für benannte Volumes genauso wie für Bind-Mounts.

Warum ein benanntes Volume und kein ./pgdata-Ordner? Weil ein Bind-Mount am neuen Pfad dem Container gehören muss und sonst mit mkdir: can't create directory '/var/lib/postgresql/18/': Permission denied scheitert – beim alten Pfad hat das Image das noch selbst geradegezogen. Für Backups brauchst du den Ordner ohnehin nicht: Ein Datenbank-Backup macht man mit pg_dump, nicht mit einer Dateikopie.

Vier davon betreffen die Adressen, die HedgeDoc erzeugt:

  • CMD_DOMAIN – deine Domain, ohne Protokoll und ohne Pfad. Fehlt sie, stehen in verschickten Links localhost.
  • CMD_PROTOCOL_USESSL: "true" – erzeugt https://-Links. Der Container selbst spricht weiter HTTP, das TLS macht Traefik.
  • CMD_URL_ADDPORT: "false" – ohne das hängt HedgeDoc einen Port an jede URL.
  • CMD_ALLOW_FREEURL: "true" – erlaubt sprechende Adressen wie /kochabend statt einer Zufalls-ID: Wer eine noch freie Adresse aufruft, legt dort eine neue Notiz an. Mit der nächsten Einstellung gilt das nur für angemeldete Nutzer.

Die anderen drei entscheiden, wer überhaupt hereinkommt:

  • CMD_ALLOW_EMAIL_REGISTER: "false" – niemand kann sich selbst registrieren. Konten legst du auf der Kommandozeile an (Schritt 4).
  • CMD_ALLOW_ANONYMOUS: "false" – Gäste dürfen keine Notizen anlegen. Der Name führt in die Irre: Lesen dürfen Gäste trotzdem, sofern die jeweilige Notiz das erlaubt (Schritt 6). Steht der Wert auf true – so ist es ab Werk –, legt jeder Besucher über New oder eine freie Adresse Notizen an, die niemandem gehören und für jeden bearbeitbar sind. Bots, die das Netz nach WordPress absuchen, erzeugen so schon in den ersten Minuten Notizen wie /wp-json und /xmlrpc.php. Nebenbei dürfen mit false nur noch angemeldete Nutzer Bilder hochladen.
  • CMD_DEFAULT_PERMISSION: "limited" – neue Notizen sind für Gäste zu. Öffnen tust du sie einzeln. Diese Vorgabe gilt nur für Notizen mit Besitzer; eine Gast-Notiz steht immer auf „Freely“ – ein Grund mehr für CMD_ALLOW_ANONYMOUS: "false".

Schritt 3: Starten und nachsehen, ob es dicht ist

Terminal
docker compose up -d
docker compose ps
Ausgabe
NAME          STATUS
hedgedoc      Up 25 seconds (healthy)
hedgedoc-db   Up 50 seconds (healthy)

Beide Container melden healthy – die Datenbank antwortet auf pg_isready, HedgeDoc auf seinen eigenen Healthcheck. Jetzt die Gegenprobe von außen. HedgeDoc hat einen offenen Status-Endpunkt, der sich gut zum Prüfen eignet:

Terminal
curl -s https://DEINE_DOMAIN/status
Ausgabe
{"onlineNotes":0,"onlineUsers":0,"distinctOnlineUsers":0,"notesCount":0,"registeredUsers":0, …}

Und die drei Türen, die zu sein sollen:

Terminal
curl -s -o /dev/null -w "register: %{http_code}\n" -X POST https://DEINE_DOMAIN/register
curl -s -o /dev/null -w "neue Notiz: %{http_code} %{redirect_url}\n" https://DEINE_DOMAIN/new
curl -s -o /dev/null -w "freie URL: %{http_code} %{redirect_url}\n" https://DEINE_DOMAIN/kochabend
Ausgabe
register: 404
neue Notiz: 302 https://DEINE_DOMAIN/
freie URL: 302 https://DEINE_DOMAIN/

404 heißt: Die Registrierungsroute existiert gar nicht mehr. Bei /new und der freien Adresse kommt es auf das Ziel der Weiterleitung an: Die Startseite heißt, der Gast wurde weggeschickt. Zeigt die Weiterleitung dagegen auf eine neue Adresse wie https://DEINE_DOMAIN/JBdRlegoRAqN… oder https://DEINE_DOMAIN/kochabend, hat HedgeDoc gerade eine Notiz angelegt – dann steht CMD_ALLOW_ANONYMOUS noch auf true. Die Gegenprobe ist notesCount im Status-Endpunkt: Der muss bei 0 bleiben, solange du selbst nichts angelegt hast.

Schritt 4: Ein Konto anlegen – auf der Kommandozeile

Weil die Selbstregistrierung zu ist, legst du Konten mit dem mitgelieferten Werkzeug an. Achte auf die Reihenfolge der Argumente – --pass steht vorn, sonst zeigt das Skript nur seine Hilfe:

Terminal
docker compose exec app bin/manage_users --pass DEIN_PASSWORT --add DEINE@MAILADRESSE
Ausgabe
Using password from commandline...
Created user with email DEINE@MAILADRESSE

Dass es geklappt hat, bestätigt der Status-Endpunkt:

Terminal
curl -s https://DEINE_DOMAIN/status | grep -o '"registeredUsers":[0-9]*'
Ausgabe
"registeredUsers":1

Auf der Startseite gibt es jetzt nur noch Sign In – keinen Registrieren-Knopf:

Der Anmeldedialog von HedgeDoc mit den Feldern E-Mail und Passwort, ohne Möglichkeit zur Registrierung.
Anmelden ja, registrieren nein – Konten legt der Administrator an

Achtung

Weitere Konten kosten dich einen Befehl, aber sie sind der einzige saubere Weg. Schalte CMD_ALLOW_EMAIL_REGISTER nicht „nur kurz" auf true – eine offene HedgeDoc-Instanz im Netz ist innerhalb von Tagen ein fremder Datenspeicher.

Schritt 5: Die erste Notiz

Nach dem Anmelden legst du mit New eine Notiz an. Links tippst du Markdown, rechts steht sofort das Ergebnis:

Der HedgeDoc-Editor in der geteilten Ansicht: links Markdown-Quelltext mit Tabelle und Aufgabenliste, rechts die formatierte Vorschau.
Links Quelltext, rechts Ergebnis – Tabellen und Aufgabenlisten funktionieren direkt

Tabellen, Aufgabenlisten mit anklickbaren Kästchen, Zitate, Codeblöcke, MathJax und ein Präsentationsmodus sind eingebaut. Wer die Notiz gleichzeitig offen hat, sieht deine Änderungen sofort – oben rechts steht, wie viele Leute gerade dabei sind.

Schritt 6: Fünf Stufen, wer was darf

Oben rechts über der Vorschau steht die aktuelle Berechtigung der Notiz. Ein Klick öffnet die Auswahl:

Das Berechtigungsmenü einer HedgeDoc-Notiz mit den fünf Stufen von Editable bis Private.
Fünf Stufen – der Zusatz forbid guests ist der entscheidende Unterschied

Die Stufen von offen nach zu:

  • Editable – angemeldete Nutzer dürfen bearbeiten, Gäste dürfen lesen
  • Limited – angemeldete Nutzer dürfen bearbeiten, Gäste bleiben ganz draußen
  • Locked – nur der Besitzer bearbeitet, Gäste dürfen lesen
  • Protected – nur der Besitzer bearbeitet, Gäste bleiben draußen
  • Private – nur der Besitzer sieht die Notiz überhaupt

Eine sechste Stufe, Freely („Anyone can edit“), bietet HedgeDoc nur an, wenn Gäste mitschreiben dürfen. Mit CMD_ALLOW_ANONYMOUS: "false" fehlt sie im Menü.

Der Zusatz „forbid guests" ist der Punkt, an dem man sich vertut. Eine Notiz auf Limited ist für Außenstehende auch dann unsichtbar, wenn du sie veröffentlichst – der Aufruf endet mit einer Weiterleitung. Willst du jemandem ohne Konto etwas zu lesen geben, ist Locked die richtige Stufe. Der Unterschied ist eindeutig: Dieselbe veröffentlichte Notiz liefert einem Gast mit Limited eine 302 und mit Locked eine 200.

Der Schalter, auf den du dich nicht verlassen darfst

CMD_ALLOW_ANONYMOUS_EDITS klingt so, als könne man damit Gästen das Schreiben verbieten. Das tut er nicht – er kann es nur erlauben. Ab Werk steht er auf false und ist trotzdem wirkungslos, solange CMD_ALLOW_ANONYMOUS auf true steht: Dann bietet HedgeDoc „Freely" an, und auf einer solchen Notiz schreibt jeder Browser ohne Anmeldung mit. Setzt du CMD_ALLOW_ANONYMOUS_EDITS bei unserem CMD_ALLOW_ANONYMOUS: "false" auf true, holst du genau diese Stufe zurück. Lass ihn deshalb weg. Was Gäste dürfen, entscheidet allein die Berechtigung der Notiz – und „Freely" ist mit unserer Konfiguration gar nicht erst wählbar.

Schritt 7: Weitergeben

Über Publish bekommt die Notiz eine zweite, aufgeräumte Adresse unter /s/… – ohne Editor, ohne Werkzeugleiste, nur der fertige Text. Das ist die Fassung, die du verschickst:

Die veröffentlichte Ansicht einer HedgeDoc-Notiz ohne Editor, mit Tabelle, Aufgabenliste und Zähler für die Aufrufe.
Die veröffentlichte Fassung – lesbar für alle, die den Link haben

Der Editor bleibt unter der ursprünglichen Adresse erreichbar; die veröffentlichte Fassung zieht bei jeder Änderung nach. Wer die Notiz nur lesen können soll, bekommt den /s/-Link und die Notiz steht auf Locked.

Wenn es nicht funktioniert

Der Datenbank-Container startet nicht und meldet etwas über pg_ctlcluster und Verzeichnisnamen. Das ist die PostgreSQL-18-Umstellung: Ab dieser Hauptversion darf der Mount nicht mehr auf /var/lib/postgresql/data zeigen, sondern muss eine Ebene höher auf /var/lib/postgresql. Der Container bricht auch dann ab, wenn das Volume nagelneu und leer ist – die Prüfung hängt am Mount, nicht am Inhalt. Bei einer bestehenden Datenbank reicht das Verschieben des Mounts allerdings nicht; dann brauchst du erst einen pg_dump mit der alten Hauptversion und einen Import in die neue.

Nach dem Umstellen des Mounts kommt mkdir: can't create directory '/var/lib/postgresql/18/': Permission denied. Du benutzt einen Bind-Mount, und das Verzeichnis auf dem Host gehört root – der Postgres-Benutzer im Container darf darin nichts anlegen. Am alten Pfad hat das Image die Rechte noch selbst korrigiert, am neuen nicht mehr. Entweder du gibst das Verzeichnis dem Container-Benutzer, oder du nimmst wie hier ein benanntes Volume, bei dem Docker die Rechte aus dem Image übernimmt.

Verschickte Links zeigen auf localhost oder tragen einen Port im Namen. HedgeDoc baut Links nicht aus dem Host-Header, sondern aus der Konfiguration. Setz CMD_DOMAIN auf deine Domain (nur den Namen, kein https://, kein Pfad), CMD_PROTOCOL_USESSL auf true und CMD_URL_ADDPORT auf false. Nach einer Änderung muss der Container neu gestartet werden, damit sie greift.

Ein Gast hat deine Notiz bearbeitet, obwohl CMD_ALLOW_ANONYMOUS_EDITS auf false steht. Diese Option wirkt ausschließlich dann, wenn CMD_ALLOW_ANONYMOUS auf false steht – bei true ist sie wirkungslos. Entscheidend ist die Berechtigung der einzelnen Notiz: Steht sie auf Freely, darf jeder mit dem Link schreiben. Setz CMD_ALLOW_ANONYMOUS auf false, starte den Container neu und stell die Notiz auf Limited oder Locked.

In der Notizliste tauchen Notizen wie wp-json oder xmlrpc.php auf, die niemand angelegt hat. Das waren Bots, die nach WordPress suchen. Bei CMD_ALLOW_ANONYMOUS: "true" zusammen mit CMD_ALLOW_FREEURL: "true" legt jeder Aufruf einer freien Adresse eine neue Notiz an. Stell CMD_ALLOW_ANONYMOUS auf false und starte den Stack neu. Die bereits angelegten Notizen haben keinen Besitzer, der sie in der Oberfläche löschen könnte; du entfernst sie in der Datenbank: docker compose exec database psql -U hedgedoc -d hedgedoc -c 'DELETE FROM "Notes" WHERE "ownerId" IS NULL;' – ihre Revisionen verschwinden dabei mit.

Ein Außenstehender bekommt beim veröffentlichten Link nur eine Weiterleitung auf die Startseite. Dann steht die Notiz auf Limited oder Protected – beide Stufen sperren Gäste komplett aus, auch bei veröffentlichten Fassungen. Für „nur lesen, aber für alle" ist Locked die richtige Wahl. CMD_ALLOW_ANONYMOUS hat damit nichts zu tun: Die Option regelt nur, ob Gäste Notizen anlegen dürfen, nicht, ob sie welche lesen.

Dein Problem ist nicht dabei? Alle Fehlermeldungen durchsuchen →

Wartung & Backups

Updates laufen über den Image-Tag. HedgeDoc 1.x veröffentlicht alle paar Wochen eine Minor-Version; die Datenbankmigration passiert beim Start:

Terminal
cd /opt/hedgedoc
docker compose exec -T database pg_dump -U hedgedoc hedgedoc > vor-update-$(date +%F).sql
sed -i 's/hedgedoc:1.12.0/hedgedoc:NEUE_VERSION/' compose.yaml
docker compose pull && docker compose up -d
docker compose logs app --tail 10

Der Dump davor ist keine Förmlichkeit – er ist der einzige Weg zurück, wenn eine Migration schiefgeht. Die Hauptversion von PostgreSQL fasst du dabei nicht mit an. Ein Sprung von 18 auf 19 braucht immer Dump und Import; ein bloßes Hochsetzen des Image-Tags endet damit, dass der Container mit einer Versionsmeldung stehen bleibt.

Sichern musst du drei Dinge:

Ausgabe
pg_dump-Ausgabe          # Notizen, Konten, Berechtigungen
/opt/hedgedoc/uploads/   # hochgeladene Bilder
/opt/hedgedoc/.env       # Datenbankpasswort und Session-Secret

Das .env gehört ausdrücklich dazu: Ohne das CMD_SESSION_SECRET sind nach einer Wiederherstellung alle Sitzungen ungültig, ohne das Datenbankpasswort kommt die Anwendung nicht an ihre eigenen Daten. Ein täglicher Dump per systemd-Timer und beides zusammen ins Restic-Backup – mehr braucht es nicht.

Realistischer Aufwand: wenig. Ein Update pro Quartal, ein Blick auf die Notizliste, wenn du merkst, dass niemand mehr aufräumt. Der Punkt, den du dir im Kalender notieren solltest, ist die Postgres-Hauptversion: Solange du beim gepinnten Tag bleibst, passiert nichts – aber irgendwann läuft der Support aus, und dann ist Dump-und-Import fällig.

Wenn es nur intern sein soll

Eine HedgeDoc-Instanz, die nur dein Haushalt oder Team braucht, muss nicht im offenen Netz stehen. Hinter einer IP-Beschränkung oder komplett im VPN sparst du dir jede Diskussion über Registrierung und Gastzugriff – das Muster steht in Dienste nur im VPN erreichbar machen.

Feedback per E-Mail: feedback@serverkueche.de

Das könnte dir auch schmecken