K3s’s default StorageClass behavior is surprisingly opinionated, often leading to confusion for newcomers expecting a more hands-off approach.

Let’s see how it works with a simple deployment. Imagine we have a Kubernetes cluster running K3s and we want to create a PersistentVolumeClaim (PVC) without explicitly specifying a storageClassName.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

When you apply this, K3s will attempt to provision a PersistentVolume (PV) for it. The magic (or confusion) happens because K3s, by default, doesn’t automatically create a StorageClass named standard or default for you. This means if you apply the above YAML, your PVC will likely remain Pending because there’s no StorageClass available to satisfy its request.

The problem K3s solves here is providing a minimal, opinionated Kubernetes distribution that’s easy to set up but requires explicit configuration for dynamic volume provisioning. It doesn’t make assumptions about your underlying storage infrastructure.

Internally, when a PVC is created without a storageClassName, the Kubernetes scheduler looks for a StorageClass that has storageclass.kubernetes.io/is-default-class: "true" annotation. If multiple StorageClasses are marked as default, the scheduler will reject the PVC. If no StorageClass is marked as default, the PVC will remain unbound. K3s, out of the box, doesn’t pre-configure any StorageClass with this annotation.

The primary lever you control is the creation and annotation of StorageClass objects. You can define different types of storage (e.g., NFS, local path, cloud provider volumes) and then designate one as the default.

Here’s how you’d typically set up a default StorageClass for local path provisioner, which is common in K3s for development or single-node setups:

First, ensure you have the local-path-provisioner installed. K3s often includes this by default, but if not, you can install it via Helm or by applying its manifest.

Then, you create a StorageClass. Let’s call it local-storage:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
  annotations:
    storageclass.kubernetes.io/is-default-class: "true" # This is the key!
provisioner: rancher.io/local-path # This is the provisioner K3s uses for local paths
reclaimPolicy: Delete # Or Retain, depending on your needs
volumeBindingMode: WaitForFirstConsumer

Apply this manifest: kubectl apply -f storageclass.yaml.

Now, when you create that my-pvc again, K3s will see the local-storage StorageClass is marked as the default. It will use the rancher.io/local-path provisioner to create a PV on the K3s node’s local filesystem (typically /var/lib/rancher/k3s/storage/) and bind it to your PVC. The WaitForFirstConsumer volume binding mode ensures that the volume is only provisioned when a pod actually starts using the PVC, which is crucial for topology-aware provisioning.

If you have multiple storage options, you’d create separate StorageClasses for each and then only annotate the one you want to be the default. For example, you might have a fast-ssd StorageClass and a slow-hdd StorageClass. To make fast-ssd the default, you’d add the storageclass.kubernetes.io/is-default-class: "true" annotation to its definition and remove it from slow-hdd.

The most surprising thing about default StorageClasses is that the "default" status is an annotation on the StorageClass object itself, not a field within the StorageClass definition or a setting at the K3s cluster level. This means you can dynamically change which StorageClass is the default by simply adding or removing this single annotation from an existing StorageClass.

After successfully setting a default StorageClass, the next common issue you’ll encounter is understanding PersistentVolume lifecycle management, particularly reclaimPolicy and how it affects the underlying storage after a PVC is deleted.

Want structured learning?

Take the full K3s course →