Architecture¶
Pipeline¶
The dark and twisted ritual underlying the conversion. The same Helm charts used for Kubernetes are rendered into standard K8s manifests, then converted to compose:
Helm charts (helmfile / helm / kustomize)
| helmfile template / helm template / kustomize build
v
K8s manifests (Deployments, Services, ConfigMaps, Secrets, Ingress...)
| helmfile2compose.py
v
compose.yml + Caddyfile + configmaps/ + secrets/
A dedicated helmfile environment (e.g. compose) typically disables K8s-only infrastructure (cert-manager, ingress controller, reflector) and adjusts defaults for compose.
Converter dispatch¶
Manifests are classified by kind and dispatched to converter classes. Each converter handles one or more K8s kinds and returns a ConvertResult (compose services, Caddy entries, or both).
K8s manifests
| parse + classify by kind
| dispatch to converters (built-in or external, same interface)
v
compose.yml + Caddyfile
Built-in converters¶
WorkloadConverter— kinds: DaemonSet, Deployment, Job, StatefulSetIngressConverter— kinds: Ingress
External converters (extensions)¶
Loaded via --extensions-dir. Each .py file (or one-level subdirectory with .py files) is scanned for classes with kinds and convert(). Loaded converters are sorted by priority (lower = earlier, default 100) and registered into the dispatch loop.
.h2c/extensions/
├── keycloak.py # flat file
├── h2c-operator-certmanager/ # cloned repo
│ ├── certmanager.py # converter class
│ └── requirements.txt
See Writing operators for the full guide.
What it converts¶
| K8s kind | Compose equivalent |
|---|---|
| DaemonSet / Deployment / StatefulSet | services: (image, env, command, volumes, ports). Init containers become separate services with restart: on-failure. Sidecar containers become separate services with network_mode: container:<main> (shared network namespace). DaemonSet treated identically to Deployment (single-machine tool). |
| Job | services: with restart: on-failure (migrations, superuser creation). Init containers converted the same way. |
| ConfigMap / Secret | Resolved inline into environment: + generated as files for volume mounts |
| Service (ClusterIP) | Hostname rewriting (K8s Service name -> compose service name) |
| Service (ExternalName) | Resolved through alias chain (e.g. docs-media -> minio) |
| Service (NodePort / LoadBalancer) | ports: mapping |
| Ingress | Caddy service + Caddyfile reverse_proxy blocks. Path-rewrite annotations -> uri strip_prefix. Backend SSL -> Caddy TLS transport. |
| PVC / volumeClaimTemplates | Host-path bind mounts (auto-registered in helmfile2compose.yaml) |
| securityContext (runAsUser) | Auto-generated fix-permissions service (chown -R <uid>) for non-root bind mounts |
Not converted (warning emitted)¶
- CronJobs
- Resource limits / requests, HPA, PDB
Silently ignored (no compose equivalent)¶
- RBAC, ServiceAccounts, NetworkPolicies, CRDs (unless claimed by a loaded extension), IngressClass, Webhooks, Namespaces
- Probes (liveness, readiness, startup) — no healthcheck generation
- Unknown kinds trigger a warning
Processing pipeline¶
- Parse manifests — recursive
.yamlscan, multi-doc YAML split, classify by kind. Malformed YAML files are skipped with a warning. - Index lookup data — ConfigMaps, Secrets, Services indexed for resolution during conversion.
- Build alias map — K8s Service name -> workload name mapping. ExternalName services resolved through chain.
- Build port map — K8s Service port -> container port resolution (named ports resolved via container spec).
- Pre-register PVCs — from both regular volumes and
volumeClaimTemplates. - First-run init — auto-exclude K8s-only workloads, generate default config.
- Dispatch to converters — each converter receives its kind's manifests + a
ConvertContext. Extensions run in priority order (lower first), then built-in converters. - Post-process env — DNS rewriting, alias resolution, port remapping, replacements applied to all service environments (idempotent — catches extension-produced services).
- Apply overrides — deep merge from config
overrides:andservices:sections. - Hostname truncation — services with names >63 chars get explicit
hostname:. - Write output —
compose.yml,Caddyfile, config/secret files.
Automatic rewrites¶
These happen transparently during conversion:
- K8s DNS —
<svc>.<ns>.svc.cluster.localand<svc>.<ns>.svcrewritten to compose service names. Applied to env vars, ConfigMap files, and Caddyfile upstreams. - Service aliases — K8s Services whose name differs from the workload are resolved. ExternalName services followed through the chain.
- Port remapping — K8s Service port -> container port in URLs.
http://svc(implicit port 80) andhttp://svc:80both rewritten tohttp://svc:8080if the container listens on 8080. - Kubelet
$(VAR)—$(VAR_NAME)in container command/args resolved from the container's env vars. - Shell
$VARescaping —$VARin command/entrypoint escaped to$$VARfor compose. - String replacements — user-defined
replacements:from config applied to env vars, ConfigMap files, and Caddyfile upstreams.
Docker/Compose gotchas¶
These are Docker/Compose limitations, not conversion limitations. See Limitations for what gets lost in translation.
- Large port ranges — K8s with
hostNetworkhandles thousands of ports natively. Docker creates one iptables/pf rule per port, so a range like 50000-60000 (e.g. WebRTC) will kill your network stack. Reduce the range in your compose environment values (e.g. 50000-50100). - hostNetwork — K8s pods can bind directly to the host network. In Compose, every exposed port must be mapped explicitly.
- S3 virtual-hosted style — AWS SDKs default to virtual-hosted bucket URLs (
bucket-name.s3:9000). Compose DNS can't resolve dotted hostnames. Configure your app to use path-style access and use areplacementif needed.