Network Storage (NAS)
Mount SMB shares from a NAS as workspace folders and move media folders onto them, configured under Settings -> Storage.
Admins can attach SMB shares from a NAS as folders inside the workspace and move large workspace folders onto them. Media and large files then stop filling the server disk, while every path in Clapilot stays the same.
Configuration lives on the admin page Settings -> Admin -> Storage (/admin/storage), card Network storage (NAS). Nothing is configured through environment variables.
What an admin configures
Each share is one row in network_storage_mounts:
| Field | Notes |
|---|---|
| Workspace folder | Top-level workspace folder the share appears as, e.g. NAS -> /app/workspace/NAS. Letters, digits, space, ., -, _; must start with a letter or digit. Fixed after creation because moved folders link into it. Reserved: tmp, mandanten, modules, skills, memory, specialized-agents, node_modules, workspace-seed. An existing workspace folder with files is refused because the mount would hide them. |
| SMB address | smb://host/share[/subfolder], \\host\share\subfolder, //host/share or host/share; an optional port as smb://host:4455/share. Credentials inside the address are rejected. Stored normalized as smb://…. |
| Username / password / domain | Optional (empty = guest). The password is stored enc:v1 (AES-256-GCM, key from CLAPILOT_AGENT_CONFIG_SECRET or AUTH_SECRET, see src/lib/secret-crypto.ts) and never returned; the API only reports hasPassword. |
| SMB version | auto (negotiate) or pin 3.1.1, 3.0, 2.1, 2.0. |
| Read-only | Mounts with ro. |
| Enabled | Disabling unmounts the share in every service but keeps the folder guarded (see below). |
Connecting is an explicit save, not autosave, like other credential dialogs. Every change bumps config_revision, and a trigger sends NOTIFY clapilot_network_storage, so all services remount within seconds. Reconnect bumps the revision without other changes.
Choosing the share
A mounted share behaves like any workspace folder: users, modules and the agent (including its shell tool) can create, change and delete everything in it, and a recursive delete of the folder's contents reaches the NAS. Point Clapilot at a subfolder reserved for it, for example smb://nas/media/clapilot, rather than a share root that also holds other data. Keep the NAS's snapshots or recycle bin enabled for that folder. The File Explorer refuses to delete or rename the share folder itself, but that guard does not cover shell commands.
How mounting works
The web, agent and streamer containers share the workspace volume but have separate mount namespaces, so each one mounts the share itself:
entrypoint.shstarts the helper while the container is still root, before it drops to thenodeuser. The first pass runs synchronously (bounded to 45 s) so a reachable share is mounted before the app starts; an unreachable NAS never blocks startup.- The helper then keeps running as the only root process in the container. It listens on
clapilot_network_storage, polls every 30 s (CLAPILOT_NETWORK_STORAGE_POLL_MS), and mounts withmount.cifs(packagecifs-utils). - The Next.js server, agent, streamer and all workers run as
nodewithout capabilities. They only see the mounted folder.
Root must never run code the node user can change, because the agent's shell tools run as node. The image therefore ships a root-owned copy of the helper, its core module and pg under /opt/clapilot-network-storage/; the source lives in scripts/network-storage-helper.mjs and scripts/lib/network-storage-core.mjs. /app/entrypoint.sh is root-owned too. The entrypoint starts the helper as /usr/local/bin/node, and the helper runs mount, umount, chattr, lsattr and stat with a fixed root-only PATH. A share is only mounted onto a verified plain directory that already carries the immutable flag. An immutable directory cannot be renamed or replaced, so the mount target cannot be swapped for a symlink out of the workspace between the check and the mount. After mounting, the helper verifies the mount point in /proc/self/mountinfo.
Mount options: uid/gid of node, file_mode=0664, dir_mode=0775, soft and echo_interval=15 (I/O fails with an error instead of hanging when the NAS goes away), noserverino, mfsymlinks (symlinks inside the share work), nosuid, nodev, iocharset=utf8. Credentials are passed through a 0600 credentials file under /run/clapilot-network-storage/ that is deleted right after the mount call. The password never appears in process arguments or /proc/mounts.
Each helper writes its state per share and service to network_storage_mount_status (mounted, error with an error code, or disabled, plus free and total bytes from statfs). It also writes its capabilities to network_storage_services. Health checks run stat -f in a killable child process with a 10 s timeout. A share that stays unreachable for 5 minutes is lazily unmounted and mounted again with exponential backoff (30 s up to 10 min). SMB reconnects on its own when the NAS comes back, usually within seconds.
Fail-safe for offline shares
Every share folder contains a read-only marker file .clapilot-network-storage, and the underlying (local) directory gets the immutable flag (chattr +i). While a share is not mounted in a container, nothing can be created in that folder there, not even as root. Media therefore never lands silently on the local disk and disappears later under the mount. Writes fail loudly (EPERM, or ENOENT inside moved folders) until the share is back. A workspace filesystem without immutable-flag support reports guard_unavailable and is not mounted.
Removing a share unmounts it everywhere and deletes the empty folder. The files on the NAS are never touched.
Error codes shown in the UI
access_denied (login or share permissions), share_not_found, host_unreachable, unreachable (mounted but not answering), timeout, protocol_unsupported (pin another SMB version), folder_conflict, local_files_present, secret_unreadable (the service has a different AUTH_SECRET), missing_capability, apparmor_confined, cifs_unavailable, guard_unavailable, not_root, unsupported_platform, invalid_config, helper_error, mount_failed.
Deployment requirements
The main docker-compose.yml grants SYS_ADMIN, DAC_READ_SEARCH and LINUX_IMMUTABLE to clapilot, clapilot-agent and clapilot-streamer. All three are required for mounting. Only the root helper holds them. After gosu switches to node, the app processes have an empty capability set, and no-new-privileges stays on. Docker's default seccomp profile allows mount once SYS_ADMIN is granted.
- Docker Desktop (macOS/Windows): works as is; the LinuxKit kernel ships CIFS.
- Linux hosts with AppArmor (Ubuntu): the default
docker-defaultprofile denies mounts even withSYS_ADMIN. The UI reportsapparmor_confined; addapparmor:unconfinedtosecurity_optof the three services. - Existing installations: capabilities are container configuration, not part of the image. After updating
docker-compose.yml, recreate the services once (docker compose up -d clapilot clapilot-agent clapilot-streamer). Watchtower image updates keep the old container configuration. - Opting out: remove the three
cap_addentries (the card then reportsmissing_capability), or setCLAPILOT_NETWORK_STORAGE_ENABLED=falseto skip the helper entirely. - Fleet instances, hosted cells and Cloud Run do not grant these capabilities. The card explains why mounting is unavailable there.
- Host-local development (
npm run dev:local-nativeon macOS) has no helper; the card shows that no service has reported.
Moving workspace folders onto a share
Once a share is connected in every service, Move folder… lists the folders that may be moved, largest first (sizes come from the storage report):
- every top-level workspace folder that is not platform-owned (
video-studio,livestream,podcasts,videos,social-media,exports, user folders, …) .clapilot/generated-imagesand.clapilot/generated-videos
Runtime state stays local: dot-folders such as .clapilotaicore and .agent-orchestrator, .clapilot/agent-media (its readers refuse symlinked owner folders), and the reserved folders above. mandanten stays local too, so the document core loop never depends on the NAS.
A move is a background job in the web container (src/lib/network-storage-relocation.ts, one at a time under a Postgres advisory lock, resumed after restarts):
- copying:
rsync -rltcopies the folder to<share folder>/clapilot-workspace/<path>while the app keeps using the local folder. The job first checks free space on the share and refuses sockets or device files. - switching: the local folder is renamed to
<name>.moving-to-network-storage-<id>and replaced by a relative symlink, e.g.video-studio -> NAS/clapilot-workspace/video-studio. The link resolves inside the workspace in every container, so every path check keeps working and stored absolute paths stay valid. - finalizing: after a 60 s grace period (
CLAPILOT_NETWORK_STORAGE_RELOCATION_GRACE_MS) for writers that still had files open, a deltarsyncruns, then a dry-run comparison by size and mtime (2 s tolerance). Only an empty comparison deletes the local copy. Files that keep changing are retried five times, 20 s apart. - done.
Nothing is deleted on failure. Before the switch the local folder is untouched. After it, the local copy stays next to the link, and Try again resumes at the verification step. A retry before the switch restarts the checks, and rsync only copies what is missing. Stop exports or streams that are writing into the folder before moving it. A process that keeps a file open past verification would be writing into the deleted local copy.
While a share is offline, moved folders fail loudly: the link target does not exist, and the guarded share folder cannot be written. A share with moved folders cannot be removed. Moving a folder back is a manual copy for now.
Interaction with other features
- Storage report and cleanup: the hourly report skips mount points (
du -xwould otherwise crawl the whole NAS). Moved folders are symlinks and drop out of local usage. Retention cleanup only touches its fixed scratch roots, andtmpis reserved as a share name. - Settings backup: export skips mounted shares, share folders and moved folders. Restore leaves them untouched and never copies backup content onto a share. Its rollback cannot delete a share either.
- File Explorer: lists moved folders as folders. Deleting or renaming a share folder (or a folder that contains a mount) returns
409with codenetwork_storage_mount. Files inside a share can be managed normally. - Agent: the share folder is a normal workspace folder for the file tools and
exec_command. The read-only admin toolinstance_storage_statusreportsnetwork_storage[]with each share's status, free space andmoved_folders. Configuring shares and moving folders are intentionally admin-UI-only: they handle NAS credentials and move data. - Apple clients: the storage settings page has no native equivalent; the admin area links to the web settings.
API
See API Reference (/api/admin/network-storage*). All routes are admin-only.
