CephFS is a POSIX-compliant distributed file system built on Ceph, deployable on Kubernetes via the Rook operator with dynamic CSI provisioning and optional NFS-Ganesha export for external clients, a self-managed, Kubernetes-native alternative to GCP Filestore. This guide deploys Rook + Ceph, creates a CephFS filesystem, provisions PVCs, exports over NFS, configures snapshots/backups, sets up CephX auth and tenant isolation, adds monitoring, and covers migrating off Filestore.
Concept Mapping
| GCP Filestore | CephFS Equivalent | Description |
|---|---|---|
| Filestore Instance | CephFilesystem | Distributed shared filesystem via Rook/Ceph |
| Filestore NFS Export | CephNFS + NFS-Ganesha | Exposes CephFS over standard NFS |
| Filestore NFSv3/v4.1 | NFS-Ganesha NFSv4 | NFS client compatibility |
| Filestore Capacity Tiers | Ceph pools + StorageClasses | Performance/placement via pool settings |
| Filestore Snapshots | VolumeSnapshot | CSI point-in-time snapshots |
| Filestore Backups | Snapshot export to object storage | Backup via snapshots + external storage |
| Filestore Multi-share | CephFS subvolumes | Isolated shared volumes via CSI |
| Filestore Performance Tiers | OSD device classes | SSD/HDD OSDs with pool tuning |
| Cloud Monitoring | Ceph Dashboard + Prometheus + Grafana | Metrics + visualization |
Components: Rook Operator (deploys/manages Ceph), MON/MGR (quorum + monitoring), OSDs (data on block storage), MDS (CephFS metadata), Ceph CSI driver (dynamic provisioning), NFS-Ganesha (NFS export), Prometheus + Grafana (monitoring).
Prerequisites: a Kubernetes cluster with 3+ worker nodes, each with a raw unformatted block storage volume attached;
kubectlconfigured; Helm 3; basic Kubernetes storage familiarity.
Install the Rook Ceph Operator
$ mkdir -p ~/rook-ceph
$ cd ~/rook-ceph
$ kubectl get nodes
Confirm all nodes are Ready.
$ helm repo add rook-release https://charts.rook.io/release
$ helm repo update
$ helm install rook-ceph rook-release/rook-ceph --namespace rook-ceph --create-namespace --version v1.19.7
This installs the operator + CRDs. Pinned to v1.19.7 — a tested release that auto-provisions the rook-ceph.cephfs.csi.ceph.com CSI driver. If you use a newer version, confirm that driver exists before provisioning PVCs.
$ kubectl get pods -n rook-ceph
$ kubectl get crds | grep ceph.rook.io
Deploy the Ceph Cluster
$ nano ceph-cluster.yaml
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
name: rook-ceph
namespace: rook-ceph
spec:
cephVersion:
image: quay.io/ceph/ceph:v19.2.3
dataDirHostPath: /var/lib/rook
mon:
count: 3
allowMultiplePerNode: false
mgr:
count: 1
allowMultiplePerNode: false
dashboard:
enabled: true
ssl: false
storage:
useAllNodes: true
useAllDevices: false
deviceFilter: "^vd[b-z]"
resources:
mgr:
requests:
cpu: "100m"
memory: "1Gi"
limits:
memory: "2Gi"
mon:
requests:
cpu: "250m"
memory: "512Mi"
limits:
memory: "1Gi"
healthCheck:
daemonHealth:
mon:
interval: 45s
osd:
interval: 60s
deviceFilter: "^vd[b-z]" tells Rook to use only additional virtio block devices (vdb, vdc, etc.) for OSDs, excluding the primary system disk (typically vda). Check your actual device names with lsblk on each node and adjust if needed.
$ kubectl apply -f ceph-cluster.yaml
$ kubectl get pods -n rook-ceph -w
Deployment can take several minutes while MON/MGR/OSD pods initialize — see the Rook troubleshooting docs if pods stay pending.
$ kubectl get pods -n rook-ceph | grep osd
$ kubectl get cephcluster -n rook-ceph
Should show PHASE: Ready, HEALTH: HEALTH_OK.
Create a CephFS Filesystem
$ nano ceph-filesystem.yaml
apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
name: cephfs
namespace: rook-ceph
spec:
metadataPool:
replicated:
size: 3
dataPools:
- replicated:
size: 3
preserveFilesystemOnDelete: true
metadataServer:
activeCount: 1
activeStandby: true
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
memory: "512Mi"
3x replication on both metadata and data pools, 1 active + 1 standby MDS for failover, filesystem data survives resource deletion.
$ kubectl apply -f ceph-filesystem.yaml
$ kubectl get pods -n rook-ceph -w
$ kubectl get pods -n rook-ceph | grep mds
$ kubectl get cephfilesystem -n rook-ceph
Should show PHASE: Ready.
Provision CephFS Volumes
$ nano cephfs-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: cephfs-storage
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
clusterID: rook-ceph
fsName: cephfs
pool: cephfs-data0
csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: true
$ kubectl apply -f cephfs-storageclass.yaml
$ kubectl get storageclass
You'll see cephfs-storage alongside whatever block-storage StorageClasses your cluster ships with by default.
Create a PVC and test pod:
$ nano cephfs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cephfs-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: cephfs-storage
resources:
requests:
storage: 5Gi
$ kubectl apply -f cephfs-pvc.yaml
$ kubectl get pvc
$ nano cephfs-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: cephfs-test-pod
spec:
containers:
- name: cephfs-test-container
image: busybox
command: ["/bin/sh", "-c"]
args:
- while true; do sleep 30; done
volumeMounts:
- name: cephfs-storage
mountPath: /mnt/cephfs
volumes:
- name: cephfs-storage
persistentVolumeClaim:
claimName: cephfs-pvc
$ kubectl apply -f cephfs-test-pod.yaml
$ kubectl get pod cephfs-test-pod
$ kubectl exec -it cephfs-test-pod -- sh
Inside the container:
$ echo "CephFS volume test successful" > /mnt/cephfs/test.txt
$ kubectl exec -it cephfs-test-pod -- cat /mnt/cephfs/test.txt
Export CephFS via NFS
$ nano ceph-nfs.yaml
apiVersion: ceph.rook.io/v1
kind: CephNFS
metadata:
name: ceph-nfs
namespace: rook-ceph
spec:
rados:
pool: .nfs
server:
active: 1
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
memory: "512Mi"
$ kubectl apply -f ceph-nfs.yaml
$ kubectl get pods -n rook-ceph | grep nfs
Deploy the toolbox to manage exports:
$ kubectl apply -f https://raw.githubusercontent.com/rook/rook/master/deploy/examples/toolbox.yaml
$ kubectl get pods -n rook-ceph | grep tools
Create the export:
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph nfs export create cephfs ceph-nfs /cephfs cephfs --path=/
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph nfs export ls ceph-nfs
$ kubectl get svc -n rook-ceph | grep nfs
Mount from a Linux Client
The NFS service is ClusterIP by default — reachable from cluster nodes/pods only. Expose it via LoadBalancer/NodePort for external access.
$ sudo mkdir -p /mnt/cephfs-nfs
Install NFS client tools (Ubuntu/Debian: sudo apt install nfs-common -y; Rocky/AlmaLinux: sudo dnf install nfs-utils -y), then:
$ sudo mount -t nfs NFS-SERVICE-IP:/cephfs /mnt/cephfs-nfs
$ echo "NFS export test successful" | sudo tee /mnt/cephfs-nfs/test.txt
$ cat /mnt/cephfs-nfs/test.txt
Storage Pools and Performance
Beyond the CephFS-backing pools, create dedicated block pools for other workloads:
$ nano ceph-blockpool.yaml
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
name: app-block-pool
namespace: rook-ceph
spec:
failureDomain: host
replicated:
size: 3
parameters:
compression_mode: none
failureDomain: host spreads replicas across nodes; compression is off here for predictable performance.
$ kubectl apply -f ceph-blockpool.yaml
$ kubectl get cephblockpool -n rook-ceph
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph status
Snapshots and Backups
Install CSI Snapshot CRDs
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-8.2/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-8.2/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-8.2/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-8.2/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/release-8.2/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
$ kubectl get pods -n kube-system | grep snapshot
Create a VolumeSnapshotClass
$ nano cephfs-snapshotclass.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: cephfs-snapshotclass
driver: rook-ceph.cephfs.csi.ceph.com
deletionPolicy: Delete
parameters:
clusterID: rook-ceph
csi.storage.k8s.io/snapshotter-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/snapshotter-secret-namespace: rook-ceph
$ kubectl apply -f cephfs-snapshotclass.yaml
$ kubectl get volumesnapshotclass
Snapshot and Restore
$ nano cephfs-snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: cephfs-snapshot
spec:
volumeSnapshotClassName: cephfs-snapshotclass
source:
persistentVolumeClaimName: cephfs-pvc
$ kubectl apply -f cephfs-snapshot.yaml
$ kubectl get volumesnapshot
Restore to a new PVC (must be ≥ source size):
$ nano cephfs-restore-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: cephfs-restore-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: cephfs-storage
resources:
requests:
storage: 5Gi
dataSource:
name: cephfs-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
$ kubectl apply -f cephfs-restore-pvc.yaml
$ kubectl get pvc
Scheduled Snapshots via CronJob
Kubernetes doesn't schedule snapshots natively — use a CronJob with RBAC to manage VolumeSnapshot resources.
$ nano cephfs-snapshot-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: cephfs-snapshot-sa
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cephfs-snapshot-role
namespace: default
rules:
- apiGroups: ["snapshot.storage.k8s.io"]
resources: ["volumesnapshots"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cephfs-snapshot-rolebinding
namespace: default
subjects:
- kind: ServiceAccount
name: cephfs-snapshot-sa
namespace: default
roleRef:
kind: Role
name: cephfs-snapshot-role
apiGroup: rbac.authorization.k8s.io
$ kubectl apply -f cephfs-snapshot-rbac.yaml
$ nano cephfs-snapshot-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: cephfs-snapshot-cronjob
spec:
schedule: "0 */6 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: cephfs-snapshot-sa
restartPolicy: OnFailure
containers:
- name: snapshot-creator
image: alpine/k8s:1.36.2
command:
- /bin/sh
- -c
- |
SNAPSHOT_NAME="cephfs-snapshot-$(date +%s)"
cat <<EOF | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: ${SNAPSHOT_NAME}
spec:
volumeSnapshotClassName: cephfs-snapshotclass
source:
persistentVolumeClaimName: cephfs-pvc
EOF
Every 6 hours, timestamped snapshot names.
$ kubectl apply -f cephfs-snapshot-cronjob.yaml
$ kubectl get cronjob
Authentication and Access Control
POSIX Permissions
$ kubectl exec -it cephfs-test-pod -- sh
$ mkdir -p /mnt/cephfs/tenant-a /mnt/cephfs/tenant-b
$ chown 1001:1001 /mnt/cephfs/tenant-a
$ chmod 700 /mnt/cephfs/tenant-a
$ ls -ld /mnt/cephfs/tenant-a
$ exit
Restricted CephFS Client Users
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- bash
$ ceph fs authorize cephfs client.tenant-a /tenant-a rw
$ ceph auth get-key client.tenant-a
$ ceph auth get client.tenant-a
$ exit
The path in ceph fs authorize resolves from the CephFS filesystem root, not the pod's PVC mount point. If the CSI driver provisions a subvolume rather than the root, get its actual path with ceph fs subvolume getpath first.
Verify restriction by mounting directly with the kernel client:
$ sudo mount -t ceph MON-IP:6789:/tenant-a /mnt/tenant-a-test -o name=tenant-a,secret=CLIENT-KEY
$ ls /mnt/tenant-a-test
A permission error outside the authorized path confirms CephX is enforcing correctly.
Monitoring
Ceph Dashboard
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph status
$ kubectl get pods -n rook-ceph | grep mgr
$ kubectl get svc -n rook-ceph | grep dashboard
$ kubectl port-forward -n rook-ceph svc/rook-ceph-mgr-dashboard 7000:7000
Open http://127.0.0.1:7000.
$ kubectl -n rook-ceph get secret rook-ceph-dashboard-password -o jsonpath="{['data']['password']}" | base64 --decode && echo
Log in as admin with that password.
Prometheus + Grafana
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph mgr module enable prometheus
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph mgr module ls | grep prometheus
$ kubectl get svc -n rook-ceph | grep mgr
$ helm repo add grafana-community https://grafana-community.github.io/helm-charts
$ helm repo update
$ helm install grafana grafana-community/grafana --namespace monitoring --create-namespace
$ kubectl get pods -n monitoring
$ kubectl get secret --namespace monitoring grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
$ kubectl port-forward -n monitoring svc/grafana 3000:80
Open http://127.0.0.1:3000, log in as admin with the retrieved password.
Verify the Deployment
$ kubectl get pvc
$ kubectl get pod cephfs-test-pod
$ kubectl exec -it cephfs-test-pod -- sh -c 'echo "CephFS deployment verification successful" > /mnt/cephfs/verify.txt'
$ kubectl exec -it cephfs-test-pod -- cat /mnt/cephfs/verify.txt
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph nfs export ls ceph-nfs
$ kubectl get svc -n rook-ceph | grep nfs
$ kubectl get volumesnapshot
$ kubectl get pvc cephfs-restore-pvc
$ kubectl get svc -n rook-ceph | grep dashboard
$ kubectl get pods -n monitoring
$ kubectl exec -it -n rook-ceph deploy/rook-ceph-tools -- ceph status
Migrating from GCP Filestore
Data: mount both the Filestore NFS export and the CephFS NFS export on the same client, then rsync -avh between them (preserves ownership/permissions/symlinks/timestamps). Parallelize across clients/subdirectories for large datasets, or copy directly into a pod with the CephFS PVC mounted.
Applications: swap each workload's PVC storageClassName from your Filestore class (e.g. filestore-rwx) to cephfs-storage, recreate the PVC, restart the workload — CephFS gives the same POSIX + ReadWriteMany semantics, so no code changes needed. Non-Kubernetes apps just repoint their NFS mount at the CephFS export.
Snapshots/backups: VolumeSnapshot resources replace Filestore snapshots (as configured above). Recreate any scheduled-backup workflow with a CronJob or external tool, storing long-term copies in an S3-compatible object storage backend.
Sizing: CephFS defaults to 3x replication — budget raw storage at ~3x usable capacity. Add capacity by attaching more block storage to worker nodes and letting Rook provision new OSDs via the device filter; Ceph rebalances automatically. Use separate pools/device classes to match Filestore's capacity tiers.
Watch for:
- NFS version: some Filestore setups use NFSv3; NFS-Ganesha defaults to NFSv4 — check client compatibility
- Performance tuning: Filestore tiers are automatic; CephFS performance depends on OSD type, replication, network, and pool config — you own the tuning
- Multi-share: maps to CephFS subdirectories or separate subvolumes for tenant isolation
- Auth: GCP IAM → CephX + POSIX permissions/ACLs
- Monitoring: Cloud Monitoring → Ceph Dashboard + Prometheus + Grafana
- Capacity planning: factor in the 3x replication overhead before migrating production data
Next Steps
CephFS is running with dynamic provisioning, NFS export, snapshots, CephX-enforced tenant isolation, and full monitoring. From here:
- Add more CephBlockPools with different device classes (SSD vs HDD) for tiered performance
- Automate scheduled snapshot pruning alongside the creation CronJob
- Explore Ceph's multi-site replication for cross-region DR
For the full guide, visit the original article on Vultr Docs.
Top comments (0)