Development10 min read

Self-Host HedgeDoc on VPS: The Collaborative Markdown Editor

Google Docs works fine until your team prefers writing technical documentation in Markdown, integrating code blocks with syntax highlighting, or simply avoiding hosting meeting notes and specs at Google. HedgeDoc (formerly CodiMD) is a real-time collaborative Markdown editor, open-source (AGPL-3.0), deployable in under fifteen minutes on a VPS with Docker Compose. Each note has a shareable URL, editing is simultaneous with multiple cursors, and your data stays on your own infrastructure.

Why HedgeDoc Instead of Notion or Google Docs?

HedgeDoc gives your technical team: native Markdown with real-time rendering, collaborative editing with multiple simultaneous cursors, code blocks with syntax highlighting for 200+ languages, embedded diagrams (Mermaid, PlantUML, Vega-lite, flowcharts), math formulas (LaTeX via MathJax), one-click export to PDF, Markdown, or HTML, and hosting on your own VPS — your data never leaves your infrastructure.

Prerequisites Before You Start

  • A VPS running Ubuntu 22.04 or Debian 12 with at least 1 GB of RAM.
  • Docker Engine ≥ 24 and Docker Compose V2 installed.
  • A domain name pointing to your VPS (required for TLS — WebSockets require a secure connection).
  • Port 3000 available (HedgeDoc default port).
  • Nginx installed for the reverse proxy (with WebSocket support — essential).

Installing HedgeDoc with Docker Compose

01

Step 1 — Create the project structure

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

Create docker-compose.yml with PostgreSQL and HedgeDoc containers, using image quay.io/hedgedoc/hedgedoc:1.11.1. Bind app to 127.0.0.1:3000:3000. Set depends_on: database: condition: service_healthy.

02

Step 2 — Create the environment file

cat > /opt/hedgedoc/.env << 'EOF'
POSTGRES_PASSWORD=STRONG_PASSWORD_HERE
CMD_DOMAIN=hedgedoc.yourdomain.com
CMD_SESSION_SECRET=A_LONG_FIXED_RANDOM_STRING
EOF

⚠️ CMD_SESSION_SECRET must be fixed and never changed after first launch. Generate it with openssl rand -base64 32.

03

Step 3 — Start the containers

docker compose up -d
docker compose ps
docker compose logs app

HedgeDoc will automatically migrate the PostgreSQL database on first startup. Once logs show listening on port 3000, the app is ready.

04

Step 4 — Configure Nginx with WebSocket support

⚠️ The Nginx configuration for HedgeDoc must include a /socket.io/ block with WebSocket headers. Missing this block causes silent real-time collaboration failure.

server {
    server_name hedgedoc.yourdomain.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    location /socket.io/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

Then run certbot --nginx -d hedgedoc.yourdomain.com.

05

Step 5 — Create the First Account

Go to https://notes.yourdomain.com and register your first account (email + password). This account becomes the admin.

To disable public registration after initial setup, add to .env:

CMD_ALLOW_REGISTRATION=false
CMD_ALLOW_ANONYMOUS=false

Then recreate the container: docker compose up -d hedgedoc.

Features to Discover

Share a note: each HedgeDoc note has a unique URL. Click the share button to get the URL and choose the access mode (read-only, comments, editing).

Insert a Mermaid diagram:

graph LR
    A[Client] --> B[API]
    B --> C[Database]

Export the note: in the menu (≡ icon), choose Export to download as Markdown, HTML, or PDF.

Presentation mode: add --- between sections to turn them into slides.

Missing Socket.io Block = Silently Broken Collaboration

Symptom: the interface looks normal, you can type in notes, but other users' changes don't appear in real time — no visible error message.

Cause: the /socket.io/ route requires a WebSocket connection. Without the dedicated Nginx block that sends Upgrade: websocket and Connection: upgrade headers, Nginx treats the request as plain HTTP and the real-time connection fails silently.

Quick check:

curl -v -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
  https://hedgedoc.yourdomain.com/socket.io/?transport=websocket

You should see 101 Switching Protocols. A 200 or 400 means the /socket.io/ block is missing.

Proxy Trailing Slash = Blank Page or 404

In Nginx's proxy_pass directive, the trailing slash changes behavior:

# CORRECT — no trailing slash
proxy_pass http://127.0.0.1:3000;

# INCORRECT — trailing slash rewrites the path
proxy_pass http://127.0.0.1:3000/;

With a trailing slash, Nginx rewrites the path, breaking HedgeDoc's internal routing. Always omit the trailing slash.

HedgeDoc vs Notion vs Confluence

CriterionHedgeDoc (self-hosted)NotionConfluence (Cloud)
PriceFree (VPS cost)Free up to 10 members, ~$10/member/mo~$5.75/user/mo (min 10)
FormatNative MarkdownProprietary blocksRich editor (WYSIWYG)
Native diagramsMermaid, PlantUML, Vega-liteLimited (via integrations)Via macros
Code blocks200+ languages with highlightingYes (limited)Yes (via plugin)
Data hostingYour VPSNotion servers (US)Atlassian servers
Markdown exportYes (native)PartialNot native
Presentation modeYes (built-in)NoVia plugin
Real-time collaborationYes (WebSocket)YesYes
LaTeX formulasYes (MathJax)YesVia plugin

Backup and Updates

# Backup PostgreSQL
docker compose exec -T database pg_dump -U hedgedoc hedgedoc | gzip > /opt/hedgedoc/backups/hedgedoc-$(date +%Y%m%d).sql.gz

# Update HedgeDoc
cd /opt/hedgedoc
docker compose pull app
docker compose up -d app

Database migrations apply automatically on new version startup. Check logs to confirm successful migrations. Current image: quay.io/hedgedoc/hedgedoc:1.11.1.

Manage Users and Permissions

HedgeDoc manages three access levels per note: Freely (anyone can edit without an account), Editable (logged-in users only can edit), Limited (owner edits, others comment), Locked (read-only for all except owner), Private (owner-only access).

Disable public registration: set CMD_ALLOW_REGISTRATION: 'false' in .env and recreate the container. Create accounts via CLI: docker compose exec app npm run manage_users -- --add [email protected] --password Password.

Enable OAuth (GitHub, GitLab…): configure CMD_GITHUB_CLIENTID / CMD_GITHUB_CLIENTSECRET in .env.

Troubleshoot Common Issues

Interface loads but real-time collaboration doesn't work: check the /socket.io/ block in Nginx (see the dedicated tip above).

502 Bad Gateway after startup: wait 60 seconds for containers to be ready. Check that database is healthy with docker compose ps. If not, inspect: docker compose logs database.

Uploaded images disappear after restart: check that the uploads volume is correctly mounted (type volume, not bind) with docker inspect hedgedoc-app-1 | grep Mounts.

PDF export error: if Chromium is not included in your Docker image version, disable PDF export with CMD_ALLOW_PDF_EXPORT: 'false'.

A VPS for Your Documentation Collaboration Stack

HedgeDoc runs comfortably on 1 GB of RAM. Our VPS plans start at a few euros per month and include daily snapshots to protect your team notes. Your tech specs, RFCs, and meeting notes stay on your own infrastructure.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.