<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Markdown – Serverküche</title><link>https://serverkueche.de/en/tags/markdown/</link><description>Markdown – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Tue, 03 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/markdown/index.xml" rel="self" type="application/rss+xml"/><item><title>HedgeDoc: Shared Markdown Notes in Real Time</title><link>https://serverkueche.de/en/tutorials/hedgedoc-markdown-notes/</link><pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/hedgedoc-markdown-notes/</guid><description>HedgeDoc 1.12 with Postgres 18 behind Traefik: type notes together live, share links – and set the permissions so they actually hold.</description><content:encoded><![CDATA[<p>An Etherpad that speaks Markdown: two people type into the same note at once, the formatted version
appears live on the right, and at the end you send a link. HedgeDoc does exactly that – and ships a
few defaults you should know about before the note is out on the internet.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We run <strong>HedgeDoc 1.12.0</strong> with <strong>PostgreSQL 18.6</strong> behind Traefik. At the end you have an editor
where several people work on the same Markdown note simultaneously, with tables, task lists and a
live preview. Notes can be handed on as a link – read-only or editable, exactly as you configure it.</p>
<p>The second part matters more than the first: HedgeDoc arrives <strong>open</strong>. Anyone may register, and
every visitor may create notes – without an account. We turn that around – closed as the base
state, opened per note – and look closely while doing it, because two of the settings do not do
what their names suggest.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li><a href="/en/tutorials/traefik-reverse-proxy/">Traefik as a reverse proxy</a> with a working Let&rsquo;s Encrypt
resolver and a domain pointing at the server</li>
<li>You need to know the domain <strong>in advance</strong>: HedgeDoc builds every link from one configured
domain, and moving afterwards is unpleasant</li>
<li>Around 1 GB of free memory for the application and database together</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/hedgedoc-markdown-notes/" data-content-piece="VPS 1000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 128 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €14.50/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">HedgeDoc and Postgres run comfortably together on the small VPS.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12.5-iv-24m-eu?ref=44083#vps-1000-g12.5-iv-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-1000" data-voucher-msg-empty="No code is available for this plan right now – use the €5.95 code." data-voucher-msg-limit="No more codes today – use the €5.95 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5.95 code."
     data-voucher-hint="Empty the cart first – the code adds the server itself.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Show code</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">another code</button>
  </p><p class="mt-1 text-xs text-slate-500 dark:text-slate-400" data-voucher-hint-target>
    Single use, valid for VPS 1000 G12.5.
    <a href="https://www.netcup.com/en/checkout/cart?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 1000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Redeem in the cart →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">€5.95 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-the-stack--with-a-current-postgres-trap">Step 1: The stack – with a current Postgres trap</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/hedgedoc <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/hedgedoc</span></span></code></pre></div>
</div>
<p>Secrets first. HedgeDoc signs its session cookies with them; without your own value it picks a
random one and every login is dropped on each restart:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">umask</span> <span class="m">077</span>
</span></span><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;POSTGRES_PASSWORD=</span><span class="k">$(</span>openssl rand -base64 <span class="m">24</span> <span class="p">|</span> tr -d <span class="s1">&#39;=+/&#39;</span> <span class="p">|</span> cut -c1-24<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;CMD_SESSION_SECRET=</span><span class="k">$(</span>openssl rand -hex 32<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span> &gt; .env</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># /opt/hedgedoc/compose.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:18.6-alpine</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hedgedoc-db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">hedgedoc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${POSTGRES_PASSWORD}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">hedgedoc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">pgdata:/var/lib/postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">hedgedoc-internal]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;pg_isready -U hedgedoc&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">quay.io/hedgedoc/hedgedoc:1.12.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">hedgedoc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_DB_URL</span><span class="p">:</span><span class="w"> </span><span class="l">postgres://hedgedoc:${POSTGRES_PASSWORD}@database:5432/hedgedoc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_SESSION_SECRET</span><span class="p">:</span><span class="w"> </span><span class="l">${CMD_SESSION_SECRET}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_DOMAIN</span><span class="p">:</span><span class="w"> </span><span class="l">YOUR_DOMAIN</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_PROTOCOL_USESSL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_URL_ADDPORT</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_ALLOW_EMAIL_REGISTER</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_ALLOW_ANONYMOUS</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_DEFAULT_PERMISSION</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;limited&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CMD_ALLOW_FREEURL</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./uploads:/hedgedoc/public/uploads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">hedgedoc-internal, proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.enable=true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.hedgedoc.rule=Host(`YOUR_DOMAIN`)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.hedgedoc.entrypoints=websecure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.routers.hedgedoc.tls.certresolver=le</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">traefik.http.services.hedgedoc.loadbalancer.server.port=3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pgdata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">hedgedoc-internal</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">internal</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Two things about this stack are deliberate. The database sits in its <strong>own internal network</strong>
(<code>internal: true</code>) and is therefore unreachable both from outside and from other containers in the
<code>proxy</code> network – only HedgeDoc, which is in both, gets to it. And the <code>healthcheck</code> together with
<code>depends_on: condition: service_healthy</code> stops HedgeDoc from starting before Postgres is ready;
otherwise it aborts on its first connection attempt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>PostgreSQL 18 changed the data path
  </p>
  <div class="prose-kitchen text-sm"><p>Older guides – including the HedgeDoc project&rsquo;s own Compose file – mount the database under
<code>/var/lib/postgresql/**data**</code>. With PostgreSQL 18 the container then <strong>refuses to start at all</strong>,
even with an empty volume:</p>
<p><code>Error: in 18+, these Docker images are configured to store database data in a format which is compatible with &quot;pg_ctlcluster&quot; … The suggested container configuration for 18+ is to place a single mount at /var/lib/postgresql</code></p>
<p>So the mount has to move one level up, to <code>/var/lib/postgresql</code>. That applies to named volumes and
bind mounts alike.</p>
</div>
</div>
<p>Why a <strong>named volume</strong> and not a <code>./pgdata</code> directory? Because a bind mount at the new path has to
belong to the container, and otherwise fails with <code>mkdir: can't create directory '/var/lib/postgresql/18/': Permission denied</code> – at the old path the image still fixed that up
itself. You do not need the directory for backups anyway: a database backup is made with <code>pg_dump</code>,
not with a file copy.</p>
<h3 id="step-2-the-variables-that-decide-links-and-security">Step 2: The variables that decide links and security</h3>
<p>Four of them concern the <strong>addresses</strong> HedgeDoc generates:</p>
<ul>
<li><strong><code>CMD_DOMAIN</code></strong> – your domain, without protocol and without path. Leave it out and your links
say <code>localhost</code>.</li>
<li><strong><code>CMD_PROTOCOL_USESSL: &quot;true&quot;</code></strong> – produces <code>https://</code> links. The container itself still speaks
HTTP; Traefik does the TLS.</li>
<li><strong><code>CMD_URL_ADDPORT: &quot;false&quot;</code></strong> – without it HedgeDoc appends a port to every URL.</li>
<li><strong><code>CMD_ALLOW_FREEURL: &quot;true&quot;</code></strong> – allows speaking addresses such as <code>/cooking-night</code> instead of a
random ID: whoever opens an address that is still free creates a new note there. With the next
setting, that only applies to signed-in users.</li>
</ul>
<p>The other three decide who gets in at all:</p>
<ul>
<li><strong><code>CMD_ALLOW_EMAIL_REGISTER: &quot;false&quot;</code></strong> – nobody can register themselves. You create accounts on
the command line (step 4).</li>
<li><strong><code>CMD_ALLOW_ANONYMOUS: &quot;false&quot;</code></strong> – guests may <strong>not create notes</strong>. The name is misleading:
guests may still read, as far as the individual note permits (step 6). If the value is <code>true</code> –
the default – every visitor creates notes via <strong>New</strong> or a free address, and those notes
<strong>belong to nobody and are editable by anyone</strong>. Bots scanning the internet for WordPress create
notes such as <code>/wp-json</code> and <code>/xmlrpc.php</code> that way within the first few minutes. As a side
effect, with <code>false</code> only signed-in users may upload images.</li>
<li><strong><code>CMD_DEFAULT_PERMISSION: &quot;limited&quot;</code></strong> – new notes are closed to guests. You open them
individually. This default only applies to notes with an owner; a guest note is always on
&ldquo;Freely&rdquo; – one more reason for <code>CMD_ALLOW_ANONYMOUS: &quot;false&quot;</code>.</li>
</ul>
<h3 id="step-3-start-it-and-check-that-it-is-closed">Step 3: Start it and check that it is closed</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d
</span></span><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME          STATUS
</span></span><span class="line"><span class="cl">hedgedoc      Up 25 seconds (healthy)
</span></span><span class="line"><span class="cl">hedgedoc-db   Up 50 seconds (healthy)</span></span></code></pre></div>
</div>
<p>Both containers report <code>healthy</code> – the database answers <code>pg_isready</code>, HedgeDoc its own healthcheck.
Now the counter-test from outside. HedgeDoc has an open status endpoint that works nicely for
checking:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/status</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;onlineNotes&#34;:0,&#34;onlineUsers&#34;:0,&#34;distinctOnlineUsers&#34;:0,&#34;notesCount&#34;:0,&#34;registeredUsers&#34;:0, …}</span></span></code></pre></div>
</div>
<p>And the three doors that should be shut:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;register: %{http_code}\n&#34;</span> -X POST https://YOUR_DOMAIN/register
</span></span><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;new note: %{http_code} %{redirect_url}\n&#34;</span> https://YOUR_DOMAIN/new
</span></span><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;free URL: %{http_code} %{redirect_url}\n&#34;</span> https://YOUR_DOMAIN/cooking-night</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">register: 404
</span></span><span class="line"><span class="cl">new note: 302 https://YOUR_DOMAIN/
</span></span><span class="line"><span class="cl">free URL: 302 https://YOUR_DOMAIN/</span></span></code></pre></div>
</div>
<p><code>404</code> means the registration route does not exist any more. For <code>/new</code> and the free address, the
<strong>target</strong> of the redirect is what counts: the start page means the guest was sent away. If the
redirect points to a new address such as <code>https://YOUR_DOMAIN/JBdRlegoRAqN…</code> or
<code>https://YOUR_DOMAIN/cooking-night</code> instead, HedgeDoc has just created a note – then
<code>CMD_ALLOW_ANONYMOUS</code> is still <code>true</code>. The counter-check is <code>notesCount</code> in the status endpoint: it
has to stay at <code>0</code> as long as you have not created anything yourself.</p>
<h3 id="step-4-create-an-account--on-the-command-line">Step 4: Create an account – on the command line</h3>
<p>Because self-registration is closed, you create accounts with the bundled tool. <strong>Mind the argument
order</strong> – <code>--pass</code> goes first, otherwise the script only prints its help:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose <span class="nb">exec</span> app bin/manage_users --pass YOUR_PASSWORD --add YOUR@EMAIL</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Using password from commandline...
</span></span><span class="line"><span class="cl">Created user with email YOUR@EMAIL</span></span></code></pre></div>
</div>
<p>The status endpoint confirms it worked:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/status <span class="p">|</span> grep -o <span class="s1">&#39;&#34;registeredUsers&#34;:[0-9]*&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&#34;registeredUsers&#34;:1</span></span></code></pre></div>
</div>
<p>The start page now only offers <strong>Sign In</strong> – no register button:</p>
<p><figure class="my-6"><img src="/en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_37a9cb1c5e60b279.webp" srcset="/en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_887d8bdbb892268f.webp 480w, /en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_37a9cb1c5e60b279.webp 768w, /en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_4455750d3345cccc.webp 1200w, /en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_3457b239af456975.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/hedgedoc-markdown-notes/hd-signin_hu_cad4d6633a46392c.webp"
    alt="The HedgeDoc sign-in dialog with email and password fields and no way to register." title="Signing in yes, registering no – the administrator creates accounts"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Signing in yes, registering no – the administrator creates accounts</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">Additional accounts cost you one command, but that is the only clean route. Do not switch
<code>CMD_ALLOW_EMAIL_REGISTER</code> to <code>true</code> &ldquo;just for a moment&rdquo; – an open HedgeDoc instance on the
internet becomes someone else&rsquo;s data store within days.</div>
</div>
<h3 id="step-5-the-first-note">Step 5: The first note</h3>
<p>After signing in you create a note with <strong>New</strong>. You type Markdown on the left and the result
appears immediately on the right:</p>
<p><figure class="my-6"><img src="/en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_4716793e7ec55b5a.webp" srcset="/en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_a18b6d7a405f9d84.webp 480w, /en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_4716793e7ec55b5a.webp 768w, /en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_c4b9c9f97d11db78.webp 1200w, /en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_f259d848114c1ba7.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/hedgedoc-markdown-notes/hd-editor_hu_98de01d7a3c80007.webp"
    alt="The HedgeDoc editor in split view: Markdown source with a table and task list on the left, the rendered preview on the right." title="Source on the left, result on the right – tables and task lists work out of the box"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Source on the left, result on the right – tables and task lists work out of the box</figcaption></figure></p>
<p>Tables, task lists with clickable boxes, quotes, code blocks, MathJax and a presentation mode are
all built in. Anyone with the note open sees your changes instantly – the top right shows how many
people are currently in.</p>
<h3 id="step-6-five-levels-of-who-may-do-what">Step 6: Five levels of who may do what</h3>
<p>Above the preview on the right is the note&rsquo;s current permission. One click opens the list:</p>
<p><figure class="my-6"><img src="/en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_7f02ff6ad6e232bf.webp" srcset="/en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_9fc27c1ca2913e04.webp 480w, /en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_7f02ff6ad6e232bf.webp 768w, /en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_8ad289a9d5d69f38.webp 1200w, /en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_3013812d3c6be37c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/hedgedoc-markdown-notes/hd-permission_hu_e123a0184659fa56.webp"
    alt="The permission menu of a HedgeDoc note showing the five levels from Editable to Private." title="Five levels – the words forbid guests make the decisive difference"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Five levels – the words forbid guests make the decisive difference</figcaption></figure></p>
<p>The levels from open to closed:</p>
<ul>
<li><strong>Editable</strong> – signed-in users may edit, guests may read</li>
<li><strong>Limited</strong> – signed-in users may edit, <strong>guests are kept out entirely</strong></li>
<li><strong>Locked</strong> – only the owner edits, guests may read</li>
<li><strong>Protected</strong> – only the owner edits, guests are kept out</li>
<li><strong>Private</strong> – only the owner sees the note at all</li>
</ul>
<p>A sixth level, <strong>Freely</strong> (&ldquo;Anyone can edit&rdquo;), is only offered when guests may write. With
<code>CMD_ALLOW_ANONYMOUS: &quot;false&quot;</code> it is missing from the menu.</p>
<p>The &ldquo;forbid guests&rdquo; suffix is where people get it wrong. A note set to <strong>Limited</strong> is invisible to
outsiders even when you publish it – the request ends in a redirect. If you want to give someone
without an account something to read, <strong>Locked</strong> is the right level. The difference is
unambiguous: the same published note returns <code>302</code> to a guest on <code>Limited</code> and <code>200</code> on <code>Locked</code>.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>The switch you must not rely on
  </p>
  <div class="prose-kitchen text-sm"><code>CMD_ALLOW_ANONYMOUS_EDITS</code> sounds like it forbids guests from writing. It does not – it can only
<strong>allow</strong> it. It defaults to <code>false</code> and still has no effect as long as <code>CMD_ALLOW_ANONYMOUS</code> is
<code>true</code>: then HedgeDoc offers &ldquo;Freely&rdquo;, and on such a note any browser writes along without signing
in. If you set <code>CMD_ALLOW_ANONYMOUS_EDITS</code> to <code>true</code> alongside our <code>CMD_ALLOW_ANONYMOUS: &quot;false&quot;</code>,
you bring exactly that level back. So leave it out. What guests may do is decided <strong>solely by the
note&rsquo;s permission</strong> – and with our configuration &ldquo;Freely&rdquo; cannot even be selected.</div>
</div>
<h3 id="step-7-handing-it-on">Step 7: Handing it on</h3>
<p><strong>Publish</strong> gives the note a second, tidied-up address under <code>/s/…</code> – no editor, no toolbar, just
the finished text. That is the version you send out:</p>
<p><figure class="my-6"><img src="/en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_464c8f2699d12b53.webp" srcset="/en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_e4ec310d3a313b6a.webp 480w, /en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_464c8f2699d12b53.webp 768w, /en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_1e1fb97c84a6eadb.webp 1200w, /en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_c8124522752a4d4c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/hedgedoc-markdown-notes/hd-publish_hu_1bb0cce1225e90bf.webp"
    alt="The published view of a HedgeDoc note without an editor, showing the table, task list and a view counter." title="The published version – readable by everyone who has the link"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The published version – readable by everyone who has the link</figcaption></figure></p>
<p>The editor stays available at the original address; the published version follows along with every
change. Anyone who should only read gets the <code>/s/</code> link <strong>and</strong> the note is set to <code>Locked</code>.</p>
<h2 id="when-it-doesnt-work">When it doesn&rsquo;t work</h2>
<div class="troubleshoot not-prose">
<p><strong>The database container refuses to start and says something about <code>pg_ctlcluster</code> and directory names.</strong> That is the PostgreSQL 18 change: from that major version the mount must no longer point at <code>/var/lib/postgresql/data</code> but one level up at <code>/var/lib/postgresql</code>. The container also aborts when the volume is brand new and empty – the check hangs on the mount, not the content. For an <strong>existing</strong> database, moving the mount is not enough; there you first need a <code>pg_dump</code> with the old major version and an import into the new one.</p>
<p><strong>After moving the mount you get <code>mkdir: can't create directory '/var/lib/postgresql/18/': Permission denied</code>.</strong> You are using a bind mount and the directory on the host belongs to root – the Postgres user inside the container cannot create anything in it. At the old path the image still fixed the permissions itself, at the new one it does not. Either hand the directory to the container user, or use a named volume as we do here, where Docker inherits the permissions from the image.</p>
<p><strong>Links you send point at <code>localhost</code> or carry a port.</strong> HedgeDoc does not build links from the <code>Host</code> header but from its configuration. Set <code>CMD_DOMAIN</code> to your domain (name only, no <code>https://</code>, no path), <code>CMD_PROTOCOL_USESSL</code> to <code>true</code> and <code>CMD_URL_ADDPORT</code> to <code>false</code>. The container has to be restarted for a change to take effect.</p>
<p><strong>A guest edited your note even though <code>CMD_ALLOW_ANONYMOUS_EDITS</code> is <code>false</code>.</strong> That option only works when <code>CMD_ALLOW_ANONYMOUS</code> is <code>false</code>; with <code>true</code> it has no effect. What counts is the individual note&rsquo;s permission: if it is on <code>Freely</code>, anyone with the link may write. Set <code>CMD_ALLOW_ANONYMOUS</code> to <code>false</code>, restart the container and set the note to <code>Limited</code> or <code>Locked</code>.</p>
<p><strong>Notes such as <code>wp-json</code> or <code>xmlrpc.php</code> show up that nobody created.</strong> Those were bots looking for WordPress. With <code>CMD_ALLOW_ANONYMOUS: &quot;true&quot;</code> together with <code>CMD_ALLOW_FREEURL: &quot;true&quot;</code>, every request to a free address creates a new note. Set <code>CMD_ALLOW_ANONYMOUS</code> to <code>false</code> and restart the stack. The notes already created have no owner who could delete them in the interface; you remove them in the database: <code>docker compose exec database psql -U hedgedoc -d hedgedoc -c 'DELETE FROM &quot;Notes&quot; WHERE &quot;ownerId&quot; IS NULL;'</code> – their revisions go with them.</p>
<p><strong>An outsider only gets a redirect to the start page from the published link.</strong> Then the note is on <code>Limited</code> or <code>Protected</code> – both levels lock guests out completely, published versions included. For &ldquo;read only, but for everyone&rdquo;, <code>Locked</code> is the right choice. <code>CMD_ALLOW_ANONYMOUS</code> has nothing to do with it: the option only controls whether guests may <strong>create</strong> notes, not whether they may read them.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Updates</strong> go through the image tag. HedgeDoc 1.x publishes a minor version every few weeks; the
database migration happens at startup:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> /opt/hedgedoc
</span></span><span class="line"><span class="cl">docker compose <span class="nb">exec</span> -T database pg_dump -U hedgedoc hedgedoc &gt; before-update-<span class="k">$(</span>date +%F<span class="k">)</span>.sql
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s/hedgedoc:1.12.0/hedgedoc:NEW_VERSION/&#39;</span> compose.yaml
</span></span><span class="line"><span class="cl">docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d
</span></span><span class="line"><span class="cl">docker compose logs app --tail <span class="m">10</span></span></span></code></pre></div>
</div>
<p>The dump beforehand is not a formality – it is the only way back when a migration goes wrong. <strong>Do
not touch the PostgreSQL major version at the same time.</strong> Going from 18 to 19 always needs a dump
and an import; simply bumping the image tag ends with the container sitting there printing a
version complaint.</p>
<p><strong>Three things need backing up:</strong></p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pg_dump output           # notes, accounts, permissions
</span></span><span class="line"><span class="cl">/opt/hedgedoc/uploads/   # uploaded images
</span></span><span class="line"><span class="cl">/opt/hedgedoc/.env       # database password and session secret</span></span></code></pre></div>
</div>
<p>The <code>.env</code> explicitly belongs in there: without the <code>CMD_SESSION_SECRET</code> every session is invalid
after a restore, and without the database password the application cannot reach its own data. A
daily dump via a systemd timer and both of them into your
<a href="/en/tutorials/restic-backups/">Restic backup</a> – that is all it takes.</p>
<p><strong>Realistic effort:</strong> low. One update per quarter, a glance at the note list when you notice nobody
is tidying up. The item worth putting in your calendar is the Postgres major version: as long as
you stay on the pinned tag nothing happens – but support runs out eventually, and then dump and
import are due.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>If it only needs to be internal
  </p>
  <div class="prose-kitchen text-sm">A HedgeDoc instance that only your household or team needs does not have to sit on the open
internet. Behind an IP restriction or entirely inside the VPN you save yourself every discussion
about registration and guest access – the pattern is in
<a href="/en/tutorials/internal-services-vpn-only/">making services reachable only over VPN</a>.</div>
</div>
]]></content:encoded></item></channel></rss>