Configuration reference (dekube.yaml)¶
The covenant was not written in stone but in clay — soft, yielding, shaped by the hand that first pressed it. Once fired, it held its form, and no disciple could reshape what the kiln had sealed.
— Necronomicon, On Covenants That Harden (debatable)
dekube.yaml is the project config file, created automatically on first run in --output-dir. After creation, the engine reads it but never writes to it — it's your file. Edit it freely.
The engine also accepts the legacy name helmfile2compose.yaml (with a deprecation warning).
Full schema¶
# Project name — used as compose file's top-level `name:` field.
# Auto-detected from helmfile on first run.
name: my-project
# Root directory for PVC bind mounts. PVC host_path values
# containing $volume_root are resolved relative to this.
# Default: ./data
volume_root: ./data
# PVC volume mappings. Auto-populated on first run.
# Each key is a PVC claim name from the K8s manifests.
# StatefulSet volumeClaimTemplates are keyed <vct>-<sts> (K8s names
# them <vct>-<sts>-<ordinal>; compose runs one replica).
volumes:
my-pvc:
host_path: ./data/my-pvc # bind mount (most common)
shared-data: # named volume (no host_path)
driver: local
data-my-statefulset: # volumeClaimTemplate "data" on StatefulSet "my-statefulset"
host_path: ./data/data-my-statefulset
# Workload names to exclude from conversion.
# Auto-populated on first run with K8s-only workloads
# (cert-manager, reflector, ingress controllers...).
# Supports fnmatch wildcards (e.g. "cert-manager-*").
exclude:
- cert-manager
- cert-manager-cainjector
- cert-manager-webhook
- haproxy-*
# User-defined string replacements applied to env vars,
# ConfigMap files, Secret files, and reverse proxy upstreams.
replacements:
- old: "https://app.prod.example.com"
new: "https://app.localhost"
# If true, skip the reverse proxy service in compose.yml but still
# write the ingress config as Caddyfile-<project> (for external use).
# Use when you manage your own reverse proxy outside of dekube.
# Default: false
# Legacy key: disableCaddy
disable_ingress: false
# If false, skip namespace inference entirely. Manifests without
# metadata.namespace will stay namespace-less — no FQDN aliases.
# Useful if your charts already set namespaces on all resources.
# Default: true
infer_namespaces: true
# Map custom ingressClassName values to canonical rewriter names.
# Without this, a custom class only matches through a rewriter's
# annotation heuristic (e.g. nginx.ingress.kubernetes.io/* annotations).
# Legacy key: ingressTypes
ingress_types:
haproxy-controller-internal: haproxy
haproxy-controller-external: haproxy
nginx-internal: nginx
# External compose network name. When set, the generated
# compose.yml uses an external network instead of creating one.
network: my-existing-network
# Per-service compose overrides. Deep-merged into the generated
# service definition. Use to patch image, env, volumes, etc.
overrides:
my-service:
image: custom-image:latest
environment:
EXTRA_VAR: "value"
volumes:
- ./custom:/app/custom:ro
# Custom compose services added verbatim to the output.
# Use to inject services that don't come from K8s manifests.
services:
maildev:
image: maildev/maildev:latest
restart: always
ports:
- "1080:1080"
# Per-extension configuration. Each key matches an extension's
# `name` attribute. Extensions read this via ctx.extension_config.
extensions:
caddy:
# Skip the Caddy compose service specifically (checked separately from
# disable_ingress; neither flag stops ingress entries from being collected)
disabled: false
# ACME email for Let's Encrypt
email: admin@example.com
# Use Caddy's internal CA instead of Let's Encrypt
tls_internal: true
# Disable an extension without removing it from --extensions-dir
my-extension:
enabled: false
# Extension-specific keys (varies per extension)
bitnami:
# ...bitnami-specific config
Key reference¶
| Key | Type | Default | Description |
|---|---|---|---|
name |
str |
(auto-detected) | Compose project name. Set from helmfile on first run. |
volume_root |
str |
./data |
Root directory for PVC bind mount paths. |
volumes |
dict |
{} |
PVC claim name → {host_path: "..."} mapping. Auto-populated on first run. volumeClaimTemplate claims are keyed <vct>-<sts>. Named volumes (no host_path) are added to compose volumes: top-level. |
exclude |
list[str] |
[] |
Workload names to skip. Supports fnmatch wildcards. |
replacements |
list[dict] |
[] |
String replacements: [{old: "...", new: "..."}]. Applied once to env vars, ConfigMap files, text Secret files, and reverse proxy upstreams. An entry with an empty or null old is skipped; a null new means "". |
disable_ingress |
bool |
false |
Skip the reverse proxy compose service. Ingress manifests are still dispatched to rewriters and the config file (e.g. Caddyfile) is still written, renamed to Caddyfile-<project> so it isn't picked up by accident. |
infer_namespaces |
bool |
true |
Infer missing metadata.namespace from sibling manifests and helmfile metadata. Set to false if your charts already set namespaces on all resources. See namespace inference. |
ingress_types |
dict[str, str] |
(none) | Custom ingressClassName → canonical rewriter name mapping. |
network |
str |
(none) | External compose network name. |
overrides |
dict |
(none) | Per-service compose overrides (deep-merged). |
services |
dict |
(none) | Custom compose services (added verbatim). |
extensions |
dict |
{} |
Per-extension config, keyed by extension name. |
Legacy bare volumes: key for volumeClaimTemplates
An existing dekube.yaml written before the <vct>-<sts> naming still works: the engine falls back to the bare <vct> key (the data path doesn't move) and warns PVC '<vct>-<sts>': using legacy mapping '<vct>' — rename it to '<vct>-<sts>' in dekube.yaml. If several StatefulSets fall back to the same legacy key, they'd share a data directory — the engine warns PVC collision: a, b share legacy mapping '<vct>' (same data directory) — give each its own entry and host_path in dekube.yaml instead. Both warnings only fire on non-first runs.
Per-extension config (extensions.*)¶
Each extension — converter, provider, indexer, transform, rewriter — receives its own config section via ctx.extension_config. The engine resolves it automatically: an extension with name = "caddy" receives the contents of extensions.caddy from the config file. An absent or empty block (caddy: with nothing under it) is {}.
extensions:
caddy:
email: admin@example.com # → ctx.extension_config["email"]
tls_internal: true # → ctx.extension_config["tls_internal"]
Any extension can be disabled without removing it from --extensions-dir:
The enabled key is checked by the engine before each convert() / transform() call and before rewriter dispatch. When false, the extension is loaded but never executed; a disabled rewriter's Ingresses fall through to the next matching rewriter.
extensions.nginx is shared
The nginx ingress provider and the nginx rewriter are both named nginx, so they read the same block — and enabled: false there disables both.
Empty entries are treated as absent rather than crashing the run: overrides: {svc: }, services: {svc: }, an empty volume_root (→ ./data), an empty ingress_types value.
Special value placeholders¶
Two placeholder patterns are resolved in config values:
-
$volume_root— replaced with the value ofvolume_root. Involumes:entries it must lead thehost_path($volume_rootor$volume_root/…); inoverrides:andservices:it's replaced anywhere: -
$secret:<secret_name>:<key>— resolved to the value of a K8s Secret key, inoverrides:,services:andreplacements:. The key ends at the first character a Secret key can't contain (anything outside[-._a-zA-Z0-9]), sopostgres://app:$secret:db:password@db:5432/appkeeps its@db. Inreplacements:the refs are resolved once the Secrets are indexed (generated ones included), right before the first provider runs; the resolved value is never written back todekube.yaml:
$ escaping in overrides:¶
Every environment value the engine or an extension generates has its $ doubled ($$) so compose passes it through literally instead of interpolating it — this runs once, after all transforms, and before overrides: are applied. overrides: values themselves stay raw on purpose: you keep compose's own ${VAR} interpolation available there. The one exception is a $secret:<name>:<key> reference inside an override — it's still resolved and escaped, so the secret value itself arrives literal.
Upgrading
If you were pre-escaping $$ by hand in chart values or replacements: to work around the old behavior, you'll now get $$$$ — remove the manual escaping. A ${VAR} you meant for compose interpolation but that arrives through chart values or replacements: is now taken literally — move it into overrides: instead, where it's left raw.
Upgrading from engine ≤ v1.7.0¶
Changes you can see after regenerating with a newer engine than v1.7.0 (helmfile2compose v3.4.0, kubernetes2simple v1.2.0):
- Mounts with
itemsget their ownconfigmaps/<name>_<hash>/(orsecrets/…) directory, so their paths incompose.ymlchange. Update anything pointing at the oldconfigmaps/<name>/. Mounts withoutitemskeep<name>/. envwins overenvFrom, as in Kubernetes (and the lastenvFromsource wins). A variable defined in both used to take theenvFromvalue.$in command/args: every$is now escaped for compose, so${X}and$$reach the container's shell as written, and$$(VAR)is a literal$(VAR). A${VAR}meant for compose interpolation in a container command must move tooverrides:. Literal env values now get$(VAR)expansion, and$$in them becomes$, as with kubelet.- An extension that fails to load stops the run (exit 1). It used to print a warning and convert without it — e.g. cert-manager with
cryptographymissing. - PVC
subPathis honoured: the mount moves to<host_path>/<subPath>. If the volume root already holds data and the subdirectory doesn't exist, the whole volume stays mounted with a warning until you move the data. - Secret files hold decoded bytes: binary keys (keystores) used to be written as base64 text. A binary value referenced as an env var is skipped with a warning instead of passing base64.
- A LoadBalancer Service publishes on its
port, not itsnodePort(simple-workload after v0.4.0): the host port moves if the chart set anodePort. NodePort Services still publishnodePort. - fix-permissions emulates
fsGroup(after v0.1.7): data directories of pods with anfsGroupget that group,g+rwXand setgid on directories — host-side modes on existing data change — and their services getgroup_addplus adepends_ononfix-permissions. - Caddy
server-ca(after v0.2.2): a CA Secret present in the manifests is now written to./secrets/<name>/. One that isn't (ExternalSecret, hand-placed files) keeps its mount, with a warning: putca.crtin./secrets/<name>/yourself. - HAProxy is the ingress fallback (haproxy rewriter after v0.1.3, priority 1100). With nginx or traefik loaded (kubernetes2simple, or
--extensions-dir), a classless Ingress carrying their annotations now goes to them instead of getting plain HAProxy routing.enabled: falsenow disables rewriters too. - servicemonitor (after v0.3.5): without
namespaceSelector, only Services in the ServiceMonitor's own namespace match, as with the operator. Cross-namespace setups neednamespaceSelector. Targets use the K8s Service name, and everyjob_namebecomesserviceMonitor/<ns>/<name>/<i>as with the operator — thejoblabel your dashboards and alerts query changes with it. - fake-apiserver (after v0.666.2) requires the service-account token and binds its exposed port to
127.0.0.1. Reconvert, then hand out the new kubeconfig. - flatten-internal-urls: path segments stay put. A Service name is rewritten only after
//or@(a URL host), no longer after any/—http://gw/api/v1keeps its/api/segment even whenapiis a Service. The network alias that segment matches is now kept instead of stripped. - nginx and traefik rewriters (after v0.4.3 and v0.3.3):
spec.defaultBackendand rules without a host are still skipped, now with a warning (same as HAProxy). nginxuse-regexpaths withoutrewrite-targetbecome real prefix matches (/api(/|$)(.*)→/api) instead of a literal glob that never matched; a regex that isn't a plain prefix wildcard falls back to its literal prefix with a warning — broader than the regex, so extra paths reach the backend.auth-tls-secret(client-cert mTLS) now joins the access-control warning. - httpGet healthchecks (simple-workload after v0.4.0) try
wget, thencurl, then bash's/dev/tcp, passhttpHeaders, and target127.0.0.1instead oflocalhost. An image without wget (debian-slim) now passes its check; ahealthcheck:override you added for that reason can go. - cnpg (after v0.2.1):
enableSuperuserAccessdefaults tofalse, as in CloudNativePG, so<cluster>-superuseris only published when a Cluster setsenableSuperuserAccess: true. An app that references that Secret needs the flag added before you regenerate. - Distribution builders:
build-distribution.pyfails when two sources define a top-level name differently (details).
Legacy key migration¶
On load, the engine auto-migrates legacy keys to their modern equivalents:
| Legacy key | Migrated to |
|---|---|
disableCaddy |
disable_ingress |
ingressTypes |
ingress_types |
caddy_email |
extensions.caddy.email |
caddy_tls_internal |
extensions.caddy.tls_internal |
helmfile2ComposeVersion |
(removed) |
Migration happens in memory on load. The file is not rewritten — rename the keys manually when convenient.
Related¶
- CLI reference — command-line flags
- Writing extensions — how extensions read
ctx.extension_config - Pitfalls — null-safe YAML access patterns