From Docker Compose to Kubernetes: What Translated and What Didn't

I rebuilt my entire homelab from Docker Compose files on Unraid to Kubernetes on Talos Linux. Here's what carried over cleanly, what didn't, and whether the complexity was worth it.

From Docker Compose to Kubernetes: What Translated and What Didn't

Before this cluster existed, my homelab was a pile of Docker Compose files on an Unraid server. Containers ran, volumes mounted, ports published. It worked. But every change was imperative: SSH in, edit a YAML file, docker compose up -d, hope nothing broke. No version history, no rollback plan, no visibility into what was running where.

In February 2026, I rebuilt everything from scratch on Talos Linux and Kubernetes with Flux GitOps. Not a migration in the traditional sense. I didn't kompose convert my way into Kubernetes. I rewrote every deployment by hand, one service at a time, learning what translated cleanly and what didn't.

This is what I learned.

What Translates Cleanly

Container images are container images. The same vaultwarden/server:1.33.2 image runs in Docker Compose and Kubernetes. No rebuilding, no re-tagging. If it works in Docker, it works in a pod. This is the easiest part of the migration and the one thing that "just works."

Environment variables carry over directly. A Compose environment: block maps 1:1 to a Kubernetes env: block. The syntax is slightly different (YAML key-value vs name/value pairs), but the concept is identical:

# Docker Compose
environment:
  - DOMAIN=https://vault.naviauxlab.com
  - SIGNUPS_ALLOWED=false

# Kubernetes Deployment
env:
  - name: DOMAIN
    value: "https://vault.naviauxlab.com"
  - name: SIGNUPS_ALLOWED
    value: "false"

Volume mounts are conceptually the same. A Compose volumes: section maps to volumeMounts + volumes in a pod spec. The mount path inside the container doesn't change. What changes is where the data lives: from a Docker-managed directory or bind mount to a PersistentVolumeClaim backed by Longhorn.

What Doesn't Translate

Networking is completely different. In Docker Compose, services find each other by container name on a shared bridge network. Port publishing (ports: "8080:80") exposes services to the host. The mental model is simple: containers are on a virtual LAN, ports poke holes to the outside.

Kubernetes networking has none of this. Services get stable DNS names (vaultwarden.vaultwarden.svc.cluster.local), but you need to explicitly create Service objects. External access requires an Ingress controller (Traefik in my case), Ingress resources with host-based routing, and TLS certificates. What was a single ports: line becomes a Service, an Ingress, a cert-manager annotation, and a Traefik middleware chain.

It's more work upfront. But it's also more explicit: I know exactly how traffic flows to every service, because I had to declare it.

"Just restart it" becomes a design decision. Docker Compose has restart: unless-stopped. Kubernetes has liveness probes, readiness probes, restart policies, and resource limits that trigger OOM kills. When a container crashes in Docker, it restarts. When a pod crashes in Kubernetes, the same thing happens, but you also get structured events, metrics, alerting, and the ability to set probe endpoints so Kubernetes knows the difference between "crashed" and "alive but not ready to serve traffic."

Every deployment in my cluster has both a liveness and readiness probe. In Docker Compose, I had neither. The apps didn't change. My operational awareness of them did.

Secrets management goes from "hope nobody reads the file" to encryption at rest. In Compose, secrets were environment variables in a .env file sitting on disk. Maybe it had restrictive permissions. Maybe it was in .gitignore. In Kubernetes, secrets are SOPS-encrypted with age keys and stored in Git. Flux decrypts them at apply time. The plaintext never touches disk outside the cluster, and every secret change is version-controlled.

This was one of the biggest upgrades. Not because Docker Compose can't do secrets properly (it can, with Docker Swarm secrets or external vaults), but because the Kubernetes ecosystem makes encryption the default path of least resistance.

What I Gained

Git as the source of truth. Every piece of my infrastructure is in a Git repository. If the cluster burns down, I clone the repo, bootstrap Flux, restore the SOPS key, and everything comes back. With Docker Compose, "the source of truth" was whatever was running on the server, and backups meant hoping I remembered to copy the right directories.

Declarative dependency ordering. My Flux setup has two Kustomizations: infrastructure and apps. Apps depend on infrastructure: Traefik, Longhorn, cert-manager, and Cilium must be healthy before any application deploys. In Docker Compose, depends_on only waits for the container to start, not for it to be ready. The number of times I watched an app crash because PostgreSQL wasn't accepting connections yet...

Network policies. Every namespace in my cluster has default-deny ingress and egress. Each app explicitly declares what traffic it needs. In Docker Compose, everything on the same bridge network could talk to everything else. There was no isolation, no segmentation, no audit trail of what was supposed to talk to what.

Resource limits and observability. I know exactly how much memory every pod uses because every pod has resource requests and limits, and Prometheus scrapes the metrics. In Docker Compose, I found out an app was using too much memory when the OOM killer showed up and took out something important.

What I Lost

Simplicity. A Docker Compose file for Vaultwarden is about 15 lines. The Kubernetes equivalent is 7 files across ~200 lines: Namespace, Deployment, Service, Ingress, PVC, NetworkPolicies, and Kustomization. Most of that is boilerplate that I now copy between apps, but it's still a lot more surface area to get wrong.

Quick iteration. In Docker Compose, changing an environment variable was: edit file, docker compose up -d, done. In Kubernetes with GitOps, it's: edit file, commit, push, wait for Flux to reconcile (up to 10 minutes), check the rollout status. The feedback loop is longer by design (Git review before deploy), but it slows down experimentation.

Direct filesystem access. Talos Linux is immutable. There's no SSH, no shell, no docker exec into the host. If I need to debug something, it's kubectl exec into a pod or talosctl to query the node API. For someone coming from Docker Compose on a regular Linux host, this is the biggest adjustment.

The Migration Approach That Worked

I didn't try to migrate everything at once. I built the platform layer first (Cilium, Traefik, Longhorn, cert-manager) and got that stable over a few days. Then I migrated apps one at a time, starting with the simplest:

  1. Day 1: Platform layer (networking, storage, ingress, TLS)
  2. Day 1: First apps: a static portfolio site and Uptime Kuma (stateless or near-stateless)
  3. Day 2: Authentik SSO + Cloudflare Tunnel (the hard infrastructure)
  4. Week 2: Stateful apps in batches: Miniflux, ntfy, Navidrome, Audiobookshelf
  5. Week 2: Everything else: Vaultwarden, Linkding, IT-Tools, Actual Budget
  6. Week 2: Ghost CMS replacing the static portfolio

Each app followed the same pattern: create the namespace, write the deployment, add the service and ingress, write network policies, test, commit. By app number five, I had a template I could fill out in 15 minutes. By app number ten, the pattern was muscle memory.

The key insight: don't use kompose. The tool exists to convert Docker Compose files to Kubernetes manifests, and it produces technically valid YAML that misses the point entirely. It doesn't generate network policies, resource limits, health probes, or proper ingress configuration. You end up with Kubernetes manifests that have all the complexity of Kubernetes and none of the benefits. Write them by hand. You'll understand your infrastructure better for it.

Was It Worth It?

Unambiguously yes, but not for the reasons I expected.

I thought the main benefit would be "Kubernetes on my resume." The actual benefit is that I finally understand my homelab. Every network path is documented in a policy. Every secret is encrypted and version-controlled. Every app has resource limits and health checks. When something breaks, I have metrics, logs, and alerts, not a hunch and docker logs -f.

Docker Compose is a great tool for running containers. Kubernetes is a great tool for operating them. The gap between those two things is where I spent most of my career learning, and this migration is where the concepts finally clicked.

If you're running a homelab on Docker Compose and it's working fine, that's genuinely fine. Don't migrate because someone on Reddit said you should. Migrate when you want to learn Kubernetes properly, when you want GitOps and network segmentation and encrypted secrets, and when you're willing to trade simplicity for operational rigor. The payoff is real, but so is the investment.