Mirrored from
INSTALL.mdat commitf31ec6a. Edit the file in the repository: this page is regenerated from it, and edits made here are overwritten.
Installing
Deploying Docmost Freenterprise with Docker. CI publishes the image, so the server needs neither the source nor a toolchain — two files and a directory.
Building the image yourself instead? See Building from source at the
bottom. Working on the code? See SETUP-DEV.md.
The layout
.
|-- compose.yml <- yours, from compose.example.yml
|-- .env <- yours, secrets live here
`-- data/ <- attachments, owned by uid 1000
That is all of it. No clone, no docmost/ directory, no patches to apply — the
published image already has them baked in.
What you need first
- Docker with the Compose plugin
- Access to
git.derg.cz's package registry, if the package is private
Postgres, Redis and the two sidecars all come from compose.
Quick start
Edit the first line, then paste the whole block. It fetches compose.yml
and .env, generates the secrets, and creates the storage directory with the
right owner. sudo at the end may prompt.
APP_URL="https://docs.example.com" # <-- change this
BASE=https://git.derg.cz/ulysia/docmost-freenterprise/raw/branch/main
curl -fsSL "$BASE/compose.example.yml" -o compose.yml
curl -fsSL "$BASE/.env.example" -o .env
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|" .env
sed -i "s|^APP_SECRET=.*|APP_SECRET=$(openssl rand -hex 32)|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" .env
mkdir -p data && sudo chown -R 1000:1000 data
Then, separately:
docker compose up -d
Open APP_URL once it settles and create the first account, which becomes the
workspace owner.
If the package or repository is private, both the
curls and the image pull need credentials — log in with a token that has package read scope, and add-H "Authorization: token <tok>"to thecurlcalls:docker login git.derg.cz -u <you>
sed -ihere is GNU sed. On macOS usesed -i ''.
Nothing else is required. The rest of this document explains what those commands did, and what else you can set.
1. compose.yml and .env
Both are templates in the bundle, fetched above. compose.yml is ready as-is;
.env ships with placeholders, which the sed lines replace.
The image is git.derg.cz/ulysia/docmost-freenterprise:main, rebuilt on every
push to the bundle's main branch. main moves. Pin a :<short-sha> tag
when you want a deployment to stay put — every build publishes one, listed under
the repository's Packages tab. Each image also carries a label recording which
upstream Docmost commit it was built against, since the interesting version is
not in either repository alone:
docker inspect --format '{{index .Config.Labels "cz.derg.freenterprise.upstream-commit"}}' \
git.derg.cz/ulysia/docmost-freenterprise:main
2. Environment
Three values are required; everything else in .env is commented out with its
default shown.
| Variable | |
|---|---|
APP_URL |
Public URL, e.g. https://docs.example.com. Builds links in email and shares, so it must be what users actually type. |
APP_SECRET |
32+ characters. Changing it invalidates every session. |
POSTGRES_PASSWORD |
Set once. compose.yml builds DATABASE_URL from it and hands the same value to the db container, so there is nothing to keep in sync. |
To regenerate a secret later:
sed -i "s|^APP_SECRET=.*|APP_SECRET=$(openssl rand -hex 32)|" .env
docker compose up -d
Changing POSTGRES_PASSWORD after first start does not change the password
Postgres already stored — the db container only reads it when it initialises an
empty data directory. Change it inside Postgres, or start over.
The rest of .env is optional and grouped: Enterprise (SSO_ACCOUNT_MERGE,
SSO_MERGE_REQUIRE_VERIFIED_EMAIL, SSO_OIDC_GROUP_SYNC) and Core (mail,
storage driver, upload limit, telemetry). Uncomment what you need.
Service URLs — DATABASE_URL, REDIS_URL, GOTENBERG_URL, TIKA_URL,
PDF_RENDER_URL — are deliberately not in .env. They are determined by
the service names in compose.yml and are set there, so renaming a service
cannot leave a stale URL behind.
3. The storage directory
mkdir -p data && sudo chown -R 1000:1000 data
Before the first start, not after. The container runs as node, uid 1000,
and Docker creates a missing bind-mount source as root — leaving a directory
the app cannot write to. Every upload then fails with Error uploading file to drive and a log line carrying nothing else, because core logs the real error in
a way that discards it. DEPLOYMENT.md has the full diagnosis under
Attachment storage.
uid 1000 numerically: the host has no node user, and only numbers cross the
container boundary.
compose.ymluses a bind mount so attachments sit somewhere you can see and back up. A named volume avoids this step entirely — Docker seeds it from the image, where the directory is already owned correctly — and the comment incompose.ymlshows the swap. Don't switch an existing install over; the files do not move themselves.
4. Start
docker compose up -d
docker compose logs -f docmost
Migrations run at startup, including the two the bundle adds.
Checking it worked
| Check | Where | Wrong looks like |
|---|---|---|
| EE loaded at all | Settings → Licence | Should read Freenterprise and list every feature |
| Audit logging | SELECT count(*) FROM audit; after logging in |
0 means the no-op service won |
| Uploads | Paste an image into a page | See Attachment storage in DEPLOYMENT.md |
| PDF export | Export any page | A blank PDF means PDF_RENDER_URL is unreachable |
| API docs | /openapi |
Swagger UI for every route the instance serves |
VERIFICATION.md records what has and has not been tested; ISSUES.md records
bugs found in live testing.
Updating
docker compose pull
docker compose up -d
:main is a moving tag, so pull is what advances it. On a pinned
:<short-sha> tag, edit compose.yml to the new tag first.
Compare your compose.yml and .env against the templates after an update —
new options show up there first.
Building from source
Only needed if you are changing the code, or deliberately want to build the image rather than pull it. This is the long way round: it needs both repositories, generates a wrapper Dockerfile, and applies the patch stack at build time.
git clone https://github.com/docmost/docmost.git
cd docmost
# The pinned upstream commit — NOT the v0.95.0 tag, which is 13 commits behind
# and predates the Node 26 / pnpm 11 upgrade. Also in upstream.pin, which is
# what CI reads. DEPLOYMENT.md explains the pin.
git checkout 7bef7b1a
git clone ssh://git@git.derg.cz/ulysia/docmost-freenterprise.git apps/server/src/ee
# Upstream still references its own private `ee` submodule and does not ignore
# ours. Both need fixing:
git rm --cached apps/server/src/ee 2>/dev/null || true
rm -f .gitmodules
printf '\n# EE bundle is its own repository\napps/server/src/ee/\n' >> .gitignore
git status --short # must NOT list apps/server/src/ee
chmod +x apps/server/src/ee/scripts/*.sh
apps/server/src/ee/scripts/gen-docker-wrapper.sh
cd ..
cp docmost/apps/server/src/ee/compose.dev.yml compose.yml
cp docmost/apps/server/src/ee/.env.example .env
# then fill in .env and create ./data exactly as above
docker compose up -d --build
On a Windows host use the PowerShell twin of the generator — no chmod needed,
and it handles the CRLF traps that otherwise break a Dockerfile generated on
Windows:
pwsh apps/server/src/ee/scripts/gen-docker-wrapper.ps1
Both read the same template and produce byte-identical output.
compose.dev.yml differs from compose.example.yml in exactly one service:
docmost builds from ./docmost with the generated Dockerfile instead of
pulling an image. Everything else is the same, ./data bind mount included.
Re-run the generator after editing the Dockerfile or moving to a new upstream
commit. If a patch no longer applies the build stops and names it;
patches/README.md explains how to regenerate one. The submodule and gitignore
edits live outside git by design, so changing commits drops them — reapply those
too.