How to Deploy Trilium on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host the Trilium Notes server on a small VPS — a pinned Docker image, one data directory, HTTPS through Caddy, the trusted-proxy setting, and backups beyond the built-in ones.
- A small VPS — 1 vCPU / 512 MB–1 GB RAM is plenty
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain or subdomain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What Trilium is
Trilium Notes is a hierarchical note-taking app for building a personal knowledge base, maintained today by the TriliumNext project under the AGPL-3.0 licence. Notes live in a tree, a single note can be cloned into several places in that tree, individual notes can be encrypted, and there are code notes and a scripting layer for people who want to automate their notes.
There are two ways to run it: the desktop app on its own, or a server that serves the same interface in a browser and acts as the sync server for the desktop apps. This guide sets up the server. It is a Node.js application with an SQLite database, so it is one container and one data directory — no separate database service.
It's a personal tool first. If you need a team wiki with permissions and co-editing, look at Docmost or BookStack instead.
Server sizing
Trilium is light. The catalog lists a 512 MB RAM floor, and on our install-verification run (GCP e2-standard-2, Ubuntu 26.04) the server idled at about 61 MB of RAM and used roughly 490 MB of disk for the image and data.
- 512 MB–1 GB RAM / 1 vCPU — plenty for one person or a household.
- Disk grows with attachments and images you paste into notes, plus the automatic backup copies of the database (see Backups). 10–20 GB is comfortable.
The official image is published for AMD64 and ARM64, so an ARM VPS works too.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along
with a non-root user and a ufw firewall. If not, work through
Docker & Compose on Ubuntu first.
Only SSH, 80 and 443 should be reachable from outside:
sudo ufw status verbose
Upstream's compose file carries a warning worth repeating: Docker's published
ports bypass ufw. A port mapped as 8080:8080 is reachable from the
internet even if ufw denies 8080. That's why the compose file below binds
Trilium to 127.0.0.1 only.
Paid link — we earn a commission if you shop through it.
Install Trilium (Docker Compose)
Create the project directory and a data directory next to it:
mkdir -p ~/trilium/trilium-data && cd ~/trilium
Upstream's Docker docs warn against the latest tag, because it can move your
instance to a new version on its own and disrupt sync with your desktop apps.
Pin a version instead — v0.106.0 was the current release when this guide was
verified — and change it deliberately when you upgrade:
cat > docker-compose.yml <<'YAML'
services:
trilium:
image: triliumnext/trilium:v0.106.0
container_name: trilium
restart: unless-stopped
environment:
- TRILIUM_DATA_DIR=/home/node/trilium-data
# Behind Caddy on the same host: trust X-Forwarded-For from private
# addresses (Docker's bridge network is one).
- TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=uniquelocal
ports:
# Loopback only: Caddy is the only way in from outside.
- "127.0.0.1:8080:8080"
volumes:
- ./trilium-data:/home/node/trilium-data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
YAML
The trilium-data directory holds everything: the document.db SQLite
database, config.ini, logs and the automatic backups.
TRILIUM_NETWORK_TRUSTEDREVERSEPROXY is the environment-variable form of the
[Network] trustedReverseProxy setting; upstream asks you to set it behind a
reverse proxy so authentication and rate limiting see real client addresses
rather than the proxy's. The docs accept true or an address list; use an
address shortcut such as uniquelocal here. On our verification run, v0.106.0
refused to start with the environment variable set to true ("invalid IP
address: true"), so don't copy true from config.ini examples into the
environment.
Start it and wait for the server to answer:
docker compose up -d
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)
case "$code" in 200|302) break ;; esac
sleep 5
done
docker compose ps
echo "Trilium answered HTTP $code"
case "$code" in 200|302) true ;; *) false ;; esac
A 200 or a 302 redirect to the setup page means it's up.
HTTPS + domain
Point an A record for notes.example.com at the server, wait for it to
resolve, then terminate TLS in front of 127.0.0.1:8080 with
Automatic HTTPS with Caddy:
notes.example.com {
reverse_proxy 127.0.0.1:8080
}
Trilium's interface uses WebSockets for live updates; Caddy proxies them
without extra configuration. If you run Caddy as a container, proxy to
trilium:8080 on a shared network and remove the host port publish.
First run
Open https://notes.example.com. A fresh server shows a setup screen:
- Choose to start a new document if this server will be your primary copy.
- Choose to sync from a desktop app if you already have notes in Trilium desktop and want this server to receive them.
- A restore from backup option is also offered on a new instance.
Then set the password. There is one password per server — Trilium is a single-user application — so make it long and unique. From then on the login page protects the instance. Two further options are in the upstream docs:
- TOTP multi-factor authentication and OpenID Connect sign-in are supported; both are worth enabling on an internet-facing server.
noAuthentication=trueturns the login off entirely. Upstream intends it for localhost-only instances or when something in front already authenticates users. Don't use it on a public server.
To connect a desktop app, choose the sync option on its first start and enter
https://notes.example.com as the server address, then the server password.
Backups
Trilium already keeps some backups on its own: it copies the database once a
day, once a week, once a month, and before every database migration into
trilium-data/backup/. That protects against a bad upgrade or a mistaken edit,
but those copies sit on the same disk as the original, so they don't survive
losing the server.
For an off-box copy, stop the container briefly so the SQLite file is consistent, archive the whole data directory, and start it again:
cd ~/trilium
docker compose stop
sudo tar czf trilium-backup-$(date +%F).tar.gz trilium-data
docker compose start
ls -lh trilium-backup-*.tar.gz
Copy the archive off the server. To restore by hand, upstream's procedure
is: stop Trilium, delete document.db plus the document.db-wal and
document.db-shm files, put your backup in place as document.db, and start
it again. If you sync with desktop apps, restore on every member of the sync
cluster, otherwise the older data can be synced over newer notes.
A synced desktop app is also a copy of your notes, but a sync client replicates deletions too — it isn't a substitute for the archive.
Upgrades
Edit the image tag to the new version, then:
cd ~/trilium
docker compose pull
docker compose up -d
Trilium backs up the database before running a migration. Keep the server and
your desktop apps on the same version: sync between different versions can
fail, which is the reason upstream steers you away from latest. Take an
archive backup before a version jump and read the release notes.
Troubleshooting
The container starts but the page never loads. Check
docker compose logs trilium. Fatal error ... invalid IP address: true means
the trusted-proxy variable is set to true; use an address value instead. A permissions error on the data directory is
the common cause; upstream says the container needs root to set itself up and
supports USER_UID / USER_GID environment variables to change the owner of
the saved data. The --user directive is not supported.
Desktop sync fails after an upgrade. The server and desktop versions don't match. Upgrade both to the same release.
Login attempts from everyone are rate-limited together. Trilium is seeing
the proxy's address for every client. Make sure
TRILIUM_NETWORK_TRUSTEDREVERSEPROXY is set (to an address shortcut such as
uniquelocal, as above) and the container was recreated after adding it.
The data directory is on a network share and the database locks. Upstream
notes that an SMB/CIFS share needs the nobrl and noperm mount options. A
local disk avoids the problem.
Verification + next steps
You're done when https://notes.example.com loads with a valid certificate,
the login page asks for your password, a desktop app can sync against the
server if you use one, and a trilium-backup-*.tar.gz sits somewhere off the
box.
Next: turn on TOTP, schedule the archive with cron, and decide how long you want to keep the built-in backups. Considering a team tool instead? The Wiki.js guide and the Outline guide cover shared wikis. For host picks, see Best VPS for Self-Hosting.