Skip to Content

Production Guide: Deploy HedgeDoc with Docker Compose + Caddy + PostgreSQL on Ubuntu

A practical production deployment for collaborative Markdown notes with HTTPS, backups, secret handling, and operational checks.

Collaborative notes become critical infrastructure faster than most teams expect. A runbook starts as a scratchpad, then incident response notes, onboarding documents, architecture decisions, and customer handoff material begin living in the same place. HedgeDoc is a strong fit for this workflow because it gives engineers a fast Markdown editor, real-time collaboration, revision history, and a simple web interface without forcing a heavy knowledge-base rollout.

This guide shows how to deploy HedgeDoc on Ubuntu with Docker Compose, Caddy, and PostgreSQL. The goal is not only to get a login screen working; it is to build a small production service that is easy to patch, back up, observe, and recover. We will use Caddy for automatic HTTPS, PostgreSQL for durable storage, a locked-down environment file for secrets, and operational checks that prove the stack is healthy before people depend on it.

The pattern mirrors the house style used across our Guides: a clear architecture overview, copy-friendly command blocks, practical security defaults, explicit verification, common failure modes, a FAQ, and a short operations-focused call to action.

Architecture and flow overview

The deployment has three moving parts. Caddy listens on ports 80 and 443, obtains and renews TLS certificates, and proxies requests to HedgeDoc on the private Docker network. HedgeDoc serves the web application, collaborative editing sessions, and API endpoints. PostgreSQL stores users, notes, permissions, and application metadata on a persistent host-mounted volume.

Traffic flow is intentionally boring: browser to Caddy over HTTPS, Caddy to HedgeDoc over the internal container network, and HedgeDoc to PostgreSQL using credentials from an environment file. Operational flow is equally important. Configuration lives under /opt/hedgedoc, secrets are readable only by the deployment user, backups are created with pg_dump, and health checks are run after every change.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with 2 vCPU, 2-4 GB RAM, and 20+ GB free disk.
  • A DNS A or AAAA record pointing docs.example.com to the server.
  • Ports 80 and 443 open from the internet, plus SSH restricted to trusted admins.
  • A sudo-capable Linux user and a plan for off-host backups.

Step-by-step deployment

1) Install Docker, Compose, and baseline tools

sudo apt update
sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg lsb-release ufw jq openssl

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"

If the copy button does not work in your browser/editor, manually select the code block and copy.

Log out and back in after adding your user to the Docker group. Avoid running the whole stack as root during normal operations; reserve sudo for package and firewall changes.

2) Create the application directory

sudo mkdir -p /opt/hedgedoc/{postgres,uploads,backups}
sudo chown -R "$USER:$USER" /opt/hedgedoc
chmod 750 /opt/hedgedoc
cd /opt/hedgedoc

If the copy button does not work in your browser/editor, manually select the code block and copy.

Keeping application data under one directory makes backups, audits, and migrations much easier. The uploads directory stores user-uploaded assets; the postgres directory stores the database volume.

3) Write secrets and application settings

cd /opt/hedgedoc
SESSION_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -base64 36 | tr -d '\n')

cat > .env <<EOF
POSTGRES_DB=hedgedoc
POSTGRES_USER=hedgedoc
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
CMD_DOMAIN=docs.example.com
CMD_PROTOCOL_USESSL=true
CMD_URL_ADDPORT=false
CMD_ALLOW_ANONYMOUS=false
CMD_ALLOW_ANONYMOUS_EDITS=false
CMD_ALLOW_EMAIL_REGISTER=true
CMD_SESSION_SECRET=${SESSION_SECRET}
EOF

chmod 600 .env

If the copy button does not work in your browser/editor, manually select the code block and copy.

For a private company instance, disable anonymous access unless you intentionally want public note creation. If you use SSO later, keep email registration closed and map identity through your provider.

4) Create Docker Compose services

cat > /opt/hedgedoc/docker-compose.yml <<'YAML'
services:
  postgres:
    image: postgres:16-alpine
    container_name: hedgedoc-postgres
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - hedgedoc_net

  hedgedoc:
    image: quay.io/hedgedoc/hedgedoc:1.10.2
    container_name: hedgedoc-app
    restart: unless-stopped
    env_file: .env
    environment:
      CMD_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      CMD_IMAGE_UPLOAD_TYPE: filesystem
      CMD_CSP_ENABLE: true
    volumes:
      - ./uploads:/hedgedoc/public/uploads
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - hedgedoc_net

  caddy:
    image: caddy:2-alpine
    container_name: hedgedoc-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - hedgedoc
    networks:
      - hedgedoc_net

networks:
  hedgedoc_net:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:
YAML

If the copy button does not work in your browser/editor, manually select the code block and copy.

Pin the HedgeDoc image for predictable upgrades. Before moving to a new version, read release notes and test a database restore in staging or on a disposable clone.

5) Configure Caddy for HTTPS

cat > /opt/hedgedoc/Caddyfile <<'CADDY'
docs.example.com {
  encode gzip zstd

  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "SAMEORIGIN"
    Referrer-Policy "strict-origin-when-cross-origin"
  }

  reverse_proxy hedgedoc:3000
}
CADDY

If the copy button does not work in your browser/editor, manually select the code block and copy.

Caddy will request and renew certificates automatically when DNS is correct and ports 80/443 are reachable. Keep the domain in .env aligned with the Caddyfile so generated links are correct.

6) Start the stack and verify first boot

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

docker compose ps
docker logs --tail=80 hedgedoc-app

If the copy button does not work in your browser/editor, manually select the code block and copy.

The first start may take a moment while PostgreSQL initializes and HedgeDoc creates tables. Repeated restarts or database connection errors should be treated as deployment blockers, not harmless noise.

7) Add a database backup job

cat > /opt/hedgedoc/backup.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/hedgedoc
source .env
stamp=$(date +%F-%H%M%S)
out="/opt/hedgedoc/backups/hedgedoc-${stamp}.sql.gz"
docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" hedgedoc-postgres \
  pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" | gzip > "$out"
find /opt/hedgedoc/backups -type f -name 'hedgedoc-*.sql.gz' -mtime +14 -delete
BASH
chmod +x /opt/hedgedoc/backup.sh

( crontab -l 2>/dev/null; echo "23 2 * * * /opt/hedgedoc/backup.sh" ) | crontab -

If the copy button does not work in your browser/editor, manually select the code block and copy.

Local backups are only the first layer. Sync backup files to object storage or another server, then run a restore drill at least quarterly.

Configuration and secrets handling best practices

Keep .env outside Git and restrict it to the deployment user. If multiple administrators need access, store generated secrets in a password manager or secret manager and document who can rotate them. Rotate CMD_SESSION_SECRET only during a planned session invalidation window, because active users may be logged out.

Prefer least privilege around SSH and Docker access. Membership in the Docker group effectively grants root-equivalent host control, so do not add casual users. For more formal environments, place deployment actions behind CI/CD or a restricted automation account and require review for Compose changes.

Verification checklist

Run these checks after first deployment and after every meaningful change:

cd /opt/hedgedoc

docker compose ps
curl -I https://docs.example.com
docker exec hedgedoc-postgres pg_isready -U hedgedoc -d hedgedoc
docker logs --tail=100 hedgedoc-app
docker logs --tail=50 hedgedoc-caddy
/opt/hedgedoc/backup.sh
ls -lh /opt/hedgedoc/backups | tail

If the copy button does not work in your browser/editor, manually select the code block and copy.

A healthy deployment returns an HTTPS response, shows stable containers, accepts PostgreSQL readiness checks, and produces a non-empty compressed backup. Create a test note, upload a small image if uploads are enabled, log out, and confirm authentication rules behave as expected.

Common issues and fixes

Caddy cannot issue certificates

Verify DNS points to this server and that ports 80 and 443 are not blocked by a cloud security group or UFW. Check docker logs hedgedoc-caddy for ACME challenge failures. Do not switch to a self-signed certificate to hide the issue; fix public reachability first.

HedgeDoc generates HTTP links behind HTTPS

Confirm CMD_PROTOCOL_USESSL=true, CMD_URL_ADDPORT=false, and CMD_DOMAIN exactly match the public hostname. Restart HedgeDoc after changing environment values.

Database password changes break startup

Changing POSTGRES_PASSWORD in .env does not automatically change the existing PostgreSQL user password inside the database. Rotate credentials intentionally with SQL, update the environment, and restart services in order.

Uploads disappear after redeploy

Make sure ./uploads:/hedgedoc/public/uploads remains mounted and included in backups. Database backups alone do not preserve uploaded files.

Users report editing conflicts

Check WebSocket proxy behavior, browser extensions, and any upstream load balancer timeouts. Caddy's basic reverse proxy configuration is usually sufficient, but external proxies in front of Caddy can introduce idle timeout problems.

FAQ

Can I use SQLite instead of PostgreSQL?

For production, use PostgreSQL. SQLite can be acceptable for experiments, but collaboration-heavy usage benefits from a real database, predictable backups, and operational visibility.

How do I disable open registration?

Set registration controls in the environment and restart the app. For private teams, combine closed registration with SSO or administrator-created accounts.

Should Caddy run on the host instead of Docker?

Either works. Containerized Caddy keeps the stack portable and is easier to version with Compose. Host Caddy can be better if one proxy serves many unrelated applications.

How often should I back up HedgeDoc?

Daily backups are a practical baseline, but high-change teams may want hourly database dumps plus continuous upload synchronization. Match retention to business impact.

What should I monitor first?

Monitor HTTPS availability, container restarts, disk usage, backup success, certificate expiry, and PostgreSQL readiness. Alerting on these basics catches most operational failures early.

How do I safely upgrade HedgeDoc?

Take a database and uploads backup, pin the current image tag, test the new tag in staging, then upgrade during a maintenance window. Keep rollback commands documented.

Can I put HedgeDoc behind a VPN only?

Yes. If notes contain sensitive operational data, private access through a VPN or zero-trust gateway can be a good default. Still keep TLS enabled and backups tested.

Related guides

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy MinIO with Docker Compose + NGINX + Let's Encrypt + UFW on Ubuntu
S3-compatible object storage with TLS, firewall hardening, and production-ready configuration.