Skip to content

Standard image incompatible with rootless Docker and security-hardened containers #8734

@tecio-first

Description

@tecio-first

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" node

This 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 permitted

Workaround

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Difficulty: HardState: TriageIssues that need to be verifiedcontainerIssues related to containers, DockerHub or GHCR

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions