-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
Summary
The standard triliumnext/trilium image's entrypoint (start-docker.sh) requires root privileges that are unavailable in rootless Docker environments with security hardening (read_only, cap_drop: ALL, no-new-privileges). The referenced docker-compose.rootless.yml uses triliumnext/notes:rootless, but this tag does not exist on Docker Hub for either triliumnext/notes or triliumnext/trilium.
Environment
- Trilium version: v0.101.3
- Docker: 29.2.1 (rootless mode)
- Docker Compose: v5.0.2
- OS: Ubuntu 24.04.4 LTS (kernel 6.8.0-100-generic)
Problem
The start-docker.sh entrypoint runs:
chown -R node:node /home/node
exec su -c "node ./main.cjs" nodeThis requires:
CAP_CHOWN, CAP_DAC_OVERRIDE for chown
CAP_SETUID, CAP_SETGID for su
A writable filesystem at /home/node
no-new-privileges must be disabled (required for su)
In a security-hardened setup (read_only: true, cap_drop: ALL, security_opt: no-new-privileges), the container fails with:
chown: /home/node/trilium-data: Operation not permitted
chown: /home/node: Read-only file system
su: can't set groups: Operation not permittedWorkaround
We bypass the entrypoint entirely by setting user: "1000:1000"
and running node ./main.cjs directly. A one-shot init container
fixes volume ownership. This preserves full security hardening:
trilium-init:
image: alpine:${ALPINE_TAG:-3.23.3}
container_name: trilium-init
command: ["chown", "1000:1000", "/data"]
volumes:
- trilium-data:/data
restart: "no"
read_only: true
mem_limit: 32M
memswap_limit: 32M
cpus: 0.25
pids_limit: 8
cap_drop:
- ALL
cap_add:
- CHOWN
security_opt:
- no-new-privileges
trilium:
image: triliumnext/trilium:${TRILIUM_TAG:-v0.101.3}
container_name: trilium
user: "1000:1000"
command: ["node", "./main.cjs"]
restart: unless-stopped
read_only: true
mem_limit: 512M
memswap_limit: 512M
cpus: 1.0
pids_limit: 128
cap_drop:
- ALL
security_opt:
- no-new-privileges
depends_on:
trilium-init:
condition: service_completed_successfully
traefik:
condition: service_healthy
environment:
- TZ=${TZ:-UTC}
- TRILIUM_DATA_DIR=/home/node/trilium-data
- TRILIUM_TRUSTED_REVERSE_PROXY=true
volumes:
- trilium-data:/home/node/trilium-data
tmpfs:
- /tmp:noexec,nosuid,nodev,size=64M
healthcheck:
test: ["CMD", "nc", "-z", "127.0.0.1", "8080"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
labels:
- "traefik.enable=true"
- "traefik.http.routers.trilium.rule=Host(`${TRILIUM_SUBDOMAIN:-hz-notes}.${BASE_DOMAIN:-hz.tecio.xyz}`)"
- "traefik.http.routers.trilium.entrypoints=websecure"
- "traefik.http.routers.trilium.tls.certresolver=myresolver"
- "traefik.http.services.trilium.loadbalancer.server.port=8080"
- "traefik.http.routers.trilium.middlewares=crowdsec-bouncer@file,authentik@file,security-headers@file,rate-limit@file"
networks:
- traefik-network
Suggestions
Publish the rootless image: The docker-compose.rootless.yml in this repo references triliumnext/notes:rootless, but this tag does not exist on Docker Hub. Publishing it would solve this cleanly.
Use gosu instead of su: gosu is compatible with no-new-privileges and doesn't require CAP_SETUID/CAP_SETGID in the same way. Many official Docker images (postgres, redis) use this pattern.
Skip chown when already correct: The entrypoint could check ownership before running chown, avoiding the need for CAP_CHOWN/CAP_DAC_OVERRIDE when the volume is already owned by the right user.