Trilium Docker Compose Example
This example is a small, working docker-compose.yml for Trilium Notes, the open-source note-taking app for large personal knowledge bases. Copy it and you have Trilium running on port 8080 with all your notes on a persistent volume.
The stack is one container. No database service, no Redis, no worker — Trilium keeps everything in an embedded SQLite file inside its data directory.
1. docker-compose.yml
Create a folder and add:
services:
trilium:
image: triliumnext/trilium:latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- TRILIUM_DATA_DIR=/home/node/trilium-data
- TZ=Europe/Berlin
volumes:
- trilium-data:/home/node/trilium-data
volumes:
trilium-data:
Key points:
127.0.0.1:8080:8080binds the port only to localhost. Remote access goes through the reverse proxy, not this port. This matters more than usual here: a fresh Trilium has no password until you set one, so an instance exposed straight to the internet can be claimed by whoever finds it first.trilium-datais a named Docker volume holding the SQLite database, attachments, images, logs, and Trilium's own automatic backups. This one volume is the entire instance — back it up.TRILIUM_DATA_DIRis what makes Trilium write into the mounted path. The image defaults to/home/node/trilium-dataanyway, but setting it explicitly means a changed default in a future image cannot quietly move your data somewhere unmounted.
The upstream Compose file also bind-mounts /etc/timezone and /etc/localtime read-only. The TZ environment variable does the same job with less coupling to the host layout, so this example uses that instead.
2. Start the stack
docker compose up -d
docker compose logs -f trilium
Trilium starts in a few seconds and prints its data directory and port. On first start it says the database is not initialized — that is expected.
3. Set the password immediately
Visit http://localhost:8080. Trilium asks you to create a new document and set an admin password.
Do this before you point a domain at it. Between first boot and the moment you set a password, the setup page accepts anyone.
4. Add a reverse proxy
Trilium serves plain HTTP. Put Caddy in front for HTTPS:
notes.example.com {
reverse_proxy 127.0.0.1:8080
}
Caddy fetches a Let's Encrypt certificate automatically the first time notes.example.com resolves to your server.
Then tell Trilium it is behind a proxy, so its rate limiter reads the real client IP instead of the proxy's:
environment:
- TRILIUM_DATA_DIR=/home/node/trilium-data
- TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=uniquelocal
The value must identify the proxy by address, not by position. uniquelocal is an Express shortcut for the private ranges (10/8, 172.16/12, 192.168/16), which is where your proxy sits if it runs in a Docker network. Use loopback if the proxy talks to a port published on 127.0.0.1, or the proxy's exact IP or CIDR if you want to be strict.
true or a hop counttrue crashes the container at startup (TypeError: invalid IP address: true).
A hop count like 1 is worse: it starts fine and matches nothing, because the
value reaches Express as a string and gets read as the address 0.0.0.1. Both
forms are valid in config.ini and neither survives the environment variable.
Without this setting Trilium still works, but every proxied request logs an ERR_ERL_UNEXPECTED_X_FORWARDED_FOR error from the rate limiter, and rate limiting counts every visitor as the same client. A wrong-but-accepted value like 1 silences the log message and leaves the rate limiter just as blind.
5. Updating Trilium
docker compose pull
docker compose up -d
Trilium migrates its database schema on start. The data volume survives the restart, so notes, attachments, and your password stay intact. Pin a version tag like v0.104.1 instead of latest if you would rather choose when a migration happens.
Common issues
Container exits immediately with "invalid IP address"
You set TRILIUM_NETWORK_TRUSTEDREVERSEPROXY=true. See the box above — use uniquelocal, or the proxy's IP or CIDR.
Notes disappeared after a restart
The volume was not mounted where Trilium writes. Check that TRILIUM_DATA_DIR and the volume mount path are the same string, and confirm with:
docker compose exec trilium ls /home/node/trilium-data
You should see config.ini and document.db (plus document.db-wal while the container runs).
Permission errors on the data directory
Usually not an issue here. The image starts as root, runs chown -R node:node /home/node, then drops to the node user, so it fixes the ownership of a fresh volume itself. If you need it to match a specific host UID for a bind mount, pass USER_UID and USER_GID:
environment:
- USER_UID=1000
- USER_GID=1000
Login works but sessions do not stick
Trilium's session cookie is SameSite=Lax and not marked Secure, because Trilium itself is speaking HTTP behind your proxy. Make sure the proxy passes the original Host header through — a rewritten host makes the browser drop the cookie.
One-click deploy on Hostim
If you would rather skip the Compose file, the reverse proxy, and the certificate, Hostim has a one-click Trilium template with HTTPS and a persistent volume preconfigured — including the reverse-proxy setting above, already set correctly.
Frequently asked questions
What port does Trilium use in Docker?
Trilium listens on port 8080 inside the container. In the example above it is published as '127.0.0.1:8080:8080', which keeps it reachable only from the host — remote access goes through the reverse proxy instead of the raw port.
Which volumes does Trilium need in Docker Compose?
One. Everything lives under '/home/node/trilium-data': the SQLite database ('document.db'), attachments, images, logs, and Trilium's automatic backups. Set 'TRILIUM_DATA_DIR' to that same path so a future change to the image default cannot move your data off the volume.
Does Trilium need a separate database container?
No. Trilium uses an embedded SQLite database stored in its data directory. There is no PostgreSQL, MySQL, or Redis service in the stack — one container is the whole deployment.
Why does Trilium crash with 'invalid IP address: true'?
Because 'TRILIUM_NETWORK_TRUSTEDREVERSEPROXY' was set to 'true'. That value is valid in config.ini but not through the environment variable, where it arrives as a string and gets parsed as an IP address. Set it to the proxy's IP or CIDR, or to an Express shortcut such as 'uniquelocal' or 'loopback'. Do not use a hop count like '1' — it starts without error but matches nothing, because only a real number is treated as a hop count and the string is read as the address 0.0.0.1.
Does Trilium handle HTTPS itself?
It can serve TLS directly if you set 'https=true' and provide certificate paths, but the normal setup is a reverse proxy such as Caddy, Traefik, or Nginx in front of plain HTTP on port 8080. A managed host that terminates HTTPS for you removes the step entirely.
Is a new Trilium instance password-protected by default?
No. On first boot Trilium shows a setup page and accepts a password from whoever reaches it first. Bind the port to localhost, set the password immediately after starting the container, and only then point a public domain at it.