Skip to Content

Production Guide: Deploy TriliumNext Notes with Docker Compose + Caddy + Backups on Ubuntu

A production-oriented private knowledge base deployment with TLS, backups, recovery checks, and practical operations.

TriliumNext Notes is a strong fit when a team wants a private knowledge base for runbooks, project notes, customer handoff details, and engineering decisions without sending that information to a hosted SaaS workspace. This guide deploys TriliumNext on a single Ubuntu server with Docker Compose, Caddy-managed HTTPS, persistent local data, and a repeatable backup routine. The goal is not just to make the login page appear; the goal is to leave behind a deployment that can be upgraded, restored, and operated by someone who was not present during the initial install.

Architecture and flow overview

The production shape is intentionally simple. Public traffic reaches Caddy on ports 80 and 443, Caddy obtains and renews TLS certificates, then forwards requests over a private Docker network to the TriliumNext container. TriliumNext stores its application data in a mounted host directory under /opt/triliumnext/data. A small systemd timer runs a backup script that creates compressed archives containing the notes data, environment file, Compose file, and Caddyfile. For a small team, this keeps the operational model understandable while still covering the basics: HTTPS, restart policies, health checks, backups, and restore testing.

Use this pattern for internal documentation, lightweight personal knowledge management, client notes, implementation checklists, or a private wiki for a small operations team. If you need multi-region high availability, central SSO, formal records retention, or strict compliance controls, treat this guide as the single-node baseline and add those controls before storing regulated data.

Prerequisites

  • Ubuntu 22.04 or 24.04 server with a non-root sudo user.
  • A DNS record such as notes.example.com pointing to the server.
  • Ports 80 and 443 open from the internet for Caddy certificate issuance.
  • At least 2 CPU cores, 2 GB RAM, and storage sized for attachments and backups.
  • A backup destination outside the server if the notes are business-critical.
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw jq sqlite3
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker $USER

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Step-by-step deployment

Start by creating a dedicated application directory. Keeping the service under /opt/triliumnext makes permissions, backups, and future handoffs easier. The umask 077 line ensures new files such as .env are not world-readable.

sudo mkdir -p /opt/triliumnext/{data,backups,caddy}
sudo chown -R $USER:$USER /opt/triliumnext
cd /opt/triliumnext
umask 077
touch .env docker-compose.yml caddy/Caddyfile

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Create the environment file and replace notes.example.com with the hostname you actually control. This file is deliberately small because secrets and deployment-specific settings should be easy to audit. Do not commit it to a public repository.

cat > /opt/triliumnext/.env <<'EOF'
TRILIUM_HOST=notes.example.com
TRILIUM_PORT=8080
TZ=UTC
BACKUP_RETENTION_DAYS=14
EOF
chmod 600 /opt/triliumnext/.env

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Next, define the Compose stack. The TriliumNext container is not exposed directly on the host; only Caddy publishes ports. That keeps the application behind one TLS and header policy while still allowing Caddy and TriliumNext to communicate over the internal Docker network.

cat > /opt/triliumnext/docker-compose.yml <<'EOF'
services:
  triliumnext:
    image: triliumnext/notes:latest
    container_name: triliumnext
    restart: unless-stopped
    environment:
      - TRILIUM_DATA_DIR=/home/node/trilium-data
      - TZ=${TZ}
    volumes:
      - ./data:/home/node/trilium-data
    networks:
      - web
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5

  caddy:
    image: caddy:2-alpine
    container_name: triliumnext-caddy
    restart: unless-stopped
    depends_on:
      - triliumnext
    ports:
      - "80:80"
      - "443:443"
    environment:
      - TRILIUM_HOST=${TRILIUM_HOST}
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - web

networks:
  web:

volumes:
  caddy_data:
  caddy_config:
EOF

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Now add the Caddyfile. Caddy handles certificate issuance automatically, so the main operational requirement is that DNS already points to the server and inbound HTTP/HTTPS traffic is allowed. The security headers here are conservative defaults for a private notes application.

cat > /opt/triliumnext/caddy/Caddyfile <<'EOF'
{$TRILIUM_HOST} {
  encode zstd gzip
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
  reverse_proxy triliumnext:8080
}
EOF

docker compose --env-file .env config >/tmp/triliumnext-compose.rendered.yml

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Start the stack and confirm that Docker reports both services as running. The first TLS request may take a moment while Caddy obtains the certificate. If the domain is not live yet, use docker compose logs -f caddy to watch the ACME challenge status before troubleshooting the application itself.

cd /opt/triliumnext
docker compose --env-file .env pull
docker compose --env-file .env up -d
docker compose ps
curl -I https://notes.example.com

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Configuration and secrets handling best practices

Treat TriliumNext notes as sensitive operational data. The instance may eventually contain customer names, architecture diagrams, passwords pasted by mistake, incident timelines, or internal decisions. Keep the server patched, restrict SSH, and avoid sharing one administrative account across the team. If you place the service behind a VPN or identity-aware proxy later, leave Caddy in place and put the additional access control in front of it.

The .env file should be readable only by the deployment user and root. If you extend this stack with SMTP settings, object storage credentials, or external backup credentials, keep those values out of shell history and out of Git. Use a password manager or secret manager for the canonical copy. For emergency recovery, store an encrypted copy of the environment file with your backup documentation.

For storage, monitor both the live data directory and the backup directory. Notes applications tend to grow quietly because screenshots, PDFs, and pasted images accumulate over time. If the application becomes important to the business, ship backups to object storage or another host rather than relying only on local archives.

Verification checklist

Verification should prove that the app is reachable, containers are healthy, TLS works, backups run, and a restore path exists. Run these commands after deployment and again after major upgrades.

cd /opt/triliumnext
docker compose ps
docker inspect --format='{{json .State.Health}}' triliumnext | jq
curl -fsS https://notes.example.com/login >/dev/null && echo "login page ok"
/opt/triliumnext/backup.sh
ls -lh /opt/triliumnext/backups | tail
sudo systemctl list-timers triliumnext-backup.timer

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

  • Create a test note, attach a small file, log out, and log back in.
  • Confirm the browser shows a valid certificate for the expected hostname.
  • Download the latest backup archive and confirm it is not empty.
  • Document the first admin account owner and the restore location.

Backups and recovery

The backup job below briefly stops TriliumNext before archiving the data directory. That is the simplest way to avoid copying a SQLite database while it is being written. The interruption is usually short and acceptable for a small private notes service. If your team needs near-zero interruption, move to storage-level snapshots or a database-specific online backup process and test it under load.

cat > /opt/triliumnext/backup.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/triliumnext
stamp=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p backups
# TriliumNext uses SQLite files in the data directory; stop briefly for a consistent archive.
docker compose --env-file .env stop triliumnext
tar -czf "backups/triliumnext-${stamp}.tgz" data .env docker-compose.yml caddy/Caddyfile
docker compose --env-file .env up -d triliumnext
find backups -type f -name 'triliumnext-*.tgz' -mtime +"${BACKUP_RETENTION_DAYS:-14}" -delete
EOF
chmod 700 /opt/triliumnext/backup.sh
sudo tee /etc/systemd/system/triliumnext-backup.service >/dev/null <<'EOF'
[Unit]
Description=Backup TriliumNext notes
[Service]
Type=oneshot
EnvironmentFile=/opt/triliumnext/.env
ExecStart=/opt/triliumnext/backup.sh
EOF
sudo tee /etc/systemd/system/triliumnext-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Run TriliumNext backup nightly
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now triliumnext-backup.timer

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

A backup that has never been restored is only a hopeful file. Practice a restore on a test server or during a maintenance window. The restore command below shows the basic flow: stop the stack, move the broken data aside, unpack a known-good archive, and restart the service.

cd /opt/triliumnext
docker compose --env-file .env down
sudo mv data data.broken.$(date -u +%s)
tar -xzf backups/triliumnext-YYYYMMDDTHHMMSSZ.tgz
chmod 600 .env
docker compose --env-file .env up -d
docker compose logs --tail=100 triliumnext

Manual copy fallback: select the command block above and copy it if the clipboard button is unavailable.

Common issues and fixes

Caddy cannot obtain a certificate

Check DNS first. The hostname must point to the server running Caddy. Then confirm that no cloud firewall, host firewall, or old web server is blocking ports 80 and 443. Caddy logs usually make ACME failures clear.

The container starts but the site shows a gateway error

Run docker compose ps and inspect TriliumNext logs. A gateway error normally means Caddy is healthy but the upstream application is not ready, crashed, or attached to the wrong network.

Uploads work until the disk fills

Move attachments and backups to larger storage before the disk is full. Add disk alerts, rotate backups, and avoid keeping unlimited local archives on the same filesystem as the application.

Login works locally but not through the public URL

Confirm the public hostname in .env, the Caddy reverse proxy target, and browser console errors. Also check whether another proxy or CDN is rewriting headers unexpectedly.

Backups are created but restore fails

Make sure the archive includes data, .env, the Compose file, and the Caddyfile. Test restore commands with the same user and permissions used by the production service.

FAQ

Is TriliumNext better with SQLite or PostgreSQL?

This guide uses the default local data model because it keeps a single-node deployment simple. If the project adds a supported external database path later for your use case, evaluate it separately and test migrations before changing production.

Can I run TriliumNext behind Cloudflare?

Yes, but keep the origin certificate and DNS mode clear. If Cloudflare is proxying traffic, ensure WebSocket and large upload behavior still works, and do not rely on Cloudflare as your only access control.

How often should I back up notes?

Nightly is a practical default for many teams. Increase frequency if the notes contain active incident work, customer handoffs, or daily operational changes that would be painful to recreate.

Should I put this behind a VPN?

For private company notes, a VPN or identity-aware proxy is a strong improvement. Public TLS is still useful, but network-level restriction reduces exposure and makes brute-force attempts less likely.

How do I upgrade safely?

Take a backup first, read the release notes, pull the new image, restart the stack, and verify login, note editing, attachment viewing, and backup creation. Keep the previous backup until users confirm the upgrade.

Can multiple users share one account?

Avoid shared accounts for production knowledge bases. Named users make access reviews, offboarding, and incident investigation much cleaner, even for a small team.

What should I monitor?

Monitor container health, HTTPS availability, disk usage, backup age, and failed login patterns. A notes system is quiet infrastructure, so alerts should focus on availability and recoverability.

Internal links

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Paperless-ngx with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
A production-ready document management stack with OCR, HTTPS, backups, and recovery checks.