Menu
Penombre docs

Storage

Where Penombre keeps your files.

Files live on the host filesystem at the path configured by STORAGE_PATH. The default is $DATA_DIR/storage/data/storage, which maps to a Docker volume in the bundled compose.yaml. Running bun run dev outside production instead writes to ./data/storage inside the repo, so a dev box needs no /data mount.

DATA_DIR is the base directory for everything the app writes: the storage root above and the SQLite database ($DATA_DIR/db). Point it somewhere else and both follow; STORAGE_PATH and DATABASE_URL still override their own path.

There is nothing to configure beyond that path — Penombre is built for a single box with a disk attached, not for object storage.

Running as a shared file browser instead of a multi-user drive? See Simple mode — it changes what lives at STORAGE_PATH.

VariableDescriptionDefault
DATA_DIRBase directory for all app data/data (./data in dev)
STORAGE_PATHAbsolute path to the storage root$DATA_DIR/storage

To change the storage location, update STORAGE_PATH in your .env file and make sure the path is mounted in your container:

volumes:
  - /your/host/path:/data/storage

What lives where

File and folder metadata (names, paths, sizes, trash state, ownership) lives in the database. The bytes live under STORAGE_PATH. A backup needs both: see Deployment for the backup routine.

Each account gets its own directory there, and each shared drive gets one under drives/. A mounted volume lives wherever you mounted it instead.

Copy, move and duplicate

Duplicate in an item's menu, or ⌘D / Ctrl+D with one item selected, makes a copy beside it — a folder with everything inside. Copy to… and Move can send items to another drive or volume as well as another folder; see moving files in and out.

Big folders

A folder, a shared drive, a mounted volume, a share, the starred view, a category and the trash all load a page at a time, so a folder of tens of thousands of files opens as fast as a small one. Folders come first, then files, in the order you picked (name, size or last modified); the server applies it, so a row further down is still in the right place. Settings → Display → Loading mode picks infinite scroll or previous/next pages. Select all selects what is loaded and says so. Recent still loads whole.

Through the API those listings answer with a nextCursor: pass it back as cursor for the next page (limit, sort and dir stay the same), until it comes back null. total counts every row, not just the page. The trash listing also carries totalSize, the bytes emptying the whole trash frees.

The trash

Deleting an item moves it to the trash, where it still occupies disk space until it is removed for good. A folder goes in whole: its files and subfolders travel with it, and restoring the folder brings them all back. The trash lists the folder itself rather than everything inside it, so what you see is what you selected; a folder's size is what its trashed contents hold.

Empty Trash empties everything, not just the rows loaded so far, and its confirmation counts and prices the whole trash. It hands the job to the server in one request, which is what lets it report the exact amount of space it gave back. A file whose bytes cannot be removed (a read-only volume, a permissions problem) keeps its entry in the trash and is counted in the message, rather than disappearing from the list while its data stays on disk.

Seeing what you use

Settings → Storage reports your own usage: total bytes across your live files, a breakdown by category, how much the trash is still holding, and your ten largest files. The bar shows your usage against the whole volume — the lighter segment is everything else on that filesystem, including other users' files, since Penombre shares one disk between accounts.

Admins get the instance-wide view under Admin → Storage: the resolved storage path, total files and bytes, trashed bytes across all accounts, and a per-user usage table sorted biggest first.

Both views read aggregates from the database plus one statfs call, so they stay cheap on a large library — nothing walks the object store.

Using a network share

Anything the container can see as a directory works — an NFS or SMB mount, a ZFS dataset, an external drive. Mount it on the host and point the volume at it:

volumes:
  - /mnt/nas/penombre:/data/storage

Penombre needs read/write access and reports free space from that filesystem, so mount the share before the container starts.

Syncing with Syncthing

Penombre has no sync client of its own, and isn't going to grow one — a directory of files is exactly what Syncthing already does well. Point it at the same directory the container mounts:

volumes:
  - /srv/penombre/files:/data/storage

…then share /srv/penombre/files as a Syncthing folder with your laptop or phone. Files land on disk and Penombre picks them up.

Use simple mode for this. Simple mode re-scans the storage root every 60 seconds, so anything Syncthing writes shows up in the UI on its own. In the default drive mode nothing rescans — every file is expected to arrive through an upload that also wrote its database row — so files dropped in from outside stay invisible.

Two things to set on the Syncthing side:

  • Ignore the thumbnail cache. Add .thumbnails to the folder's ignore patterns — Penombre regenerates it per instance, so syncing it to every device is pure waste.
  • Pick a conflict strategy. Syncthing keeps both sides of a conflict as *.sync-conflict-* files; Penombre lists them like any other file rather than resolving them for you.

Simple mode cannot run with ENCRYPTION_KEY (see Encryption). On an encrypted drive-mode instance, anything that reads STORAGE_PATH behind Penombre's back, Syncthing included, sees sealed files, not your documents.

This guide lives in the project repo: edit it there, and this page follows within a day.