HedgeDoc Collaborative Editor: Host Your Own Real-Time Markdown Pad

Deploy HedgeDoc using Docker Compose. Author, share, and collaborate on markdown documents in real-time on your own VPS.

Directory Structure and Preparation

Before deploying the containers, establish a structured directory layout under /opt/hedgedoc to persist database records and file uploads. Run the following commands to create the directories and set correct ownership:

sudo mkdir -p /opt/hedgedoc/volumes/db_data
sudo mkdir -p /opt/hedgedoc/volumes/uploads
sudo chown -R 10000:10000 /opt/hedgedoc/volumes/uploads

Note: The HedgeDoc container runs as UID/GID 10000 by default. Assigning ownership of the uploads directory to 10000 prevents permission errors when uploading images.


Docker Compose Configuration

Create /opt/hedgedoc/docker-compose.yml with the configuration below. This deployment pairs HedgeDoc with PostgreSQL 15 on an internal bridge network.

version: '3.8'

services:
  db:
    image: postgres:15-alpine
    container_name: hedgedoc_db
    restart: always
    environment:
      POSTGRES_USER: hedgedoc
      POSTGRES_PASSWORD: MustJwDRIBziphPdGoOpWao2GDZL
      POSTGRES_DB: hedgedoc
    volumes:
      - /opt/hedgedoc/volumes/db_data:/var/lib/postgresql/data
    networks:
      - hedgedoc_net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U hedgedoc"]
      interval: 5s
      timeout: 5s
      retries: 5

  hedgedoc:
    image: quay.io/hedgedoc/hedgedoc:1.9.9
    container_name: hedgedoc_app
    restart: always
    depends_on:
      db:
        condition: service_healthy
    environment:
      - NODE_ENV=production
      - CMD_DB_URL=postgres://hedgedoc:MustJwDRIBziphPdGoOpWao2GDZL@db:5432/hedgedoc
      - CMD_DOMAIN=hedgedoc.example.com
      - CMD_PROTOCOL_USESSL=true
      - CMD_URL_ADDPORT=false
      - CMD_SESSION_SECRET=a_very_long_random_string_for_session_encryption_key
      - CMD_IMAGE_UPLOAD_TYPE=filesystem
      - CMD_ALLOW_FREEURL=true
      - CMD_DEFAULT_PERMISSION=editable
    volumes:
      - /opt/hedgedoc/volumes/uploads:/hedgedoc/public/uploads
    ports:
      - "127.0.0.1:3000:3000"
    networks:
      - hedgedoc_net

networks:
  hedgedoc_net:
    driver: bridge

Essential Parameters Explained

  • CMD_DB_URL: Specifies the PostgreSQL connection string. The hostname db matches the container service name inside the shared bridge network.
  • CMD_DOMAIN: Set this to your fully qualified domain name (FQDN). It ensures absolute URLs generated in emails and invites match your public address.
  • CMD_PROTOCOL_USESSL: Instructs HedgeDoc that it is served behind an SSL-terminating reverse proxy. Cookies will be set with the Secure flag.
  • CMD_SESSION_SECRET: A cryptographically secure random string used to sign session cookies.
  • CMD_IMAGE_UPLOAD_TYPE: Configured to filesystem to store images locally. They will persist in the /opt/hedgedoc/volumes/uploads directory.

Nginx Reverse Proxy Setup

HedgeDoc relies on WebSockets for real-time collaborative editing. The reverse proxy must be configured to pass the standard connection upgrade headers, or client connections will degrade to HTTP polling.

Virtual Host Configuration

Place the following server block configuration in your Nginx configuration directory (e.g., /etc/nginx/sites-available/hedgedoc.conf):

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name hedgedoc.example.com;

    # Redirect all HTTP traffic to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name hedgedoc.example.com;

    # SSL Certificates (managed via Certbot / Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/hedgedoc.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/hedgedoc.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Increase client body size to allow image uploads
    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;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_read_timeout 86400;
    }
}

Applying the Nginx Configuration

Validate the syntax of your configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/hedgedoc.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Deployment and Verification

Start the containers in detached mode:

cd /opt/hedgedoc
docker compose up -d

Confirm that the application is running and the database connection is initialized by viewing the container logs:

docker compose logs -f hedgedoc

You should see output similar to:

2026-07-06T21:30:00.000Z - info: DB connection has been established successfully.
2026-07-06T21:30:01.000Z - info: HTTP Server listening at http://0.0.0.0:3000

Backup and Maintenance

Backing up the Database

To perform a hot backup of the PostgreSQL database without interrupting the service, execute:

docker exec -t hedgedoc_db pg_dump -U hedgedoc hedgedoc > /opt/hedgedoc/backups/backup_$(date +%F).sql

Upgrading HedgeDoc

To upgrade the application container when a new version is released:

cd /opt/hedgedoc
docker compose pull hedgedoc
docker compose up -d --remove-orphans

The database migrations will run automatically upon container startup.