Use case · Wiki and documentation

Docmost self hosted on a dedicated server

Self-hosting Docmost means running the three containers that the upstream compose file defines (the Docmost app on port 3000, Postgres 18 and Redis 8), with a reverse proxy added on top for HTTPS that passes WebSockets. This page builds that stack on one dedicated server with the app bound to 127.0.0.1, creates the workspace owner over an SSH tunnel before anything is public, then adds the certificate, mail, file storage, backups and upgrades.

See the machines

What the stack is made of

Docmost's installation guide names Docker as the recommended install and Docker as the only prerequisite. Its compose file defines three services; the docs' Caddy guide adds a fourth for TLS.

ServiceImageWhat it holds
docmostdocmost/docmost:latestThe app on port 3000; uploaded files under /app/data/storage
dbpostgres:18The workspace data, reached through DATABASE_URL; volume mounted at /var/lib/postgresql
redisredis:8Started with append-only persistence and the noeviction policy
caddycaddy:2HTTPS certificates and the proxy to docmost:3000

The app answers health checks at /api/health. The repository's release workflow builds the image for linux/amd64 and linux/arm64 and tags each release as latest, as its full version (for example 0.96.0) and as major.minor (0.96). That detail comes from the workflow file, not from the docs.

Sizing the machine

The Docmost docs publish no CPU, memory or disk figures, so there is no official sizing table to quote. What grows over time is the attachment folder and the database; the containers themselves are fixed. Our own rule, following the dedicated server range: size memory first, then cores, then storage for the files your team will upload.

  • Moonshot cartridge: one service per machine, which is exactly one Docmost stack. Among cartridges a console comes only with the m510 and m710x; the others offer power control alone.
  • Single socket: the range describes it as a web or application server with its database on the same box, which is this workload, with an out-of-band console (iLO, iDRAC or a Supermicro BMC).
  • Dual socket: when the same box also runs other services next to the wiki.
  • ARM: the arm64 image runs on the Ampere Altra line. Skip the X-Gene cartridge: it boots only Ubuntu 16.04 and 18.04.

A ready server is live about 30 minutes once its payment is approved; a build to order takes 4 to 24 hours. The port is unmetered, so a team moving big attachments around costs the same each month. Neighbouring wikis are on Outline and BookStack.

Firewall first, then the compose file

Install Docker Engine with the commands in Docmost's installation guide, and check docker version reports 28 or newer: Engine 28 and later keep 127.0.0.1-bound mappings local to the host (moby#45610). Then set the firewall up before any container starts:

  1. sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw so every rule below gets an IPv6 copy. ufw only writes IPv6 rules for rules added while this is set.
  2. sudo ufw allow OpenSSH (or sudo ufw allow your own SSH port if it listens elsewhere).
  3. sudo ufw --force enable, which skips the "Proceed (y|n)?" question.
  4. sudo ufw allow 80,443/tcp for Caddy and the Let's Encrypt challenge.

Why 80 and 443 still need ufw rules: a port mapping with no address is published on 0.0.0.0 and [::]. Published traffic skips ufw's rules on IPv4, and on IPv6 only when Docker's own IPv6 networking is enabled; otherwise the [::] listener belongs to docker-proxy, an ordinary process that ufw filters, and IPv6 visitors would be dropped. The block below writes the compose file. Our changes to the official one: env_file instead of inline secrets, the 127.0.0.1 bind, a host folder for storage so backups and restores are plain file copies, and a Caddy container taken from the docs' Caddy page.

sudo install -d -m 750 /opt/docmost /opt/docmost/storage
sudo tee /opt/docmost/compose.yaml >/dev/null <<'EOF'
services:
  docmost:
    image: docmost/docmost:latest
    depends_on: [db, redis]
    env_file: .env
    ports:
      - "127.0.0.1:3000:3000"
    restart: unless-stopped
    volumes:
      - /opt/docmost/storage:/app/data/storage
  db:
    image: postgres:18
    environment:
      POSTGRES_DB: docmost
      POSTGRES_USER: docmost
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql
  redis:
    image: redis:8
    command: ["redis-server", "--appendonly", "yes", "--maxmemory-policy", "noeviction"]
    restart: unless-stopped
    volumes:
      - redis_data:/data
  caddy:
    image: caddy:2
    depends_on: [docmost]
    ports:
      - "80:80"
      - "443:443"
    restart: unless-stopped
    volumes:
      - /opt/docmost/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
volumes:
  db_data:
  redis_data:
  caddy_data:
  caddy_config:
EOF

Secrets and the first start

The docs require APP_SECRET to be at least 32 characters, suggest openssl rand -hex 32, and say the app fails to start if the placeholder is left in. DATABASE_URL takes the form postgresql://docmost:PASSWORD@db:5432/docmost, with the same password Postgres is given. A hex password needs no URL escaping; that is our reason for generating it that way. The official compose file keeps these secrets inline; moving them into a .env created at mode 600 before anything is written to it is our choice. The values are generated on the server, so they never appear in a command you type.

DBPW=$(openssl rand -hex 24)
sudo install -m 600 /dev/null /opt/docmost/.env
sudo tee /opt/docmost/.env >/dev/null <<EOF
APP_URL=http://localhost:3000
APP_SECRET=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$DBPW
DATABASE_URL=postgresql://docmost:$DBPW@db:5432/docmost
REDIS_URL=redis://redis:6379
EOF
unset DBPW
sudo tee /opt/docmost/Caddyfile >/dev/null <<'EOF'
docs.example.com {
    reverse_proxy docmost:3000
}
EOF
sudo docker compose -f /opt/docmost/compose.yaml run --rm --no-deps --user root --entrypoint chown docmost node:node /app/data/storage
sudo docker compose -f /opt/docmost/compose.yaml up -d docmost
curl -fsS http://127.0.0.1:3000/api/health

Replace docs.example.com with your name. The run line hands the storage folder to the image's non-root node user (the Dockerfile switches to USER node before the app starts). If the health check fails at first, the app is still starting: run it again. Postgres creates the docmost user from POSTGRES_PASSWORD only on its very first start, which is why the .env must exist before the db container ever runs.

Creating the owner account over a tunnel

Until a workspace exists, anyone who loads the setup screen first can claim the instance and becomes the workspace owner. That comes from the source code (the setup guard refuses only once a workspace exists) and from the docs' note that whoever completes setup becomes owner. So create it privately, before Caddy runs.

  1. From your workstation, start the tunnel: ssh -N -L 3000:127.0.0.1:3000 youruser@your.server.ip (your own login; Ctrl+C closes the tunnel). On a first connection SSH asks "Are you sure you want to continue connecting (yes/no/[fingerprint])?"; compare the key and answer yes.
  2. Open http://localhost:3000. The form asks for a workspace name, your name, an email address and a password of 8 to 70 characters.
  3. Submit it. From then on the setup endpoint answers that setup is already completed.

DNS, then the certificate, then APP_URL

  1. DNS. Point an A record (and an AAAA record if you added IPv6) at the server, and wait until dig +short docs.example.com returns its address.
  2. Certificate. sudo docker compose -f /opt/docmost/compose.yaml up -d caddy. The docs' Caddy guide says Caddy obtains and renews Let's Encrypt certificates on its own and handles WebSocket connections.
  3. Config. In /opt/docmost/.env set APP_URL=https://docs.example.com, then sudo docker compose -f /opt/docmost/compose.yaml up -d --force-recreate docmost.

With any other proxy, pass the Upgrade and Connection request headers through to port 3000. Without WebSockets the docs say the page editor drops to read-only mode.

Mail for invites, and where files live

Inviting users needs a mail transport. Add these to /opt/docmost/.env with sudo nano, then recreate the docmost container as above:

VariableValue
MAIL_DRIVERsmtp (or postmark with POSTMARK_TOKEN)
SMTP_HOST, SMTP_PORTYour relay, e.g. port 587
SMTP_USERNAME, SMTP_PASSWORDThe relay login
SMTP_SECUREtrue for TLS from the first byte, typically port 465; default false
MAIL_FROM_ADDRESS, MAIL_FROM_NAMESender shown on invites

Files go to local storage by default (STORAGE_DRIVER=local), which is the folder this setup backs up. For S3-compatible storage set STORAGE_DRIVER=s3 and AWS_S3_ACCESS_KEY_ID, AWS_S3_SECRET_ACCESS_KEY, AWS_S3_REGION, AWS_S3_BUCKET, plus AWS_S3_ENDPOINT and AWS_S3_FORCE_PATH_STYLE for non-AWS stores; the bucket then needs its own backup. The docs default the upload limit (FILE_UPLOAD_SIZE_LIMIT) to 50mb. Docmost sends anonymous usage counts (version, users, pages, spaces, workspaces); DISABLE_TELEMETRY=true stops them.

Backups, restore and upgrades

The docs have no backup page, so this recipe is ours. It covers everything the app stores: the database dump, the uploaded files, and the .env, compose file and Caddyfile without which the dump cannot be reopened. Redis is left out by our choice: it carries queues, caches and real-time sync (per the server's dependencies) and, by our reading, not your pages or files. Copy /var/backups/docmost off the machine yourself, for example by pulling it with rsync from another host.

# file: /usr/local/sbin/docmost-backup.sh
set -eu
umask 077
D=/var/backups/docmost
T=$(date +%F)
mkdir -p -m 700 "$D"
docker compose -f /opt/docmost/compose.yaml exec -T db pg_dump -U docmost -Fc docmost > "$D/db-$T.dump"
tar -C /opt/docmost -czf "$D/files-$T.tgz" storage .env compose.yaml Caddyfile
find "$D" -type f -mtime +14 -delete

# file: /etc/cron.d/docmost-backup
30 3 * * * root /bin/bash /usr/local/sbin/docmost-backup.sh >> /var/log/docmost-backup.log 2>&1

Live restore, with DATE as the backup's date and every command from any directory:

  1. Check it exists: sudo test -s /var/backups/docmost/db-DATE.dump && sudo test -s /var/backups/docmost/files-DATE.tgz && echo found. Continue only when the output reads found.
  2. Halt Docmost and Caddy: sudo docker compose -f /opt/docmost/compose.yaml stop docmost caddy.
  3. Move the database aside: sudo docker compose -f /opt/docmost/compose.yaml exec -T db psql -U docmost -d postgres -c 'ALTER DATABASE docmost RENAME TO docmost_old', then sudo docker compose -f /opt/docmost/compose.yaml exec -T db createdb -U docmost docmost.
  4. Load the dump: sudo sh -c 'docker compose -f /opt/docmost/compose.yaml exec -T db pg_restore -U docmost -d docmost < /var/backups/docmost/db-DATE.dump'.
  5. Swap the files: sudo mv /opt/docmost/storage /opt/docmost/storage.old, sudo tar -C /opt/docmost -xzf /var/backups/docmost/files-DATE.tgz storage, sudo chown -R --reference=/opt/docmost/storage.old /opt/docmost/storage.
  6. Start and check: sudo docker compose -f /opt/docmost/compose.yaml up -d docmost caddy, then the health URL, then open a page with an attachment.
  7. Only then delete the old copies: sudo rm -rf /opt/docmost/storage.old and sudo docker compose -f /opt/docmost/compose.yaml exec -T db dropdb -U docmost docmost_old.

On a new server, after the firewall and Docker: sudo install -d -m 750 /opt/docmost, extract the whole files archive into it (this puts .env in place before Postgres ever starts, and the files keep their owners), run sudo docker compose -f /opt/docmost/compose.yaml up -d db redis, wait until sudo docker compose -f /opt/docmost/compose.yaml exec -T db pg_isready -U docmost reports accepting connections, then do steps 3, 4 and 6. The docs' upgrade is docker pull docmost/docmost:latest and a force-recreate of the docmost service. Ours adds steps around it: read the release notes on GitHub, take a fresh backup by running /usr/local/sbin/docmost-backup.sh as root (prefix it with sudo bash), sudo docker compose -f /opt/docmost/compose.yaml pull docmost, sudo docker compose -f /opt/docmost/compose.yaml up -d --force-recreate docmost, check health. To roll back, set the image to the previous version tag and restore the backup taken before the upgrade; we do not assume an older image accepts a database a newer one has used.

Licence: AGPL core, enterprise directories

The repository README says the core is licensed under AGPL 3.0 and that all files in three directories fall under the Docmost Enterprise license:

PathLicence
Everything else in docmost/docmostGNU AGPL-3.0 (root LICENSE file)
apps/server/src/eeDocmost Enterprise license; in the repo it is a git submodule pointing at docmost/ee
apps/client/src/eeDocmost Enterprise license
packages/eeDocmost Enterprise license; the licence text is packages/ee/LICENSE (the README calls it packages/ee/License)

The Enterprise license allows production use only with a valid Docmost subscription for the right number of seats; copying and modifying for development and testing needs none. The docs list three editions: Open Source (AGPL-3.0, free), Business and Enterprise, the last two unlocked by a licence key under Settings, License & Edition. When a key expires, the docs say the workspace reverts to the open source edition with accounts and content intact. Our general reading of AGPL-3.0, not legal advice: you may run and change the core; anyone who passes copies on owes the recipients the corresponding source, and a modified version offered to users over a network must offer them its source too.

Questions

Is self-hosted Docmost free?

The Open Source edition is free and licensed under AGPL-3.0. Business and Enterprise are commercial editions that need a licence key, which you add under Settings, License & Edition. The docs say an expired key returns the workspace to the open source feature set without touching accounts or content.

Does single sign-on need a licence?

The installation guide lists SSO with Bases, AI, audit logs and SCIM provisioning as enterprise features unlocked by a trial or licence key. The authentication page marks LDAP, SAML 2.0 and OpenID Connect as enterprise and lists email and password sign-in without that mark. Docmost's pricing page has the full comparison by edition.

Why does the editor open read-only behind my proxy?

The real-time editor needs WebSockets, and the docs say the page editor is read-only without them. Configure the proxy to pass the Upgrade and Connection request headers on. Caddy does this by default, according to the docs' Caddy guide.

Can it run on an ARM server?

Yes on arm64: the release workflow publishes linux/arm64 images next to linux/amd64, so the Ampere Altra line works. The X-Gene cartridge does not suit it, because it boots only Ubuntu 16.04 and 18.04.

How large can uploads be?

FILE_UPLOAD_SIZE_LIMIT sets the largest file per upload; the environment variable reference gives 50mb as the default. Imports have their own limit, FILE_IMPORT_SIZE_LIMIT, which defaults to 200mb. Change either in .env and recreate the docmost container.

Pick the machine for your wiki

Every dedicated line with its processor, memory, disks, port and cities, priced live from the catalogue.

See the machines

Related on Server Room

Other use cases