1. Nobody Thought About "Who Owns the Volume" in Docker Compose

When you're doing local development with Docker Compose, few people ever stop to think about who actually owns a volume. Write volumes: ./data:/app/data in your docker-compose.yml, and the host directory just shows up inside the container. Permission issues almost never come up.
That's expected behavior: Docker's own documentation explains that bind mounts have write access to files on the host by default (Docker Docs: Bind mounts). In other words, whatever permissions the host directory has get reflected straight into the container — Docker doesn't add a separate permission layer of its own. If you create the directory with permissive enough settings for development, most applications just work without anyone thinking about it.
That exact assumption — "it just works" — is what trips people up first when migrating from Docker Compose to K3s. K3s (and Kubernetes clusters in general) takes a fundamentally different approach to volumes than Compose does. The more you hand-assemble the configuration yourself, the more of these assumption gaps you end up discovering and fixing one by one.
2. What Happens the Moment You Bring It to K3s — The local-path-provisioner Behavior

K3s ships with Rancher's local-path-provisioner built in, so simply creating a PVC (PersistentVolumeClaim) automatically provisions a volume directory on the node's local disk (K3s Docs: Volumes and Storage). By default it uses the local-path StorageClass, and the actual volume data lives under /var/lib/rancher/k3s/storage.
The catch is that this provisioning process doesn't take care of directory ownership (chown). Looking at the setup script in the official local-path-provisioner repository, the only operation it runs is mkdir -m 0777 -p "$VOL_DIR" — the permission bits are set to 777 (world read/write), but the owning user and group are never changed (rancher/local-path-provisioner, k3s-io/k3s local-storage.yaml).
You'd think 777 permissions mean anyone can write to it. In practice, many official container images are built around runAsNonRoot or a specific UID/GID, and depending on how securityContext.runAsUser is set on the Pod — or how the image itself is designed to start as a non-root user — combined with SELinux settings or umask on the node, write failures have been reported in the wild (k3s-io/k3s Issue #10130). A question that never surfaced with Docker Compose's bind mounts — "who exactly is writing here?" — suddenly becomes impossible to ignore on K3s.
3. Relative Paths Are a Trap Too — hostPath Requires an Absolute Path

If you're used to Docker Compose's bind mounts, there's another easy-to-miss assumption. Compose's volumes: accepts paths relative to docker-compose.yml, but a Kubernetes hostPath volume won't work unless it's an absolute path (Kubernetes Docs: Volumes).
volumes:
- name: data-volume
hostPath:
path: /mnt/data # must be absolute; relative paths don't work
type: Directory
Carry over a Compose-era habit of writing a relative path like ./data into your manifest, and Kubernetes simply can't locate that directory anywhere on the host — the Pod gets stuck with something like CreateContainerConfigError. On top of that, hostPath grants direct access to the node's filesystem, which is a fairly powerful privilege, so the Kubernetes documentation itself recommends limiting it to the minimum necessary directories in production and adding readOnly: true where possible. In practice, it's safer to lean on the PVC-backed volumes that local-path-provisioner generates automatically, and keep direct hostPath usage to a minimum.
4. The Fix — fsGroup, initContainers, and Custom StorageClass Patterns

The good news is that there are a few built-in mechanisms for solving this permission problem without writing anything elaborate yourself.
- Set
securityContext.fsGroup: SpecifyfsGroupin the Pod's securityContext, and Kubernetes automatically changes the volume's group ownership to that GID before the container starts (Kubernetes Docs: Security Context). Combine it withrunAsUserand even a non-root container can write to the volume. This is the simplest option to try first when setting things up. - Customize
userPattern/groupPattern/permPatternon the StorageClass:local-path-provisionersupports StorageClass parameters that let you change the ownership and permission pattern used when it creates directories (rancher/local-path-provisioner). This is a good fit if your whole team standardizes on one UID/GID. - chown it in an initContainer: For the edge cases where fsGroup doesn't take effect (certain CSI drivers, legacy images), a root-privileged initContainer that explicitly chowns the target directory before the main container starts is still a reliable fallback.
Whichever approach you pick, the underlying point doesn't change: the volume ownership that Docker Compose never made you think about has to be explicitly designed on K3s. Deciding between fsGroup and a custom StorageClass pattern is exactly the kind of judgment call that eats up time when you haven't hit it before. With Kubo's AI-Driven Deployment, you can describe your application's requirements and get a proposed volume permission design up front, cutting down on the trial-and-error.
5. depends_on Disappears Too — Compose Configs Built Around Startup Order Don't Just Work as-is

Alongside volume permissions, startup order is another thing that's easy to overlook during migration. Docker Compose's depends_on is a convenient feature that guarantees container startup order, but Kubernetes has no equivalent concept — Pods generally start in parallel.
Even when you mechanically convert a Compose definition into Kubernetes manifests using kompose, the depends_on key is simply ignored (kompose official conversion doc, OneUptime: Converting with Kompose). It's common for the application container to start before the database container, crash-loop on a connection error, and present itself as the next wall right after you've just solved the volume permission problem.
The standard fix is to add logic in an initContainers step that waits until a dependency like the database is reachable (something like until nslookup db-service; do sleep 2; done), or to implement connection retries in the application itself (10 Common Pitfalls When Migrating Docker Compose to Kubernetes). Name resolution between services, on the other hand, is conceptually close between Docker Compose's built-in DNS and Kubernetes' CoreDNS, so that part tends to be a relatively easy piece of the migration (Service Discovery and DNS in Containers).
This pattern — something that worked as an implicit assumption locally now demanding an explicit design decision on K3s — shows up in both volume permissions and startup order.
Summary
Boiling down the common stumbling blocks when migrating from Docker Compose to K3s gives you three points:
- Volume permissions:
local-path-provisioneronly creates the directory with mode 777 — it never chowns it. You need to explicitly design ownership usingfsGroupor a custom StorageClass pattern. - Path format: Compose's relative paths don't work with K3s's
hostPath. Absolute paths are required. - Startup order: There's no Kubernetes equivalent to
depends_on— you need to substitute it with an initContainer or application-level retries.
Each of these is a trap built on the assumption that "if it worked locally, it'll work in production too." These kinds of early setup pitfalls keep recurring for as long as you're hand-assembling the configuration from scratch. With Kubo's AI-Driven Deployment, you can work through volume permission design and StorageClass selection using natural-language instructions, cutting down on the time spent writing YAML line by line and tracking down the cause of a permission error. The full picture of your setup stays visible in Kubo Captain UI, so you're never left guessing at a black box.
If you want fewer do-overs migrating from Docker Compose — or you're stuck on a Permission denied error right now — you can also reach out via Contact Us for a configuration consultation. Kubo, built on K3s, aims to give you the power of EKS/AKS with the ease of Vercel, and it's designed with exactly these kinds of early migration pitfalls in mind. If there's a reason you can't send data outside your own environment, Kubo On-Premise offers the same K3s-based foundation built entirely within your own infrastructure.