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.
| Service | Image | What it holds |
|---|---|---|
| docmost | docmost/docmost:latest | The app on port 3000; uploaded files under /app/data/storage |
| db | postgres:18 | The workspace data, reached through DATABASE_URL; volume mounted at /var/lib/postgresql |
| redis | redis:8 | Started with append-only persistence and the noeviction policy |
| caddy | caddy:2 | HTTPS 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:
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufwso every rule below gets an IPv6 copy. ufw only writes IPv6 rules for rules added while this is set.sudo ufw allow OpenSSH(orsudo ufw allowyour own SSH port if it listens elsewhere).sudo ufw --force enable, which skips the "Proceed (y|n)?" question.sudo ufw allow 80,443/tcpfor 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.
- 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. - Open
http://localhost:3000. The form asks for a workspace name, your name, an email address and a password of 8 to 70 characters. - Submit it. From then on the setup endpoint answers that setup is already completed.
DNS, then the certificate, then APP_URL
- DNS. Point an A record (and an AAAA record if you added IPv6) at the server, and wait until
dig +short docs.example.comreturns its address. - 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. - Config. In
/opt/docmost/.envsetAPP_URL=https://docs.example.com, thensudo 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:
| Variable | Value |
|---|---|
| MAIL_DRIVER | smtp (or postmark with POSTMARK_TOKEN) |
| SMTP_HOST, SMTP_PORT | Your relay, e.g. port 587 |
| SMTP_USERNAME, SMTP_PASSWORD | The relay login |
| SMTP_SECURE | true for TLS from the first byte, typically port 465; default false |
| MAIL_FROM_ADDRESS, MAIL_FROM_NAME | Sender 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:
- 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. - Halt Docmost and Caddy:
sudo docker compose -f /opt/docmost/compose.yaml stop docmost caddy. - 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', thensudo docker compose -f /opt/docmost/compose.yaml exec -T db createdb -U docmost docmost. - 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'. - 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. - 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. - Only then delete the old copies:
sudo rm -rf /opt/docmost/storage.oldandsudo 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:
| Path | Licence |
|---|---|
| Everything else in docmost/docmost | GNU AGPL-3.0 (root LICENSE file) |
| apps/server/src/ee | Docmost Enterprise license; in the repo it is a git submodule pointing at docmost/ee |
| apps/client/src/ee | Docmost Enterprise license |
| packages/ee | Docmost 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.