What you are setting up
HedgeDoc is a markdown editor that several people can type in at once, with everyone's changes appearing live for the others. A note can also be presented as a slide deck. The project grew out of code from the HackMD team and now has its own organisation; its stable series is 1.x, and the newest release when this was written is 1.12.0, from August 2026.
On your server it is two pieces: the app, a Node.js program shipped as a container on quay.io, and a database for notes, accounts and history. The official guide pairs it with Postgres. SQLite is possible, but the docs strongly recommend a proper database for anything beyond a one-person instance. Images people paste into notes are saved to a folder on disk unless you point them at another store, such as S3, MinIO or Azure.
How big a server does this need?
The project publishes no hardware minimum, so nobody can honestly give you a core count. What grows over time is disk: every screenshot dropped into a note stays there. So pick for storage first.
Our dedicated servers include small x86 cartridge machines meant for one job each, which is exactly what this is. Pass over the ARM cartridge, X-Gene: its only systems are Ubuntu 16.04 and 18.04. The range also includes one-processor servers, where most sites, apps and databases start. Either way nobody else is on the hardware, you log in as root, and nothing meters your traffic. A ready server is handed over within 30 minutes of the payment being approved.
Rack servers, blades, and two cartridge models (m510, m710x) give you a remote screen and power button that keep working when SSH stops answering. The remaining cartridges only offer power control, so double-check firewall changes on those before you press Enter.
Before you begin
- A hostname such as
notes.example.comwhose DNS record points at your new machine. - Ubuntu 24.04 LTS, plus Docker Engine (version 28 at least) and its Compose plugin, both installed from Docker's repository. Since 28, a port published on 127.0.0.1 can only be reached from the server itself (moby#45610).
- nginx from Ubuntu's packages, and a certificate for your hostname from Let's Encrypt, for example through certbot.
- A root shell on the machine.
Up and running in one sitting
Paste this as root, with your hostname in place of the example. It turns on the firewall, writes two private settings files and the compose file, and starts both containers:
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw allow 80,443/tcp
ufw --force enable
install -d -m 700 /srv/hedgedoc && cd /srv/hedgedoc
umask 077
DBPASS=$(openssl rand -hex 24)
cat > postgres.env <<EOF
POSTGRES_USER='hedgedoc'
POSTGRES_PASSWORD='$DBPASS'
POSTGRES_DB='hedgedoc'
EOF
cat > hedgedoc.env <<EOF
NODE_ENV='production'
CMD_DB_URL='postgres://hedgedoc:$DBPASS@database:5432/hedgedoc'
CMD_DOMAIN='notes.example.com'
CMD_PROTOCOL_USESSL='true'
CMD_URL_ADDPORT='false'
CMD_SESSION_SECRET='$(openssl rand -hex 32)'
CMD_ALLOW_ANONYMOUS='false'
CMD_ALLOW_EMAIL_REGISTER='false'
CMD_DEFAULT_PERMISSION='limited'
CMD_ENABLE_STATS_API='false'
CMD_IMAGE_UPLOAD_TYPE='filesystem'
EOF
unset DBPASS
chmod 600 postgres.env hedgedoc.env
cat > compose.yaml <<'EOF'
services:
database:
image: postgres:18.6-alpine
env_file: postgres.env
restart: always
volumes:
- database:/var/lib/postgresql
app:
image: quay.io/hedgedoc/hedgedoc:1.12.0
env_file: hedgedoc.env
restart: always
depends_on:
- database
ports:
- "127.0.0.1:3000:3000"
volumes:
- uploads:/hedgedoc/public/uploads
volumes:
database:
uploads:
EOF
docker compose up -d
- The firewall goes first. IPv6 is switched on in ufw before any rule is added, because ufw only makes IPv6 copies of rules created while that setting is on. nginx runs directly on the machine, outside Docker, so it needs 80 and 443 opened for both kinds of address.
- The passwords never touch your screen. They are generated inside the shell, so they do not show up in your history or the process list, and both settings files end up readable by root alone. Single quotes around each value stop anything in it being reinterpreted.
- HedgeDoc listens on 127.0.0.1 only. The official example maps port 3000 with no address, and Docker then listens on every address the server has, IPv4 and IPv6. A port published that way goes around ufw for IPv4 traffic, and for IPv6 too when Docker's IPv6 networking is enabled. Kept on loopback, the only way in is through nginx.
- Postgres 18 moved its data. The official example mounts
/var/lib/postgresql/data, but the Postgres 18 image keeps its files in/var/lib/postgresql/18/docker. With a volume on the old path it will not create the database at all: it stops with an error about an unused mount. Mounting the parent folder avoids that.
Keeping strangers out
Out of the box, HedgeDoc lets anyone who finds it create notes as a guest and register an account. The settings file above closes both doors before the site is reachable:
CMD_ALLOW_ANONYMOUSdefaults to true. At false, visitors who are not signed in can no longer create notes.CMD_ALLOW_ANONYMOUS_EDITSalready defaults to false. Turning it on would let guests edit notes whose owners allow it, so leave it alone unless you want that.CMD_ALLOW_EMAIL_REGISTERdefaults to true. At false, the sign-up form goes away while email sign-in keeps working.CMD_DEFAULT_PERMISSIONdecides who sees a new note. Its default, editable, lets anyone who is not signed in read it; limited, set here, keeps new notes to your team, and each owner can still open a note up later.CMD_SESSION_SECRETsigns the login cookie. Leave it out and HedgeDoc invents a new one at every restart, signing everyone out each time.- The three link settings (
CMD_DOMAIN,CMD_PROTOCOL_USESSL,CMD_URL_ADDPORT) make HedgeDoc write links with your hostname, https, and no port number, because visitors come in on 443. CMD_ENABLE_STATS_APIat false hides the/statusand/metricspages, which the production guide suggests keeping off the open internet.
With guests off and CMD_ENABLE_UPLOADS left unset, only signed-in people can upload images. Since nobody can sign up, you add each person yourself: inside /srv/hedgedoc, docker compose exec app node bin/manage_users --add colleague@example.com asks for their password. Skip the --pass option, which would leave the password in your shell history. The same tool takes --reset for a forgotten password and --del to remove someone.
Built-in accounts lack two-factor login and self-service resets, the project notes; its docs explain logging in through LDAP, SAML or OAuth2 as well.
Making live editing work through HTTPS
Live editing runs over a websocket at /socket.io/, so nginx has to pass the connection upgrade along, and it must tell HedgeDoc whether the visitor used https. This site file follows the official reverse-proxy guide, with one line added that the guide's sample leaves out: proxy_http_version 1.1; for the socket.io path. nginx talks HTTP/1.0 to the app unless told otherwise, and the websocket upgrade only works over 1.1.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name notes.example.com;
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/notes.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/notes.example.com/privkey.pem;
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;
}
location /socket.io/ {
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Watch for a slash at the end of a proxy_pass address: it stops the websocket, so the editor keeps loading forever. Behind Cloudflare, Rocket Loader must be off. Check the file with nginx -t, reload nginx, then open your hostname and sign in. HTTP/2 is optional: nginx 1.25.1 and up takes an http2 on; line, older versions add http2 to each listen line.
Copies of your notes, and updating safely
The HedgeDoc docs say a backup should hold three things: the database, the uploads and your configuration. Run this as root. It makes a folder only root can open, then writes the nightly schedule to /etc/cron.d/hedgedoc-backup:
install -d -m 700 /var/backups/hedgedoc
cat > /etc/cron.d/hedgedoc-backup <<'EOF'
10 2 * * * root umask 077; cd /srv/hedgedoc && docker compose exec -T database pg_dump -U hedgedoc hedgedoc > /var/backups/hedgedoc/notes-$(date +\%F).sql
15 2 * * * root umask 077; cd /srv/hedgedoc && docker cp "$(docker compose ps -q app)":/hedgedoc/public/uploads - > /var/backups/hedgedoc/images-$(date +\%F).tar
20 2 * * * root umask 077; tar -C /srv/hedgedoc -cf /var/backups/hedgedoc/settings-$(date +\%F).tar compose.yaml postgres.env hedgedoc.env
40 2 * * * root find /var/backups/hedgedoc -type f -mtime +14 -delete
EOF
chmod 644 /etc/cron.d/hedgedoc-backup
That keeps two weeks of copies, all of them still sitting beside the originals. You manage the server; we keep the hardware and the network running. So copy the folder somewhere else every day, and treat the settings archive as a secret, since it holds your passwords.
To bring a copy back on a fresh machine, run the four firewall lines from the setup block, put the backup files back in /var/backups/hedgedoc, and work as root with DAY set to the date you want (for example DAY=2026-10-22):
- Check both files are there and not empty:
test -s /var/backups/hedgedoc/notes-$DAY.sql && test -s /var/backups/hedgedoc/images-$DAY.tar && echo ok. Go on only if it prints ok. - Unpack your settings so the passwords match the dump:
install -d -m 700 /srv/hedgedoc && tar -C /srv/hedgedoc -xf /var/backups/hedgedoc/settings-$DAY.tar && cd /srv/hedgedoc. - Start the database by itself with
docker compose up -d database, wait untildocker compose logs databasesays it is ready to accept connections, then load the notes into the new, empty volume:docker compose exec -T database psql -U hedgedoc hedgedoc < /var/backups/hedgedoc/notes-$DAY.sql. - Create the app container without starting it,
docker compose create app, and copy the images in:docker cp - "$(docker compose ps -a -q app)":/hedgedoc/public < /var/backups/hedgedoc/images-$DAY.tar. - Start everything with
docker compose up -d. WithCMD_IMAGE_UPLOAD_TYPEset to filesystem, the image hands the uploads folder back to its own user as it starts.
Updating is a change of one tag. Read the release notes first, as the docs ask every time; HedgeDoc uses semantic versioning, which means a minor or patch release is not meant to break anything you would notice. Run the dump by hand, change the version in compose.yaml, pull the new image and start it again with docker compose up -d. Keep Postgres on major version 18 until you plan a dump-and-restore move, because a new major version cannot read the old data folder directly.
Is it the right fit, and is the licence a problem?
HedgeDoc is released under the AGPL-3.0, which covers the code but not the logo. Here is how we read it in general terms; it is not legal advice. Using it as it ships, for your own team, asks nothing of you. Handing copies to others, whether you changed them or not, means giving them the licence and the source too. And if you modify it and let people use your changed version over the network, they must be able to get your source; the settings reference wants CMD_SOURCE_URL to point at wherever it lives. For anything unusual, check with a lawyer.
If what you need is a structured handbook that people browse for years, a wiki like BookStack or Outline suits it better, and for private notes synced to phone and laptop apps, look at Joplin Server.
Questions people ask
- Could I skip Postgres and use SQLite?
- You could, and for a single person the docs say it is probably fine. For a team they strongly recommend a real database, since it holds up better than one sqlite.db file. On a whole server of your own, the extra Postgres container costs you very little.
- Why does everybody get logged out when the container restarts?
- Because no session secret was set, so HedgeDoc made up a random one at start-up and the old login cookies no longer match. Put a fixed value in
CMD_SESSION_SECRET, as the settings file above does, and logins survive restarts and updates. - Which of your servers should I pick for a small team?
- A note editor with its database is one modest job, so the small x86 cartridges or a single-processor server on the dedicated server page fit. Choose more disk if your team pastes many images, and a model with a remote console if you want a safety net for firewall changes.