Get started
Requirements
What Lunedo needs to run. The setup script checks the host part itself and says what is missing.
| Requirement | Value |
|---|---|
| Operating system | Linux. Lunedo is not supported on macOS or Windows hosts, and the setup script refuses to run on macOS. |
| Container runtime | Docker Engine 24 or newer with Docker Compose v2. |
| Architecture | The image ghcr.io/lunedo/lunedo is published for linux/amd64 and linux/arm64. |
| CPU and RAM | Depend on the size of the organization; see Measured requirements. |
| Disk space | The setup script needs at least 4 GB free to install. What the data and backups need later is listed under Measured requirements. |
| Port | One free host port for Lunedo, 3000 by default. |
| Clock | Synchronized, for example with NTP. The system check warns when Lunedo and the database differ by more than 5 seconds. |
Database
Section titled “Database”| Requirement | Value |
|---|---|
| Server | PostgreSQL 16, 17 or 18. The image contains pg_dump, pg_restore and psql for these three major versions and picks the one that matches the server for backups. The Compose file of the setup script uses postgres:17-alpine. |
| Shared memory | 1 GB of /dev/shm for the PostgreSQL container (shm_size: 1gb in the Compose file of the setup script). Parallel queries on large databases fail without it, because Docker gives a container only 64 MB. It is used only while queries need it and counts towards the memory of PostgreSQL. |
| Connection settings | Lunedo turns off just-in-time compilation (jit) for its own connections by sending -c jit=off as a startup option. Nothing changes on the server. |
| Extension | pg_trgm, for search. A migration creates it with CREATE EXTENSION IF NOT EXISTS "pg_trgm". It is a trusted extension, so a database user with the CREATE privilege on the database can create it. On a managed server that does not allow this, create it beforehand as an administrator of the server. |
| Rights | The user in LUNEDO_DATABASE_URL owns the database or may create tables, types and indexes in it. Lunedo applies its migrations at every start. |
| Optional right | CREATEDB for the monthly test restore of backups, which restores into a temporary database on the same server, for example ALTER ROLE "lunedo" CREATEDB;. Without it, a test restore only verifies the backup. See Backups. |
| Free space | Room for one more copy of the database while a test restore runs. |
| Storage | An SSD for the database from size medium on; for large, an NVMe SSD is recommended. Saving a change waits until PostgreSQL has written it to disk, so the database storage should not be shared with write-heavy workloads such as CI runners, other databases or backups during working hours: on the reference machine, another workload writing to the same SSD made page opens take seconds and requests fail. On shared storage, use SSDs with power-loss protection, which confirm writes from their cache. |
Database connections
Section titled “Database connections”A container runs the worker, the collaboration server and one API process per CPU, at most 8 (LUNEDO_API_PROCESSES sets another number). Requests are spread over the API processes, so the API uses more than one CPU without a proxy in front of several containers.
Each process has its own pool of database connections. Without LUNEDO_DATABASE_POOL_SIZE, the worker and the collaboration server use twice the number of CPUs of the container plus 2, at least 6 and at most 24. A single API process uses the same number; with several API processes, it is divided among them, at least 8 each. With LUNEDO_DATABASE_POOL_SIZE, every process, each API process included, uses that number. Every process also holds one connection for notifications; the worker holds up to four more for the job queue and every API process one.
Add up for every container that uses the same database:
- worker: pool size + 5
- collaboration server: pool size + 1
- every API process: pool size + 2 + search slots
The sum, plus a few connections for backups and psql, must stay below max_connections of PostgreSQL. With the defaults, one container on up to 4 CPUs stays below the PostgreSQL default of 100, for example 78 connections on 4 CPUs; on 8 CPUs it needs 154. The PostgreSQL container of the setup script sets max_connections to 200. Raise max_connections before you raise the pool size or the number of API processes, or run more containers.
Searches at once
Section titled “Searches at once”Searches run in PostgreSQL in a lane of their own, so that many broad searches cannot slow down opening pages:
- At most
LUNEDO_SEARCH_SLOTSsearches run at the same time, counted for all API processes and containers that use the same database: by default one less than the CPUs of the container, at least 1 and at most 4. When PostgreSQL runs on another machine, set it to one less than that machine’s CPUs. - A search waits for a free slot for at most
LUNEDO_SEARCH_WAIT_SECONDS(2 by default) without holding a database connection. After that the API answers429withRetry-After, and the app says that search is busy and tries once more. - Searches use connections of their own (
lunedo-searchinpg_stat_activity, one per slot in every API process), without parallel query, and each statement may run at mostLUNEDO_SEARCH_TIMEOUT_SECONDS(5 by default). - Searches in knowledge base portals, also by visitors who are not signed in, use the same lane. When no slot becomes free in time, the portal answers
503and asks to search again, and its start page leaves out the frequent searches.
A search with more than 1,000 matches, counted as matching sections of pages and files rather than as results, counts and ranks only the 1,000 most recently changed of them; the app shows the number with a plus sign and says that it shows the most recently changed results. On large instances a broad search therefore prefers recently changed pages. Add more words to narrow it to fewer matches and find older ones.
The metrics lunedo_search_slot_wait_seconds, lunedo_search_slots_in_use and lunedo_search_rejected_total show how full the lane is; see Monitoring.
Every connection keeps the statements Lunedo uses repeatedly prepared, at most 150 per connection with their plans, so PostgreSQL does not plan them again on every call. Lunedo needs connections that keep their session: directly to PostgreSQL, or through a pooler in session mode such as PgBouncer with pool_mode = session. A pooler in transaction mode is not supported, because Lunedo uses notifications (LISTEN) and releases prepared statements with DEALLOCATE.
Limits that protect the memory of a process count per API process: diagram sanitizing and uploads, Word and PDF files created at once and password checks.
Migrations only go forward. When several processes start at the same time, a PostgreSQL advisory lock lets one of them migrate while the others wait. Readiness (/health/ready) answers 503 while the schema is behind the code. When migrations are pending, the first process takes a backup before it migrates; see Backups before updates.
Optional services
Section titled “Optional services”| Service | When it is needed |
|---|---|
| Redis or Valkey | Only when more than one collaboration server runs. Set LUNEDO_REDIS_URL on every collaboration node; see Several nodes. Any Redis-compatible server works. |
| Render container | Only for PDF files created on the server. The image ghcr.io/lunedo/render runs headless Chromium; see Rendering. |
| S3-compatible storage | Only when attachments or backups go to object storage. See File storage and Backups in S3. |
| Mail server or Microsoft Graph | For invitations, password resets and e-mail notifications. See E-mail. |
Container roles
Section titled “Container roles”One image holds every part of Lunedo. The first argument of the container chooses what it runs:
| Role | Port | Runs |
|---|---|---|
all |
3000, 3001, 3002 | The API, the collaboration server and the worker in one container. The default, and what the Compose files use. |
api |
3000 | The API and the web client. It also forwards the collaboration WebSocket on /collab to LUNEDO_COLLAB_UPSTREAM. |
collab |
3001 | The collaboration server for real-time editing. |
worker |
3002 | Background jobs: backups, mail, search indexing, imports, rendering and the update check. |
In all, only the API port needs to be published; the API forwards /collab to the collaboration server in the same container. Each process answers /health/live, /health/ready and /metrics on its own port; see Monitoring.
Every other argument runs a command instead of a server, such as backup, keys, audit, search, maintenance, version or release-notes. See Command line.
Container user and volumes
Section titled “Container user and volumes”The container runs as the user node with UID 1000 and GID 1000.
| Path in the container | Content |
|---|---|
/var/lib/lunedo |
The data volume: files (the file store, LUNEDO_FILES_DIR), secrets (generated secrets, LUNEDO_SECRETS_DIR), backups (LUNEDO_BACKUP_DIR) and work (temporary files of backups, LUNEDO_WORK_DIR). |
/data/import |
The import folder for large exports (LUNEDO_IMPORT_DIR). Mount it read-only; see Imports. |
Directories that you mount must belong to UID 1000, or the system check reports that the file store is not writable.
Measured requirements
Section titled “Measured requirements”This section is generated from docs/data/requirements.json, packages/server/src/backup/configuration.ts by pnpm docs:generate. Do not edit it by hand; change the sources and run the command again.
Measured on a reference machine with 4 vCPU (12th Gen Intel(R) Core(TM) i5-12500), 7.8 GiB of RAM and 272.5 GB of disk (ZFS on one consumer NVMe SSD), running Ubuntu 24.04.5 LTS with kernel 6.8.0-147-generic, Docker 29.9.2, Docker Compose 5.6.1 and PostgreSQL 17.11. Each size ran on a fresh installation from the setup script, without TLS and without a reverse proxy, with synthetic content and the default limits. Size, backups and start times were measured on 2026-11-01 with commit 8c70161e. The load came from one computer, which sent the requests of each person from a client address of its own, in load runs of 15 minutes.
Scenarios
Section titled “Scenarios”The load runs follow one of two scenarios. Both are assumptions about how people use a wiki, not measured use. In both, five of the people active at the same time edit the same page together.
| Scenario | Wait between two actions | Open a page | Search | Edit and publish | Notifications and For you |
|---|---|---|---|---|---|
| typical | 30 to 90 seconds, 60 on average | 70 % | 15 % | 2 % | 13 % |
| peak | 7 to 36 seconds, 18 on average | 70 % | 15 % | 10 % | 5 % |
The targets apply to typical; peak shows how much room is left. A size measured only with peak is judged by peak, and its hardware comes from peak. Response times of the first minutes of a run, while the people start and the load generator opens its connections, are not counted where the table says so; errors always are.
| Size | Scenario | Load runs | Measured on | Commit | Not counted at the start |
|---|---|---|---|---|---|
| small | peak | 3 | 2026-10-29 | 8c70161e |
nothing |
| medium | typical | 2 | 2026-11-01 | adb35241 |
4 min |
| medium | peak | 2 | 2026-11-01 | adb35241 |
4 min |
| large | typical | 2 | 2026-10-31 | 21e978c3 |
4 min |
| large | peak | 3 | 2026-10-31 | 21e978c3 |
4 min |
| Size | People | Pages | Spaces | Attachments | Active at the same time |
|---|---|---|---|---|---|
| small | 25 | 2,002 | 10 | 1.9 GB | 5 |
| medium | 250 | 20,048 | 60 | 19.5 GB | 50 |
| large | 1,000 | 99,594 | 200 | 48.9 GB | 200 |
Hardware
Section titled “Hardware”Minimum is what the reference machine used at the peak of the load runs, rounded up: CPU from the busy time of the whole machine, but at least 2 vCPU, because fewer were never measured; RAM from the Lunedo and PostgreSQL containers. Add what the operating system needs. Recommended RAM is the peak × 1.5, and recommended CPU keeps the response times below the targets with room for peaks: at least the peak × 1.5 and at least what the peak scenario used, never more than the reference machine has. Disk is the image, the database, the file store and backups, with 30 % headroom; with backups on the same host, it counts the 20 backups that the default retention keeps.
| Size | Minimum CPU | Minimum RAM | Minimum disk | Recommended CPU | Recommended RAM | Recommended disk, backups elsewhere | Recommended disk, backups on this host |
|---|---|---|---|---|---|---|---|
| small | 2 vCPU | 2 GiB | 6 GB | 3 vCPU | 3 GiB | 7 GB | 56 GB |
| medium | 2 vCPU | 5 GiB | 42 GB | 2 vCPU | 7 GiB | 55 GB | 541 GB |
| large | 2 vCPU | 5 GiB | 107 GB | 3 vCPU | 7 GiB | 139 GB | 1,371 GB |
Response times under load
Section titled “Response times under load”Median of the load runs; p95 is the time within which 95 % of the requests finished. Readable is the time until the page is shown in the browser.
| Size | Scenario | Open a page, API | Open a page, readable | Search | Save | For you and notifications | Errors |
|---|---|---|---|---|---|---|---|
| small | peak | 160 ms / 256 ms | 353 ms / 855 ms | 86 ms / 136 ms | 54 ms / 85 ms | 45 ms / 69 ms | 0 |
| medium | typical | 35 ms / 59 ms | 223 ms / 261 ms | 65 ms / 95 ms | 27 ms / 38 ms | 124 ms / 142 ms | 0 |
| medium | peak | 35 ms / 60 ms | 223 ms / 246 ms | 66 ms / 97 ms | 24 ms / 42 ms | 127 ms / 152 ms | 0 |
| large | typical | 38 ms / 67 ms | 223 ms / 249 ms | 180 ms / 229 ms | 22 ms / 59 ms | 21 ms / 38 ms | 0 |
| large | peak | 43 ms / 105 ms | 227 ms / 333 ms | 199 ms / 480 ms | 24 ms / 57 ms | 25 ms / 66 ms | 0 |
Each cell shows p50 / p95.
Targets
Section titled “Targets”| Size | Scenario | Target | Limit | Measured (worst run) | Met |
|---|---|---|---|---|---|
| small | peak | Open a page, API (p95) | below 500 ms | 263 ms | yes |
| small | peak | Open a page, readable (p95) | below 1.50 s | 879 ms | yes |
| small | peak | Search (p95) | below 800 ms | 148 ms | yes |
| small | peak | Save (p95) | below 500 ms | 101 ms | yes |
| small | peak | Errors | 0 | 0 | yes |
| medium | typical | Open a page, API (p95) | below 500 ms | 60 ms | yes |
| medium | typical | Open a page, readable (p95) | below 1.50 s | 272 ms | yes |
| medium | typical | Search (p95) | below 800 ms | 95 ms | yes |
| medium | typical | Save (p95) | below 500 ms | 39 ms | yes |
| medium | typical | Errors | 0 | 0 | yes |
| medium | peak | Open a page, API (p95) | below 500 ms | 60 ms | yes |
| medium | peak | Open a page, readable (p95) | below 1.50 s | 248 ms | yes |
| medium | peak | Search (p95) | below 800 ms | 101 ms | yes |
| medium | peak | Save (p95) | below 500 ms | 43 ms | yes |
| medium | peak | Errors | 0 | 0 | yes |
| large | typical | Open a page, API (p95) | below 500 ms | 67 ms | yes |
| large | typical | Open a page, readable (p95) | below 1.50 s | 257 ms | yes |
| large | typical | Search (p95) | below 800 ms | 231 ms | yes |
| large | typical | Save (p95) | below 500 ms | 81 ms | yes |
| large | typical | Errors | 0 | 0 | yes |
| large | peak | Open a page, API (p95) | below 500 ms | 106 ms | yes |
| large | peak | Open a page, readable (p95) | below 1.50 s | 334 ms | yes |
| large | peak | Search (p95) | below 800 ms | 538 ms | yes |
| large | peak | Save (p95) | below 500 ms | 62 ms | yes |
| large | peak | Errors | 0 | 0 | yes |
Footprint and operations
Section titled “Footprint and operations”| Size | Database | File store | Image | One full backup | Backup takes | Restore takes | Start until ready | RAM when idle | New page found in search (p95) | Job queue under load (peak) |
|---|---|---|---|---|---|---|---|---|---|---|
| small | 150 MB | 2.0 GB | 1.1 GB | 2.0 GB | 45.5 s | 22.8 s | 9.30 s | 741 MiB | 700 ms | 16 |
| medium | 1.3 GB | 19.6 GB | 1.1 GB | 19.7 GB | 7 min 25 s | 4 min 52 s | 9.30 s | 826 MiB | 1.00 s | 16 |
| large | 6.4 GB | 49.2 GB | 1.1 GB | 49.9 GB | 20 min 8 s | 17 min 4 s | 10.0 s | 885 MiB | 2.40 s | 16 |
Writing through the API
Section titled “Writing through the API”How fast the content of each size was written through the public API, as a migration script would do it: pages with their versions, attachments, comments and permissions, by several clients at once.
| Size | Clients at once | Pages per minute | Versions per minute | Requests per second | Upload | Busy CPU |
|---|---|---|---|---|---|---|
| small | 4 | 240 | 1,189 | 47 | 4.1 MB/s | 80 % |
| medium | 4 | 263 | 1,314 | 52 | 4.4 MB/s | 80 % |
| medium | 8 | 276 | 1,388 | 55 | 4.6 MB/s | 92 % |
| large | 4 | 228 | 1,142 | 43 | 2.0 MB/s | 83 % |