Using helmfile2compose with your own project¶
You're here because someone asked for a docker-compose. A colleague, a client, a mass of uninitiated who do not care that Kubernetes exists for a reason. You have explained. They do not care. They want docker compose up and they want it yesterday.
I've been there. The first time, I ignored the request. The second time, I built a tool. The third time, I went back to my own platform, said "why not" — and the tool stopped being a tool. At least you're warned about what it does, which is more than most ecosystems offer before pulling you in.
helmfile2compose vs dekube-engine
dekube-engine is the bare conversion engine — an empty pipeline with no built-in converters. helmfile2compose is the distribution: dekube-engine bundled with 8 extensions (workloads, indexers, HAProxy, Caddy) into a single helmfile2compose.py. This is what you download, run, and ship. When this page says "helmfile2compose", it means the distribution — the thing you actually use.
He who renders the celestial into the mundane does not ascend — he merely ensures that both realms now share his suffering equally.
— Necronomicon, On the Folly of Downward Translation (or so I recall)
For the dark and twisted ritual underlying the conversion — what gets converted, how, and what unholy transformations are applied — see Architecture.
Requirements¶
- Python 3.10+
pyyamlhelmfile+helm(only if rendering from helmfile directly)- Docker Compose (v2) — recommended for running the generated output. nerdctl compose requires the
flatten-internal-urlstransform to work (see Network aliases). Podman Compose works (v1.0.6+).
Before you start: ingress controller¶
Your helmfile already uses an ingress controller. That choice is baked into your Ingress manifests — annotations, ingressClassName — and you're not going to change it for a compose migration. helmfile2compose needs a rewriter that speaks your controller's annotation dialect and translates it into reverse proxy configuration. If your controller isn't supported, your Ingress manifests will be silently skipped and you'll have no reverse proxy — which is the single most visible thing that breaks.
The default reverse proxy is Caddy, bundled with the distribution. The entire ingress system is pluggable — rewriters can be installed or written, and Caddy itself can be replaced.
| Controller | Rewriter | Status |
|---|---|---|
| HAProxy | bundled | stable |
| Nginx | dekube-rewriter-nginx (extension) | stable |
| Traefik | dekube-rewriter-traefik (extension) | POC |
If you use something else (Contour, Ambassador, Istio, AWS ALB, etc.) — standard Ingress host/path/backend fields are always read, so basic routing works, but controller-specific annotations won't translate. Check this before investing time in the rest of the setup.
CRDs (Keycloak, cert-manager, trust-manager, Prometheus) are a different story — they're optional, and unsupported CRDs are simply skipped with a warning. Your stack works without them; you just lose the resources they would have produced. Ingress is not optional.
kubernetes2simple
kubernetes2simple exists as an all-inclusive distribution — helmfile2compose + every extension, one script, zero config. It's convenient for a quick test, but probably not what you want long-term: it enables transforms like bitnami that silently replace images, which may not be desired for your stack. As a maintainer, you'll want to pick your extensions deliberately.
fix-permissions is bundled with the distribution and active by default. It generates a busybox init service that fixes bind mount ownership for non-root containers — in most cases a net gain. If you don't want it: bundled extensions can't be individually disabled yet (it's on the roadmap). In the meantime, use a compose.override.yml to neutralize it:
compose.override.yml is automatically merged by Docker Compose, never overwritten by helmfile2compose, and yours to maintain.
Installation¶
Download helmfile2compose.py from the latest helmfile2compose release.
If your stack uses CRDs that have an dekube extension (Keycloak, cert-manager, trust-manager), grab the extension .py files from their repos too and drop them in an extensions/ directory next to the script:
extensions/
├── keycloak.py # from dekube-provider-keycloak
└── cert_manager.py # from dekube-converter-cert-manager
That's it. No package manager needed at this stage — dekube-manager is for later, when you ship a generate-compose.sh to your users (see Recommended workflow).
Preparing your helmfile¶
Create a dedicated environment (e.g. compose) that disables K8s-only infrastructure. These components have no meaning in compose and will be auto-excluded on first run anyway, but disabling them avoids rendering useless manifests:
# environments/compose.yaml
certManager:
enabled: false
ingress:
enabled: false
reflector:
enabled: false
Everything else stays enabled — the tool needs to see your Deployments, Services, ConfigMaps, Secrets, and Ingress resources to do its job.
If your stack uses CRDs that have an dekube extension (Keycloak, cert-manager, trust-manager), keep those enabled — the extensions you should have previously acquired will handle them.
First run¶
python3 helmfile2compose.py --helmfile-dir ~/my-project -e compose \
--extensions-dir extensions --output-dir .
docker compose up -d
This renders the helmfile and converts in one step. Despite the name, helmfile is not required — --from-dir accepts any directory of pre-rendered K8s YAML (from helm template, kustomize build, etc.).
On first run, the tool creates dekube.yaml with sensible defaults:
- All PVCs registered as host-path bind mounts under ./data/
- K8s-only workloads auto-excluded (any workload whose name contains cert-manager, ingress, or reflector — this targets controller Deployments, not Ingress resources)
- Project name derived from the source directory
Stop here and review dekube.yaml. You will almost certainly need to:
- Adjust volume paths
- Exclude workloads that make no sense outside K8s (operators, CRD controllers, etc.)
- Add overrides for images that need replacing (e.g. bitnami -> vanilla)
See Configuration for the full reference.
CLI flags¶
| Flag | Description |
|---|---|
--helmfile-dir |
Directory containing helmfile.yaml or helmfile.yaml.gotmpl (default: .) |
-e, --environment |
Helmfile environment (e.g. compose, local) |
--from-dir |
Skip helmfile, read pre-rendered YAML from this directory |
--output-dir |
Where to write output files (default: .) |
--compose-file |
Name of the generated compose file (default: compose.yml) |
--extensions-dir |
Directory containing extension .py files |
--from-dir and --helmfile-dir serve the same purpose (providing input manifests). If both are specified, --from-dir takes priority.
Output files¶
compose.yml— services (incl. Caddy reverse proxy), volumesCaddyfile(orCaddyfile-<project>whendisable_ingress: true) — reverse proxy config from Ingress manifestsdekube.yaml— persistent config (see Configuration)configmaps/— generated files from ConfigMap volume mountssecrets/— generated files from Secret volume mounts
Check known issues and workarounds¶
Before debugging something for hours, check the known workarounds. Recurring tentacles have been identified, sliced, and served — no need to catch the same kraken twice.
If your stack includes Bitnami charts (Redis, PostgreSQL, Keycloak), install the bitnami transform — it handles the worst of it automatically.
What works well¶
- Deployments, StatefulSets, DaemonSets, Jobs — converted to compose services with the right image, env, command, volumes, and ports. Init containers and sidecars get their own services.
- ConfigMaps and Secrets — resolved inline into environment variables, or generated as files when volume-mounted.
- Services — network aliases (K8s FQDNs resolve natively via compose DNS), alias resolution, port remapping. If your K8s Service remaps port 80 to targetPort 8080, the tool rewrites URLs and env vars automatically.
- Ingress — converted to a Caddy reverse proxy with automatic TLS. Path-based routing, host-based routing, catch-all backends. Backend SSL annotations supported.
- PVCs — registered in config as bind mounts.
volumeClaimTemplates(StatefulSets) included. - CRDs — with extensions, Keycloak, cert-manager, and trust-manager CRDs are fully converted.
What needs manual help¶
- Unsupported CRDs — Zalando PostgreSQL, Strimzi Kafka, etc. are skipped with a warning unless you write an extension. You'll need to add equivalent services manually via the
services:section in config. - Bitnami images — often need replacing with vanilla equivalents via
overrides:. Bitnami images have opinions about environment variables, init scripts, and volume paths that don't always translate well. - S3 virtual-hosted style — compose DNS can't resolve
bucket.minio:9000. Force path-style in your app config and use areplacementif needed. - CronJobs — not converted. Run them externally or use a sleep-loop wrapper (but please don't).
See Limitations for the complete list of what gets lost in translation.
Ingress controllers — details¶
Compatibility was covered above. This section documents annotation coverage and configuration.
Annotations handled¶
| Controller | Annotations |
|---|---|
| HAProxy | haproxy.org/path-rewrite → strip prefix (scoped per path on multi-path rules), haproxy.org/server-ssl + server-ca → backend TLS, server-sni |
| Nginx | rewrite-target, backend-protocol, enable-cors, proxy-body-size, configuration-snippet (partial) |
| Traefik | router.tls, standard Ingress path rules. No middleware CRD support. |
HAProxy is built into the helmfile2compose distribution. Nginx and Traefik are extensions — install them with dekube-manager or drop the .py file in your extensions/ directory.
Custom ingress class names¶
If your cluster uses custom ingressClassName values (e.g. haproxy-internal, nginx-dmz), add a mapping in dekube.yaml (see Configuration — ingress_types):
Without this, helmfile2compose won't recognize the class and the Ingress is skipped with a warning.
Recommended workflow¶
-
One helmfile, two environments. Keep your K8s environment as-is. Add a
composeenvironment that disables cluster-only components. Same charts, same values (mostly), different targets. -
Ship a
generate-compose.sh. A wrapper script that downloads dekube-manager, installs helmfile2compose + extensions, runs the conversion, and maybe generates secrets. See stoatchat-platform or lasuite-platform for examples. -
Ship a
dekube.yaml.template. Pre-configure excludes, overrides, and volume mappings that are specific to your project. The generate script copies it todekube.yamlon first run. Users then customize their copy. -
Pin a release. Use
--distribution-versionin dekube-manager and==versionfor extensions. Don't point atmain. The tool mutates between releases — that's documented in the changelog, which is more than most ecosystems offer. Pin your versions. The heretic pins his own.
The two projects that caused this to exist¶
- stoatchat-platform — 15 services. The first patient. Worked on the first try, which was the most dangerous outcome.
- lasuite-platform — 22 services + 11 init jobs. The second patient. Tentacles started appearing around the volumeClaimTemplates.
Both ship with generate-compose.sh and dekube.yaml.template. Reading their setup is probably more useful than anything I could write here.
Garbage in, garbage out¶
helmfile2compose does zero validation of your helmfile output. If your manifests reference a ConfigMap that doesn't exist, a Secret with a missing key, or a Service pointing at a non-existent Deployment — helmfile2compose will crash with an ugly Python traceback, not a helpful error message.
This is by design. Error handling for malformed K8s manifests is not helmfile2compose's job — it would massively increase complexity for something that helmfile lint, helm template --validate, and kubectl apply --dry-run already do. helmfile2compose assumes its input is valid. If it isn't, the consequences are yours.
Make sure your helmfile works on a real Kubernetes cluster first. A real runtime — one with an actual apiserver, actual controllers, actual sanity — should have validated the output before helmfile2compose ever sees it. helmfile2compose is a downstream consumer, not a linter. Fix your helmfile, re-render, re-convert. Actions, consequences.
Final warning¶
This tool works. It has been tested on real helmfiles with real users. But it is, fundamentally, an act of desecration — stripping Kubernetes of everything that makes it Kubernetes (scheduling, scaling, self-healing, network policies, RBAC) and leaving behind a flat list of containers. Every edge case you hit is a reminder that you are running something that was designed for orchestration on a machine that has no orchestra — and that the person who asked you for a docker-compose owes you a drink — or the psychiatric bill, whichever comes first.
The temple was not translated — it was dismantled, stone by stone, and rebuilt as a shed. The prayers still worked. The architect watched, powerless, as the faithful praised the shed.
— De Vermis Mysteriis, On Unnecessary Simplifications (debatable)