Post

FileBrowser Quantum on k3s: NAS shares over NFS from LXC nodes

FileBrowser Quantum on k3s: NAS shares over NFS from LXC nodes

Purpose

FileBrowser Quantum is a web file manager. Here it sits in front of two personal datasets that desktops already reach over SMB, so the same files are also reachable from a browser.

The k3s nodes are privileged Proxmox LXC containers, and CIFS is not available there. The datasets are therefore exported over NFS as well, and the pod mounts them through static PersistentVolumes. The hard part is ownership: one uid and gid pair has to work for both SMB and the pod.

Deployment

The app is deployed by the ArgoCD app filebrowser (Synced, Healthy) from the k3s branch of its repo. It runs as one replica of gtstef/filebrowser:2.0.2-stable with the Recreate strategy. The repo README still says 2.0.1-stable, so it lags the cluster.

The nodes have features: mount=nfs set on their LXC config and nfs-common installed. Both exports are permanent: one is a TrueNAS NFS share, the other is exported by the Proxmox host from its own ZFS pool through a drop-in exports file. Both are root-squashed and limited to the three k3s node addresses.

flowchart LR
    D["Desktops"] -->|"SMB"| N["NAS datasets"]
    B["Browser"] --> T["Traefik"]
    T --> P["FileBrowser pod<br/>uid 1000, gid 1002"]
    P --> V["Static PVs<br/>NFS 4.2"]
    V -->|"root-squashed, limited to the 3 nodes"| E1["TrueNAS share"]
    V -->|"root-squashed, limited to the 3 nodes"| E2["Proxmox host export"]
    P --> Z["local-zfs claim<br/>app database"]

Configuration

The pod runs as uid 1000 and gid 1002 (the SMB users group) with runAsNonRoot, all capabilities dropped and no privilege escalation:

1
2
3
4
5
6
securityContext:
  runAsUser: 1000
  runAsGroup: 1002
  runAsNonRoot: true
  fsGroup: 1000
  fsGroupChangePolicy: OnRootMismatch

The datasets are owned 0:1002 and are group-writable. FileBrowser creates files as 664 and directories as 775, the same as SMB, so a file written in the browser can be edited over SMB and the other way round. The top-level directory of the Proxmox-exported dataset had to be made group-writable (775) so the top level was writable too.

fsGroup only changes ownership on the local app-data volume, never on the NFS shares. The pod does not re-chown user data: both shares are still 0:1002 after a week of the pod running.

Other settings that cost a restart or two:

  • A non-root container can’t bind port 80, so the app listens on 8080. The Service is ClusterIP on that port.
  • The admin password comes from an environment variable out of the SealedSecret. A password set in the config file is reset on every start.
  • Version 2.0.1 rejected auth.methods.password.adminUsername as an unknown field and the pod crash-looped. The default admin user is admin.
  • Sources are configured private (no public share links), with password login and sign-up off. Sources are not enabled by default: an admin grants each user their source.

Storage

Each dataset has one static PersistentVolume, pre-bound to its claim with claimRef and volumeName. storageClassName: "" keeps the default local-zfs class from provisioning a volume. Each PV is 1Ti, RWX, with reclaim policy Retain, and carries the ArgoCD annotation Prune=false so a sync cannot delete it. Mount options are nfsvers=4.2, hard and noatime.

1
2
3
4
5
6
7
spec:
  capacity:
    storage: 1Ti
  accessModes: [ReadWriteMany]
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""
  mountOptions: [nfsvers=4.2, hard, noatime]

This is the opposite of the old dynamic approach in the NFS provisioner post, which has since been removed. A provisioner creates new volumes on demand. Here the data already exists, so the volumes are written by hand and bound to it.

An NFS source on a PV is immutable. To repoint a PV, delete the PV and its claim and let ArgoCD recreate them. The data stays untouched because of Retain.

App state is kept apart from user data: a 10Gi local-zfs RWO claim holds the sqlite database and the temp files (previews, archives, index).

Access

The web UI is a Traefik IngressRoute using the wildcard TLS cert (see the wildcard TLS store post). Login is by password.

Secrets are SealedSecrets. Rotating the JWT secret logs everyone out, and rotating the TOTP secret invalidates enrolled 2FA.

Gatus checks the app with a ping of the /health URL and a DNS check. Logs reach Graylog through the k3s Vector DaemonSet.

Notes

  • For another way to expose files, see the SFTPGo post.
  • The repo README lags the cluster on the image version and still calls the Proxmox-host export temporary. It is permanent.
This post is licensed under CC BY 4.0 by the author.