Proxmox LXC¶
proxmox/deploy-proxmox.sh deploys the whole Filearr stack into a
Docker-enabled LXC on Proxmox VE, mounting your network storage inside
the container so the deployment does not depend on host-side mounts. It is a
wizard on first run and an idempotent redeploy on later runs.
Run it on the Proxmox host shell (as root), from inside the project folder:
git clone https://github.com/filearr/filearr.git && cd filearr # once
bash proxmox/deploy-proxmox.sh # first run -> wizard, then deploy
bash proxmox/deploy-proxmox.sh # later runs -> redeploy with saved defaults
bash proxmox/deploy-proxmox.sh --reconfigure # re-run the wizard
bash proxmox/deploy-proxmox.sh --storages # re-run only the storage definitions
bash proxmox/deploy-proxmox.sh --status # CT + mounts + stack status
bash proxmox/deploy-proxmox.sh --destroy # stop & delete the container
What the wizard asks¶
The wizard saves your answers so redeploys never re-ask. It prompts once for:
- Container basics — starting VMID (first free
>=your number is used), hostname, network bridge, DHCP or static IP, rootfs storage, disk size, CPU cores, memory, web UI port (default 8484), HTTPS port (default 8443). - Public base URL — the absolute prefix for export/report download links
(e.g.
https://filearr.example.com:8443); blank means site-relative links. - TLS mode —
internal(self-signed LAN CA in the container; no public DNS) oracme-dns(Let's Encrypt wildcard*.<domain>via Cloudflare DNS-01; the container terminates public TLS itself and also fronts the agent mTLS plane and the step-ca SNI passthrough). - Distributed agents — enable the step-ca certificate authority and the enrollment endpoints. Safe to enable now and enroll machines later.
- Thumbnail volume — an optional dedicated volume for the thumbnail cache
(
THUMBS_STORAGE+ size in GB): pick a storage to keep a large cache off the CT rootfs, or leave blank to share the rootfs. The cache is disposable either way (bounded by the thumbnail GC). - Optional app settings — the optional feature knobs (semantic search, content sniffing, update check, log recorder, thumbnail budget, and — only when agents are enabled — the agent auth mode). See below.
- Storage definitions — one or more network shares (see below).
The "Optional app settings" step¶
Optional features ship off. The wizard asks about them and writes the answers
into deploy.conf, where you can see and edit them on the host:
── Optional app settings ──
Configure optional app settings (semantic search, thumbnail budget, ...)? [y/N]
- Answer
N(the default) and the deploy still records the current effective values asFILEARR_*=VALUElines indeploy.conf— the knobs become explicit without any interrogation. Values you already answered are never rewritten. - Answer
yand it walks the knobs one at a time, each with a one-line explanation. The default offered for each is the current effective value: an existing line indeploy.conf, else a line inenv.overrides, else the shipped default. Press Enter to keep it. Booleans accept onlytrue/false.
| Prompt | Default | What it does |
|---|---|---|
FILEARR_SEMANTIC_ENABLED |
false |
Semantic/hybrid search. The worker loads a local embedding model (~500 MB RSS) and backfilling 1M+ items takes hours. |
HF_TOKEN |
(blank) | Only asked when semantic search is true. Optional Hugging Face token for the one-off model download (higher rate limit). A secret: written to the CT .env only, never deploy.conf, never echoed. Blank keeps what the CT has (or downloads anonymously); none removes it. |
FILEARR_CONTENT_SNIFF_ENABLED |
false |
libmagic reclassification of extensionless files. |
FILEARR_UPDATE_CHECK_AUTO |
false |
Auto-refresh the GitHub update check (outbound network call). |
FILEARR_LOG_DB_ENABLED |
true |
The database log recorder behind the console Logs panel. |
FILEARR_THUMBNAIL_BUDGET_GB |
5 |
Advisory thumbnail-cache budget; 0 disables the advisory. |
FILEARR_AGENT_AUTH_MODE |
fingerprint |
Only asked when distributed agents are enabled. fingerprint = interim bearer; both = accept both during migration; mtls-header = mTLS only — flip after every agent shows the mTLS badge. |
Every answer (including an accepted default) is written to deploy.conf, so
after any wizard or --reconfigure run the knobs are literally set there —
and applied to the container's .env on that same deploy. An ordinary
redeploy with saved answers never re-asks; --reconfigure always re-offers the
section. If env.overrides pins a different value for a key, the deploy says
so — that file still wins (see the precedence rules below).
The prompt-once model, and where secrets go¶
Answers persist to ~/.config/filearr/deploy.conf; storage definitions
(including credentials) persist to ~/.config/filearr/storages.env (mode 0600).
Both are re-applied on every redeploy, so the container is fully disposable —
destroy and rebuild it and your configuration returns.
The three host-side files, and what each is for:
File (~/.config/filearr/) |
Holds | Applied |
|---|---|---|
deploy.conf |
Wizard answers (VMID, bridge, ports, TLS mode …) plus, optionally, any FILEARR_*=VALUE app-setting lines — so one file can describe the whole deployment. |
Every deploy. FILEARR_* lines are upserted into the container's .env. |
env.overrides |
FILEARR_*=VALUE app settings only (the dedicated file). |
Every deploy, after the deploy.conf lines — so it wins on a duplicate key. |
storages.env |
Storage definitions incl. share credentials (mode 0600). | Every deploy (mount units rebuilt). |
Secrets never go in deploy.conf
deploy.conf holds only non-secret settings. The Cloudflare API token, the
auto-generated proxy shared secret, the auto-generated FILEARR_SECRET_KEY,
and the extracted CA provisioner JWK are secrets and live in the
container's .env only — never in deploy.conf, never echoed to the
terminal. On a redeploy, a blank token answer means "keep the container's
existing one".
The deploy enforces this: FILEARR_SECRET_KEY,
FILEARR_PROXY_SHARED_SECRET, FILEARR_CA_PROVISIONER_JWK,
FILEARR_MEILI_MASTER_KEY, FILEARR_DATABASE_URL and
FILEARR_PROCRASTINATE_DSN found in deploy.conf are skipped with a ⚠
warning, not applied. Those six are container-managed.
Storage: rclone/NFS mounts inside the container¶
Each storage mounts read-only at /data/media/<name> inside the container,
and Docker Compose binds /data/media into the app and worker. Mounts are
installed as systemd units ordered Before=docker.service, so containers always
see them.
| Type | How it is mounted | Container privilege |
|---|---|---|
| SMB/CIFS, FTP, SFTP, WebDAV | rclone FUSE mount (userspace) | Unprivileged CT with the fuse=1 feature |
| NFS | kernel mount | Privileged CT (the script switches automatically and warns) |
| local | host path bind-mounted via pct |
the only type that touches the host |
NFS forces a privileged container
Kernel NFS mounts require a privileged LXC. If you define an NFS storage,
the script switches the container to privileged and warns you. SMB/FTP/SFTP/
WebDAV all work in an unprivileged container via rclone FUSE — prefer those
where you can. FUSE mounts also need fuse=1 on the container (the script
sets it).
For SMB, credentials are collected once per host and reused across every share on that host. AD domain goes in the separate domain field; use a bare username (no domain prefix).
Adding, removing, and redefining storages¶
bash proxmox/deploy-proxmox.sh --storages re-opens the storage wizard against
the saved definitions: add more, remove some by name, redefine from
scratch, or keep as-is. Applying the config reconciles the CT — mount units,
NFS fstab entries, and mountpoints for storages that are no longer defined are
stopped and removed (a local bind is a pct-level mapping and fully
disappears only when the CT is recreated).
Before a removal or a from-scratch redefine, the wizard checks the running stack for libraries rooted on the affected storage and asks for explicit confirmation if any exist — their files become unreachable, so the next scan tombstones every item (recoverable from the recycle bin until retention purges; nothing is hard-deleted). Delete or repoint such libraries in Admin first if that isn't what you want. When the stack isn't reachable the wizard says it could not check instead of pretending nothing is affected.
The credential-free share map¶
Because the deploy alone knows the real share URL behind each mount, it writes a
credential-free map to /config/share-map.json in the container
(regenerated on every deploy). Filearr reads it read-only and auto-populates each
library's user-facing share location from the mount that covers its root — so
the "open in Explorer / Finder" hint survives remounts and redeploys with no hand
maintenance. A manual share location always wins. A missing or malformed file
simply disables the feature; the app never fails to start.
TLS and reverse-proxy topology¶
The container runs a Caddy TLS front. Two modes:
internal— Caddy mints a self-signed LAN CA and serves HTTPS on your chosen port (default 8443). No DNS, no ACME, no egress needed. Trust the root CA on your clients once to remove the browser warning (see Operations → TLS).acme-dns— a Let's Encrypt wildcard*.<domain>via Cloudflare DNS-01. The container terminates public TLS itself (no external nginx), and its Caddy also carries a layer-4 listener that raw-TCP-proxiesca.<domain>straight to step-ca (SNI passthrough — the CA must never be L7-terminated, or agent cert renewal silently breaks).
An acme-dns example pattern (all three hostnames share port 443 via SNI):
filearr.example.com A/AAAA -> <container-ip> # web UI / API
agents.example.com A/AAAA -> <container-ip> # agent mTLS plane
ca.example.com A/AAAA -> <container-ip> # step-ca SNI passthrough
The Cloudflare API token needs both Zone:Read and DNS:Edit on the zone.
DNS-01 needs no inbound port 80, so issuance works behind NAT. On a split-horizon
LAN (a local resolver answering for your domain), add an override for each
hostname to the container IP — a missing ca. override breaks agent renewal from
inside the LAN.
Optional: iGPU passthrough for video thumbnails¶
iGPU passthrough is deliberately not wired automatically (it is host-specific
and the thumbnail pipeline degrades cleanly to software). To enable QSV video
poster frames after the first deploy, add the DRI cgroup allow and /dev/dri
mount entry to the container config and reboot it — the worker compose service
already carries the device mapping, added conditionally only when /dev/dri
exists. See the comments in deploy-proxmox.sh.
Redeploy behavior¶
A redeploy is safe and self-quiescing. It:
- Gracefully stops running scans (progress kept, no tombstoning) and remembers their libraries.
- Updates the container OS packages and Docker engine.
- Re-applies the storage mounts and regenerates the share map.
- Pushes the current source (with a dirty-tree guard — it refuses to silently
deploy uncommitted work) and does a clean extract, preserving
.env,config/, and the compose override. - Builds and starts the stack, runs the idempotent DB bootstrap, and verifies the running image was built from the source just pushed (a build-stamp check) plus a functional smoke test.
- Re-triggers the scans it stopped in step 1.
See Upgrades & migrations for what happens to the schema on redeploy.
Deploy fails with STAMP MISMATCH¶
Every deploy fingerprints the source it pushes (a content hash + push
timestamp), bakes that stamp into the image, and — after the stack starts —
reads the stamp back out of the running app container. STAMP MISMATCH
means the running container was built from an earlier push: the source
just pushed is on the CT's disk, but the image build for it didn't take, so
Docker kept running the previous build.
What state you're in:
- Nothing partial — the stack still runs the previous build in its entirety, and no new database migrations were applied.
- Scans the deploy quiesced were not auto-resumed (the script aborts before that step) — restart them from the Libraries page, or just rerun the deploy.
The two usual causes are a full CT disk (pct exec <vmid> -- df -h /)
and Docker Hub being unreachable (the build pulls base images); the
script aborts at the build step with the real error, so scroll up to it.
After fixing the cause, retry with FORCE_REBUILD=1 ./deploy-proxmox.sh
<same args> — that forces a cache-less rebuild, which also cures the rarer
corrupted-build-cache case.
After it's up¶
The wizard's summary prints your URLs (and, with agents on, the one-command
agent installer line). Then follow the shared
first-run guide: the one-time create-admin screen on
first visit, your first library, a scan, and a search. Back up Postgres
from the host on a schedule — scripts/backup.sh is pct exec-friendly and
wraps a pg_dump -Fc with retention; everything else in the CT is
disposable/rebuilt by redeploy.
Changing a configuration setting (.env)¶
Every FILEARR_* setting (see the configuration
reference) ends up in /opt/filearr/.env
inside the container, which the deploy preserves across redeploys. There are
two ways to manage settings — prefer the first:
Host-side (recommended): deploy.conf or the overrides file. Put
KEY=VALUE lines either straight into ~/.config/filearr/deploy.conf (keeps
the whole deployment in one file) or into
~/.config/filearr/env.overrides next to it; every deploy upserts them into
the CT's .env, last, so they always win over anything the deploy writes.
Settings managed this way survive redeploys and even a CT recreation — the
host file is the source of truth. Only FILEARR_* keys are applied (other
lines are ignored), and the six container-managed secret/plumbing keys listed
above are refused from deploy.conf with a ⚠ warning.
If the same key appears in both files, env.overrides wins — its lines are
merged after deploy.conf's. The deploy prints what it staged, e.g.
host env overrides staged (2 from deploy.conf + 1 from env.overrides).
# example: accept a larger thumbnail cache — 50 GiB advisory budget
mkdir -p ~/.config/filearr
echo 'FILEARR_THUMBNAIL_BUDGET_GB=50' >> ~/.config/filearr/env.overrides
# ...or, single-file style, the same line in deploy.conf:
echo 'FILEARR_THUMBNAIL_BUDGET_GB=50' >> ~/.config/filearr/deploy.conf
bash proxmox/deploy-proxmox.sh # applied on this and every future deploy
Every optional feature is already written into .env
The deploy writes each optional feature
knob —
FILEARR_SEMANTIC_ENABLED, FILEARR_CONTENT_SNIFF_ENABLED,
FILEARR_UPDATE_CHECK_AUTO, FILEARR_THUMBNAIL_BUDGET_GB,
FILEARR_LOG_DB_ENABLED, FILEARR_AGENTS_ENABLED,
FILEARR_AGENT_AUTH_MODE — into the CT's .env with its default value, so
cat /opt/filearr/.env shows what exists instead of leaving it implicit.
This happens only when the key is absent: your in-CT edits, the agents
wizard's FILEARR_AGENTS_ENABLED=true, and your host-side lines are never
overwritten.
To set them on the host instead — visible and editable in
deploy.conf, surviving a CT rebuild — use the wizard's Optional app
settings step (--reconfigure re-offers it).
In-CT (immediate, no redeploy needed): edit .env directly with the
duplicate-safe remove-then-append pattern, then recreate the stack (compose
only recreates services whose environment actually changed):
pct exec <vmid> -- bash -c "cd /opt/filearr \
&& grep -v '^FILEARR_THUMBNAIL_BUDGET_GB=' .env > .env.new \
&& echo 'FILEARR_THUMBNAIL_BUDGET_GB=50' >> .env.new \
&& mv .env.new .env \
&& docker compose up -d"
If you use both, mirror the value into the overrides file — otherwise the next deploy re-asserts whatever the host file says.
Verify what the running container actually sees:
The same procedure applies to every variable in the reference — only
FILEARR_SECRET_KEY and FILEARR_PROXY_SHARED_SECRET should never be
changed after first use (rotating them orphans encrypted alert-channel
secrets / breaks the agent mTLS proxy trust); neither belongs in the
overrides file.
Shell access — where's the container password?¶
Nowhere: the wizard deliberately never sets a root password inside the CT, so there is no stored credential to look up. Administration happens from the Proxmox host shell (node → Shell in the web UI, or SSH to the host), which can always enter an LXC as root without one:
pct enter <vmid> # root shell inside the container
pct exec <vmid> -- <cmd> # run one command without entering
The CT's >_ Console in the web UI shows a login: prompt, and with no
password set root cannot log in there. If you want console (or in-CT SSH)
logins to work, set a password once from the host:
Inside the container the stack lives at /opt/filearr — that directory holds
.env, docker-compose.yml, and config/, and it's where docker compose
commands run. The application's admin login is separate and unrelated: it
is whatever you created on the first-visit create-admin screen
(first-run guide).