Kubernetes storage is a problem with no good solution.
Your options: store data in containers (lost on crash), use cloud storage (vendor lock-in, expensive), or manage your own NFS server (who wants that?).
We built the fourth option: A FreeBSD VM running ZFS + NFS in the same VPC as our K8s cluster.
It’s not flashy. It’s not a managed service. But it works.
The K8s Storage Problem
When you run stateful applications in Kubernetes, you hit a wall:
Option 1: Ephemeral Storage (emptyDir)
- Data lost when pod crashes
- No recovery
- Use case: scratch space, caching
Option 2: Cloud Storage (EBS, Persistent Disks)
- Works well at first
- Costs $500+ per month at scale
- Vendor lock-in (can’t move easily)
- No snapshots for point-in-time recovery
- You’re trusting someone else’s backups
Option 3: Self-Hosted Storage (NFS)
- Requires setup, maintenance, monitoring
- No data protection (corruption destroys everything)
- Backups are separate problem
- Recovery is manual and slow
Real scenario: We have multiple Kubernetes clusters (stage + prod) that need:
- Persistent storage for databases, documents, caches
- Point-in-time recovery (crash/corruption/accidental delete)
- Offsite backups for compliance
- Monthly budget < $500
Cloud storage didn’t fit. Manual NFS didn’t fit. So we built something better.
Why ZFS + NFS
We chose this combination because each layer does one thing well:
NFS - Standard Kubernetes protocol
Works across cluster boundaries. No fancy CSI drivers required. Kubernetes understands it natively. Can mount same storage from multiple clusters simultaneously.
ZFS - Data protection layer
Snapshots are point-in-time views (zero-copy, instant creation). Incremental replication for offsite backups. Copy-on-write means snapshots don’t consume significant space. Transparent compression and deduplication. Automatic corruption detection.
FreeBSD - Stable storage platform
Best ZFS support (it was invented for Solaris, FreeBSD is the standard). Minimal overhead. NFS performance is excellent. Runs on standard cloud VMs without bloat.
Together: automated data protection without the cost.
Real Architecture
Everything runs in the same VPC. Here’s the three-layer setup:
Layer 1: Kubernetes Clusters (Stage + Prod) - Cloud VMs
Apps request storage via PersistentVolumeClaim. Kubernetes mounts standard NFS storage class. Pods connect to the FreeBSD storage VM via internal VPC network.
Layer 2: Storage Node (Single FreeBSD VM in same VPC)
- VM specs: 2 vCPU, 4GB RAM (minimal, but sufficient for NFS)
- Export:
/export/k8s-datavia NFS - Filesystem:
zroot/k8s-data(ZFS with snapshots) - Snapshots: Hourly (automatically created, pruned after 3 days)
- Protection: Point-in-time recovery ready
- Access: Low-latency internal NFS connection (same VPC)
Layer 3: Offsite Replication (External Storage)
- Target: rsync.net or similar external offsite storage
- Replication: Nightly incremental sends over WireGuard/SSH
- Retention: Full dataset + 30 days of snapshots
- Recovery: Clone offsite snapshot to new volume, mount as new PV
How Snapshots Protect K8s Data
A snapshot is a point-in-time view of the filesystem. It’s not a copy.
Key insight: Creating a snapshot takes microseconds and costs almost nothing in storage space. You can have 365+ snapshots per dataset and use nearly the same space as the original.
Real scenarios:
Scenario 1: Pod Crashes, Data Lost
Pod crashes → container restarts → mounts same NFS
Data is intact (stored on NFS, not in container)
Application sees its data, continues normally
Recovery time: Zero (automatic)
Scenario 2: Application Writes Bad Data
App bug corrupts database files
You notice 30 minutes later
Run: zfs rollback zroot/k8s-data@2026-04-15-09-00
Database returns to clean state from 30 min ago
Recovery time: <5 minutes
Scenario 3: Accidental Delete
Human error deletes important files
Too late for rm -r recovery
Clone offsite snapshot to recovery volume:
zfs clone zroot/remote/k8s-data@2026-04-14 \
zroot/recovery
Mount recovery dataset as new PV
App restores files from 24 hours ago
Recovery time: 5-10 minutes
Scenario 4: Entire Cluster Loss
Cluster hardware fails
All pods gone, all PVs gone
Have offsite ZFS snapshot from this morning
Clone offsite snapshot:
zfs clone remote/k8s-data@2026-04-15 \
zroot/recovery
Create new PV pointing to recovery dataset
Redeploy pods pointing to new PV
Data restored, minimal loss
Recovery time: 15-30 minutes (depends on cluster redeploy speed)
Implementation
Storage Class Configuration
Standard Kubernetes NFS storage class:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: storage-node.internal # Your FreeBSD storage nodes
share: /export/k8s-data
nfsvers: 3
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
Apps request storage normally:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
Automatic Snapshots with zfs-autobackup
We use zfs-autobackup, a Python tool that handles the entire workflow automatically.
Step 1: Tag datasets
zfs set autobackup:k8s-offsite=true zroot/k8s-data
Step 2: Run zfs-autobackup daily
The tool:
- Creates snapshots automatically (tagged with the backup label)
- Sends full dataset on first run
- Sends only incremental changes on subsequent runs
- Destroys incompatible snapshots automatically
- Maintains retention policy (10 recent + daily + weekly + monthly)
zfs-autobackup supports two modes:
Push mode (run on source, push to destination):
zfs-autobackup \
--clear-mountpoint \
--destroy-incompatible \
--ssh-source storage-node.internal \
k8s-offsite \
zroot/remote/k8s-data
Pull mode (run on destination, pull from source):
zfs-autobackup \
--clear-mountpoint \
--destroy-incompatible \
storage-node.internal \
zroot/remote/k8s-data
Use push mode if the storage node initiates backups. Use pull mode if the offsite target initiates backups (more common in air-gapped environments).
Run this daily via cron (3:15 AM, for example):
15 3 * * * keep /usr/local/bin/zfs-autobackup-run.sh >> /var/log/zfs-autobackup.log 2>&1
Real performance:
- Initial full backup (100GB): ~47 hours
- Daily incremental: <10 minutes
- Bandwidth: Efficient (only changed blocks sent)
- Retention: Automatic (10 snapshots kept + time-based rotation)
What zfs-autobackup Does
- Automation: No manual snapshot commands needed
- Idempotent: Safe to run multiple times
- Incremental: Only changed blocks transferred after first sync
- Retention: Automatic snapshot pruning (keeps N recent + age-based)
- Resume-safe: Tracks which snapshots were sent, safe to interrupt/resume
Maintenance & High Availability
Keeping FreeBSD Updated
FreeBSD is trivial to maintain. Security updates are seamless:
freebsd-update fetch install
No restarts required for most updates. Reboot only when kernel patches land. A single storage node is simpler to manage than a managed service—no vendor updates to wait for, no API changes to track.
High Availability (Optional)
For production HA, replicate to a second storage node in the same VPC:
# On second storage node, pull from primary via zfs-autobackup
zfs-autobackup \
--destroy-incompatible \
primary-storage.internal \
zroot/k8s-data-replica
Then use CARP (Common Address Redundancy Protocol) on both FreeBSD storage nodes to provide a virtual IP (VIP) for NFS access. K8s storage class points to the VIP instead of a single node. Sub-second failover is automatic—if the primary storage node fails, CARP promotes the replica and NFS requests resume on the secondary without pod restarts.
This gives you true HA storage with automatic failover, all managed with open-source tools.
What We Actually Store
In production, the storage nodes host a mix of real workloads:
Databases
- MySQL instances backing web applications and CMS platforms
- 200MB–2GB per application (varies by size and retention)
- Point-in-time recovery critical: corruption or accidental deletes are catastrophic
User-Generated Content
- CMS asset libraries (images, PDFs, documents uploaded by users)
- Can grow to 33GB+ for mature content platforms
- Needs recovery if media library corrupts or users accidentally delete content
Build & CI/CD Artifacts
- Jenkins: compiled binaries, build logs, caches, workspace data
- 1–5GB depending on build frequency and artifact retention
- Must survive pod restarts without data loss
Application State
- Cache, temporary files, application configuration
- PHP-FPM files, Java build caches, config backups
- Often the difference between pod restart and data corruption
Package Repositories
- Composer/NPM mirrors cached for faster deployments
- Can be 10GB+ for large teams
- Recoverable but expensive to rebuild
This isn’t ephemeral data. It’s the stuff that breaks your application if it disappears. That’s why snapshots matter.
Real Numbers
Our setup:
- NFS Node: 2 vCPU, 4GB RAM FreeBSD VM
- Data: 1TB active K8s storage (with 998GB ZFS pool in staging, 97.5GB in production)
- Snapshot storage: 50GB additional (snapshots use copy-on-write)
- Hourly snapshots: 72 retained (3 days)
- Daily replication: <10 minutes (incremental only)
- Recovery time: <5 min (clone snapshot)
Cost:
- FreeBSD VM (2 vCPU, 4GB RAM, 2TB storage): ~$30-40/month
- Offsite storage: ~$50/month (rsync.net)
- Kubernetes CSI driver: Free (open source)
- Total: ~$80-90/month
vs. AWS EBS + backup solution:
- EBS: $300+/month for 1TB
- Backup service: $200+/month
- Total: $500+/month
Savings: ~$410-420/month vs managed solutions
Why This Matters
You’re not betting on a vendor’s backup strategy. You control recovery.
You’re not paying per-gigabyte for enterprise backup software. You’re using filesystem-level snapshots.
You’re not hoping the cloud provider backs up your data correctly. You’re replicating to your own offsite target.
Trade-offs
This isn’t perfect for everyone:
Good for:
- Self-managed Kubernetes (cloud VMs, on-premises, or hybrid)
- Private clusters where you control the infrastructure
- Applications that tolerate occasional brief NFS hiccups
- Teams comfortable managing their own storage layer
Not ideal for:
- Managed Kubernetes services (GKE, EKS) where you can’t provision your own VMs
- Multi-region requirements (NFS latency across regions)
- Applications requiring sub-millisecond latency (use local storage)
- Completely serverless workloads (you need to manage the storage VM)
The Lesson
Don’t assume managed services or enterprise backup tools are the only way. A single FreeBSD VM with ZFS + NFS gives you better data protection and costs 3-4x less than managed cloud storage.
The key: understand what you’re protecting (point-in-time data), choose tools that do that well (ZFS snapshots), and automate everything. Run it in the cloud, on-premises, or hybrid—the architecture works anywhere as long as your K8s cluster and storage node can talk on low-latency networks.
That’s how you survive.