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
Step 1 — Create the project structure
mkdir -p /opt/hedgedoc && cd /opt/hedgedocCreate 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.
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.
Step 3 — Start the containers
docker compose up -d
docker compose ps
docker compose logs appHedgeDoc will automatically migrate the PostgreSQL database on first startup. Once logs show listening on port 3000, the app is ready.
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.
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=falseThen 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=websocketYou 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
| Criterion | HedgeDoc (self-hosted) | Notion | Confluence (Cloud) |
|---|---|---|---|
| Price | Free (VPS cost) | Free up to 10 members, ~$10/member/mo | ~$5.75/user/mo (min 10) |
| Format | Native Markdown | Proprietary blocks | Rich editor (WYSIWYG) |
| Native diagrams | Mermaid, PlantUML, Vega-lite | Limited (via integrations) | Via macros |
| Code blocks | 200+ languages with highlighting | Yes (limited) | Yes (via plugin) |
| Data hosting | Your VPS | Notion servers (US) | Atlassian servers |
| Markdown export | Yes (native) | Partial | Not native |
| Presentation mode | Yes (built-in) | No | Via plugin |
| Real-time collaboration | Yes (WebSocket) | Yes | Yes |
| LaTeX formulas | Yes (MathJax) | Yes | Via 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 appDatabase 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'.