[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host Docmost on your own VPS without Docker Run Docmost v0.96.0 on your own Ubuntu server with ox: Postgres, Redis and a pnpm build, with the first admin made at deploy. The config that ran. Saurav Sharma Oct 17, 2026 11 min read On this page - [What you need](https://deploywithox.com/blog/self-host-docmost#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-docmost#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-docmost#config) - [Postgres and Redis](https://deploywithox.com/blog/self-host-docmost#services) - [Updates and backups](https://deploywithox.com/blog/self-host-docmost#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-docmost#faq) To self-host Docmost you need an Ubuntu server, a fork of the Docmost repo with an `ox.toml` and one script added, eleven variables, and an `unset` line for each optional key the review lists. In our run the first deploy took about 3 minutes, and ox set up Postgres and Redis for it. **How we tested** On Oct 17, 2026, ox deployed Docmost v0.96.0 from commit [`9b3e5dd3`](https://github.com/docmost/docmost/commit/9b3e5dd3dc21588fa20c02c5b8646d2f46ee73af) on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, created a page with a title and some text, and saw both after a reload, and checked the app's services and its backup. [Docmost](https://github.com/docmost/docmost) is an open-source wiki for teams. Pages live in spaces, several people can edit one page at once, and it has comments, page history, search, file attachments and diagrams. It is a TypeScript app in a pnpm and nx workspace: a NestJS server answers the API, serves the React client and takes the live editor's websocket at `/collab`. Postgres holds the data, and Redis holds the job queues, the cache and the rate-limit counters. The core is under the AGPL 3.0 license, and the files in its `ee` folders, such as `apps/server/src/ee`, are under a separate enterprise license. ox deployed Docmost v0.96.0 (commit 9b3e5dd3) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 3 minutes and a redeploy took 2 minutes. - **First deploy:** 3 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 2 minutes, until the app passed the checks again. - **Server:** DigitalOcean, 8 vCPUs and 16 GB of memory (`s-8vcpu-16gb`), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app. - **Memory in use after the reboot:** 1117 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less. - **Services ox runs for it:** `postgres`, `redis`. - **Source:** [github.com/docmost/docmost](https://github.com/docmost/docmost) at v0.96.0, commit [`9b3e5dd3`](https://github.com/docmost/docmost/commit/9b3e5dd3dc21588fa20c02c5b8646d2f46ee73af). The Docmost login page The Docmost home page with the General space after signing in The new Docmost page Verify team handbook with its text, listed in the space sidebar ## What you need - An Ubuntu LTS server. The run used the size in the facts above, with Docmost alone on it. - A GitHub account to fork `docmost/docmost`, and an ox account, which you open by signing in with GitHub. - An email address and a password for the first admin. Docmost sends mail through SMTP or Postmark. The run pointed SMTP at a test mail catcher, so real delivery was not tested, and neither were S3 or Azure file storage. ## The steps Press Add a server in the ox dashboard and run the command it shows, as root, on your VPS. That joins the server to your account. Fork `docmost/docmost` on GitHub and check out the tag `v0.96.0`, the release the run deployed. Add the `ox.toml` and `ox-first-admin.mjs` shown below to the root of the fork, then commit and push. Write a `docmost.env` file with one `KEY=value` line for each of the eleven keys in the variables list. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /docmost ox review docmost --from-file docmost.env --wait ``` The project is named after the repo, so the commands call it `docmost`. The review saves your values and starts the first deploy, and `--wait` prints the run until it ends. The [quickstart](https://deploywithox.com/docs/quickstart) shows the console's version. The review also lists keys for features the run did not use: S3 and Azure file storage, Postmark, a Draw.io server, Gotenberg, an SMTP login, an upload size limit and allowed iframe origins. The run answered each one with an `unset KEY` line in `docmost.env`, which marks a key not needed, and `ox review docmost` prints the full list. When the deploy is green, open the address ox shows for the project and sign in at `/login` with `ADMIN_EMAIL` and `ADMIN_PASSWORD`. The test agent first tried a wrong password and Docmost refused it. With the right one it reached the home page, opened the General space, clicked New page, typed a title and a line of text, and reloaded. The title, the text and the sidebar entry were all still there. The run left `ADMIN_NAME` and `WORKSPACE_NAME` unset, so the workspace is called Docmost and the admin Admin. It used ox's preview address, so a domain of your own was not tested. The [domains page](https://deploywithox.com/docs/operate/domains) says how ox adds one. ## The ox.toml, line by line TOML ```toml # Docmost as its Dockerfile runs it: pnpm install, `pnpm build` (nx), then # `pnpm start` (apps/server start:prod), which serves the API, the built # client and the /collab websocket on $PORT. # # The first workspace and its admin come from ADMIN_EMAIL and ADMIN_PASSWORD: # Docmost makes them only through its running server (the setup page posts # to /api/auth/setup, open while there is no workspace), so migrate does # that once (ox-first-admin.mjs) and the setup page is closed before the # deploy goes live. required_variables = ["ADMIN_EMAIL", "ADMIN_PASSWORD"] [app] health = "/api/health" [build] # apps/server/src/ee is docmost's private enterprise module; the open-source # build loads it only when present (app.module.ts), and the Dockerfile ships none. skip_submodules = ["apps/server/src/ee"] # The server also migrates at boot in production; running it here first # keeps a first deploy's 52 migrations out of the start's health wait. migrate = "pnpm --filter ./apps/server run migration:latest && node ox-first-admin.mjs" [services] postgres = {} redis = {} [storage] # STORAGE_DRIVER=local writes uploads to /data/storage (the image's VOLUME). keep = ["data/storage"] ``` The config has no install, build or start line, the three steps of Docmost's Dockerfile. ox reads `pnpm-lock.yaml` and the `packageManager` pin (pnpm 11.25.0), installs with the frozen lockfile, runs the `build` script, and starts the `start` script on the `PORT` ox sets. - `required_variables` names the admin's email and password, which ox asks for before the first deploy. Docmost has no command that makes a user. Its Create workspace page posts to `/api/auth/setup`, which answers whoever calls first until a workspace exists. So the first admin has to come from the deploy. - `health` is `/api/health`, Docmost's own route. It pings Postgres and Redis, and ox switches to a new release only once it answers. - `skip_submodules` leaves out `apps/server/src/ee`, the enterprise module, which lives in a repository only its vendor can read. Docmost loads it only when the folder holds it, so the open-source build runs without it. Without this line the deploy stops at the fetch. - `migrate` runs the server's `migration:latest` script and then `node ox-first-admin.mjs`. Docmost also migrates when it boots in production. Running the migrations here keeps them out of the new release's health wait. - `[services]` asks for Postgres and Redis, and ox fills in the `DATABASE_URL` and `REDIS_URL` Docmost needs. - `[storage]` keeps `data/storage`, where Docmost stores attachments unless `STORAGE_DRIVER` says otherwise. ox keeps it across deploys. There are no workers or cron jobs. Docmost runs its queues inside the app process, and the server had no Docmost timer. `ox-first-admin.mjs` Code ``` // The first workspace and its admin, made once from ADMIN_EMAIL and // ADMIN_PASSWORD (ADMIN_NAME and WORKSPACE_NAME are optional). // // Docmost makes them only through its running server: the "Create // workspace" page posts to /api/auth/setup, which answers 403 once a // workspace exists, and there is no command for it. So, when the database // has no workspace yet, this starts the built server on a free loopback // port, waits for its health (database and redis), posts the setup as that // page does, and stops the server again. It runs in [build] migrate, after // the migrations, so the deploy goes live with a way in and the setup page // is closed to visitors. import { createRequire } from 'node:module'; import { spawn } from 'node:child_process'; import net from 'node:net'; import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; const server = path.resolve('apps/server'); const require = createRequire(path.join(server, 'package.json')); const postgres = require('postgres'); function log(msg) { console.log(`ox-first-admin: ${msg}`); } function fail(msg) { console.error(`ox-first-admin: ${msg}`); process.exit(1); } // The URL as Docmost reads it (normalizePostgresUrl): no schema parameter. const dbUrl = new URL(process.env.DATABASE_URL); dbUrl.searchParams.delete('schema'); const sql = postgres(dbUrl.toString(), { max: 1, onnotice: () => {} }); const [{ n }] = await sql`select count(*)::int as n from workspaces`; await sql.end(); if (n > 0) { log(`a workspace exists (${n}), nothing to do`); process.exit(0); } for (const key of ['ADMIN_EMAIL', 'ADMIN_PASSWORD']) { if (!process.env[key]) fail(`${key} is not set; it makes the first admin`); } const port = await new Promise((resolve, reject) => { const s = net.createServer(); s.on('error', reject); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); }); }); log(`no workspace yet: starting the server on 127.0.0.1:${port} to make it`); const child = spawn(process.execPath, ['dist/main'], { cwd: server, env: { ...process.env, NODE_ENV: 'production', HOST: '127.0.0.1', PORT: String(port) }, stdio: 'inherit', }); let exited = null; const done = new Promise((resolve) => { child.on('exit', (code, signal) => { exited = signal || String(code); resolve(); }); }); async function stop() { if (exited !== null) return; child.kill('SIGTERM'); await Promise.race([done, sleep(30_000)]); if (exited === null) { child.kill('SIGKILL'); await done; } } const base = `http://127.0.0.1:${port}`; let status = 0; let body = ''; try { let healthy = false; for (let i = 0; i < 180 && exited === null; i++) { try { const r = await fetch(`${base}/api/health`); if (r.ok) { healthy = true; break; } } catch { // not listening yet } await sleep(1000); } if (!healthy) { throw new Error( exited !== null ? `the server exited (${exited}) before it was healthy` : 'the server was not healthy after 180 s', ); } const r = await fetch(`${base}/api/auth/setup`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: process.env.ADMIN_NAME || 'Admin', email: process.env.ADMIN_EMAIL, password: process.env.ADMIN_PASSWORD, workspaceName: process.env.WORKSPACE_NAME || 'Docmost', }), }); status = r.status; body = (await r.text()).slice(0, 300); } catch (err) { await stop(); fail(err.message); } await stop(); if (status === 200 || status === 201) { log(`made the workspace and its admin ${process.env.ADMIN_EMAIL}`); } else if (status === 403) { log('the workspace was made meanwhile (setup answered 403)'); } else { fail(`setup answered ${status}: ${body}`); } ``` `ox-first-admin.mjs` runs at the end of `migrate` and makes the first workspace and admin the way the setup page does. It counts the rows in Docmost's `workspaces` table and exits if there is one, so later deploys skip it. With none, it starts the built server on a free port of its own, waits up to 180 seconds for `/api/health`, posts the admin's name, email, password and the workspace name to `/api/auth/setup`, and stops the server. Any answer but success or a 403, which means someone made the workspace first, stops the deploy before ox switches releases. Shell ```bash APP_URL APP_SECRET JWT_TOKEN_EXPIRES_IN ADMIN_EMAIL ADMIN_PASSWORD MAIL_DRIVER MAIL_FROM_ADDRESS MAIL_FROM_NAME SMTP_HOST SMTP_PORT SMTP_SECURE ``` - `APP_URL` is the address people open Docmost at. Docmost builds its links from it and marks its cookies secure when it starts with https. ox gives every project that address as `PUBLIC_URL`, and [variables](https://deploywithox.com/docs/operate/variables) explains references. - `APP_SECRET` signs the sign-in tokens. It must be at least 32 characters, and Docmost refuses the example file's placeholder. The example file makes one with `openssl rand -hex 32`. Choose it once and keep it. - `JWT_TOKEN_EXPIRES_IN` is how long a sign-in lasts: 90 days by default, `30d` in the example file. - `ADMIN_EMAIL` and `ADMIN_PASSWORD` are read only by `ox-first-admin.mjs`, and only while no workspace exists. - `MAIL_DRIVER`, `MAIL_FROM_ADDRESS`, `MAIL_FROM_NAME`, `SMTP_HOST`, `SMTP_PORT` and `SMTP_SECURE` set up mail for invitations and password resets. With `MAIL_DRIVER` unset, Docmost uses a log driver that sends nothing in production. ## Postgres and Redis Postgres is one instance shared by the projects on the server, and Docmost gets its own database and role in it. Redis is a private instance for this project, as its own unit. After the reboot the test agent found both units active with no restarts, and still active minutes later. ## Updates and backups To move to a newer release, merge its tag into your fork and push, or run `ox deploy docmost --wait`. ox builds the new release beside the running one and switches when `/api/health` answers. Before each migrate it dumps Postgres. The run's redeploy used the same commit, so a move to a newer Docmost version was not tested. Backups need no setup. The run's backup folder held two Postgres dumps, one from Back up now and the one ox takes before a migrate. `ox backups docmost` lists them, and the Backups tab shows the same list. The [backups page](https://deploywithox.com/docs/operate/backups) says a kept folder such as `data/storage` is archived every day too, and the run did not check that. A restore was not tested. ## Frequently asked questions ### Do I need Docker to self-host Docmost? No. Docmost's repo ships a Docker Compose file with Postgres and Redis containers, but the app is a Node server. ox builds it from your fork on the server with pnpm and runs it as a system service beside Postgres and Redis. An update is a push or one deploy command. ### Which database does Docmost need? Postgres and Redis, both required: Docmost's settings check rejects a missing `DATABASE_URL` or `REDIS_URL`. Postgres holds the pages and accounts, and Redis holds job queues and cache. ox starts both from two lines of `ox.toml` and passes their addresses to the app, so neither URL is in your variables file. ### How do I create the first Docmost admin? Docmost makes the first workspace through its Create workspace page, which anyone can use until a workspace exists. With ox, set `ADMIN_EMAIL` and `ADMIN_PASSWORD` before the first deploy. The migrate step creates the workspace from them, so the page is closed when the site goes live, and you sign in at `/login`. ### How much memory does Docmost use? After the run's reboot, the whole server used the memory shown in the facts, with Docmost, Redis, Postgres, Caddy and the ox agent running on it. That is what this one run used. ox takes any server from 1 GB up and tells you when memory is why an app stopped. ### Does self-hosted Docmost send data to Docmost? Yes, by default. Once a day in production, Docmost posts the version, the counts of users, pages, spaces and workspaces, and an anonymous hash of the workspace id to its telemetry address. Set `DISABLE_TELEMETRY=true` to stop it. The run left it unset, so the default applied. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-docmost#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-docmost#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-docmost#config) - [Postgres and Redis](https://deploywithox.com/blog/self-host-docmost#services) - [Updates and backups](https://deploywithox.com/blog/self-host-docmost#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-docmost#faq) - [Do I need Docker to self-host Docmost?](https://deploywithox.com/blog/self-host-docmost#faq-docker) - [Which database does Docmost need?](https://deploywithox.com/blog/self-host-docmost#faq-database) - [How do I create the first Docmost admin?](https://deploywithox.com/blog/self-host-docmost#faq-admin) - [How much memory does Docmost use?](https://deploywithox.com/blog/self-host-docmost#faq-memory) - [Does self-hosted Docmost send data to Docmost?](https://deploywithox.com/blog/self-host-docmost#faq-telemetry) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host Documenso on your own VPS without Docker](https://deploywithox.com/blog/self-host-documenso) [Newer post How to self-host Dify 1.17.1 on your own VPS, no Docker](https://deploywithox.com/blog/self-host-dify) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://deploywithox.com/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## Deploy to a server you own ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project. [Sign up with GitHub](https://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)