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.
| Variable | Description | Default |
|---|---|---|
DATA_DIR | Base directory for all app data | /data (./data in dev) |
STORAGE_PATH | Absolute 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/storageWhat 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/storagePenombre 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
.thumbnailsto 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.