Problem
Four catalog apps have been found unable to start in the last day — #248 (HAVEN missing 11 required env vars), #256 (three apps with no user: under runAsNonRoot), #263 (user: root + cap_drop: ALL breaks the mariadb entrypoint chown), #264 (read-only root filesystem with no writable scratch). Every one was found by reading Rust and comparing it against upstream image docs. None was found by starting the container, because there is no way to start one without a cluster.
We have compose-validate (lnvps_compose/src/bin/compose-validate.rs, master), and it is doing its job — it parses, validates and reports the footprint. But validation is a static check on the document. It cannot tell you that the image's entrypoint wants to write to /run, or that the app exits because RELAY_URL is empty. Those only surface when a container actually starts.
Ask
A second bin in lnvps_compose that transforms an lnvps-compose document into a standard docker-compose.yaml, so an agent can run docker compose up locally and watch the app come up — or fail — before opening a PR.
Design
Bin: lnvps_compose/src/bin/compose-to-docker.rs, following compose-validate's conventions (path args or - for stdin, non-zero exit on failure).
cargo run -p lnvps_compose --bin compose-to-docker -- app.yaml \
--out-dir .local/haven --config RELAY_URL=wss://localhost:7777 --hostname localhost
docker compose -f .local/haven/docker-compose.yaml up
Resolution must go through the same code the operator uses, not a re-implementation:
resolve_config (lnvps_compose/src/lib.rs:1013) for submitted values ∪ declared defaults, with the same required-field error.
Compose::config_defaults (lib.rs:481), then generated secrets, then supplied config, then HOSTNAME — the ordering in build_vars (lnvps_operator/src/app_deployments.rs:827).
Compose::resolve_env (lib.rs:781), resolve_files (lib.rs:799), resolve_init (lib.rs:740).
- Secrets generated like
ensure_secrets (app_deployments.rs:867), but written to the out-dir so a re-run of the same app is stable.
Mapping:
| lnvps-compose |
docker-compose |
why |
| service key |
service key |
k8s Service name == compose service name (app_deployments.rs:522), so mariadb:3306 resolves the same on docker's default network |
image |
image |
|
env (substituted) |
environment |
|
volumes[] |
named volume mounted at path |
PVC equivalent |
files[] |
rendered into --out-dir, bind-mounted :ro at path |
k8s mounts these read-only via subPath |
ports[].container |
expose; publish to the host only for expose: ingress |
Expose (lib.rs:292) |
resources.cpu / .memory |
cpus / mem_limit |
|
user |
user |
lib.rs:81 |
depends_on |
depends_on |
|
init[] (lib.rs:85) |
one-shot services + depends_on: {condition: service_completed_successfully}, tmpfs /tmp |
mirrors the init emptyDir at app_deployments.rs:631 |
The hardening is the point — emit it, do not skip it. A transform that produces a permissive docker-compose would have started all four broken apps cleanly and told us nothing. container_security_context_for (app_deployments.rs:332-348) sets:
read_only_root_filesystem: true → read_only: true
capabilities.drop: ["ALL"] → cap_drop: [ALL]
allow_privilege_escalation: false → security_opt: ["no-new-privileges:true"]
run_as_user / runs_as_root (lib.rs:160,165) → user:
#263 and #264 both reproduce locally under those four lines and under nothing weaker.
Known non-equivalences — document them, don't paper over them
- Volume ownership. k8s sets
fsGroup from run_as_user so a fresh PVC is writable by a non-root container. Docker has no equivalent; a fresh named volume is root-owned 0755 and a non-root service cannot write to it. Expect a class of failure that appears locally but not in the cluster. State the workaround in the docs rather than making the transform quietly chown.
- No ingress, TLS or cert-manager:
${HOSTNAME} is whatever --hostname says.
- No
Recreate / RWO PVC semantics, no scheduler, no capacity accounting.
A green docker compose up means the image starts and the app's own startup checks pass under our security context. It is not a deployment test.
Docs
docs/managed-app-examples.md: new section after "Validating a compose document" (master line 250) — validate, transform, docker compose up, and the non-equivalences above.
docs/agents/build-and-test.md: add the flow as the expected step before opening a PR that touches a catalog compose.
Acceptance
Filed by Alejandra (PM) on Kieran's account.
Problem
Four catalog apps have been found unable to start in the last day — #248 (HAVEN missing 11 required env vars), #256 (three apps with no
user:underrunAsNonRoot), #263 (user: root+cap_drop: ALLbreaks themariadbentrypoint chown), #264 (read-only root filesystem with no writable scratch). Every one was found by reading Rust and comparing it against upstream image docs. None was found by starting the container, because there is no way to start one without a cluster.We have
compose-validate(lnvps_compose/src/bin/compose-validate.rs, master), and it is doing its job — it parses, validates and reports the footprint. But validation is a static check on the document. It cannot tell you that the image's entrypoint wants to write to/run, or that the app exits becauseRELAY_URLis empty. Those only surface when a container actually starts.Ask
A second bin in
lnvps_composethat transforms an lnvps-compose document into a standarddocker-compose.yaml, so an agent can rundocker compose uplocally and watch the app come up — or fail — before opening a PR.Design
Bin:
lnvps_compose/src/bin/compose-to-docker.rs, followingcompose-validate's conventions (path args or-for stdin, non-zero exit on failure).Resolution must go through the same code the operator uses, not a re-implementation:
resolve_config(lnvps_compose/src/lib.rs:1013) for submitted values ∪ declared defaults, with the same required-field error.Compose::config_defaults(lib.rs:481), then generated secrets, then supplied config, thenHOSTNAME— the ordering inbuild_vars(lnvps_operator/src/app_deployments.rs:827).Compose::resolve_env(lib.rs:781),resolve_files(lib.rs:799),resolve_init(lib.rs:740).ensure_secrets(app_deployments.rs:867), but written to the out-dir so a re-run of the same app is stable.Mapping:
app_deployments.rs:522), somariadb:3306resolves the same on docker's default networkimageimageenv(substituted)environmentvolumes[]pathfiles[]--out-dir, bind-mounted:roatpathsubPathports[].containerexpose; publish to the host only forexpose: ingressExpose(lib.rs:292)resources.cpu/.memorycpus/mem_limituseruserlib.rs:81depends_ondepends_oninit[](lib.rs:85)depends_on: {condition: service_completed_successfully}, tmpfs/tmpemptyDiratapp_deployments.rs:631The hardening is the point — emit it, do not skip it. A transform that produces a permissive docker-compose would have started all four broken apps cleanly and told us nothing.
container_security_context_for(app_deployments.rs:332-348) sets:read_only_root_filesystem: true→read_only: truecapabilities.drop: ["ALL"]→cap_drop: [ALL]allow_privilege_escalation: false→security_opt: ["no-new-privileges:true"]run_as_user/runs_as_root(lib.rs:160,165) →user:#263 and #264 both reproduce locally under those four lines and under nothing weaker.
Known non-equivalences — document them, don't paper over them
fsGroupfromrun_as_userso a fresh PVC is writable by a non-root container. Docker has no equivalent; a fresh named volume is root-owned0755and a non-root service cannot write to it. Expect a class of failure that appears locally but not in the cluster. State the workaround in the docs rather than making the transform quietlychown.${HOSTNAME}is whatever--hostnamesays.Recreate/ RWO PVC semantics, no scheduler, no capacity accounting.A green
docker compose upmeans the image starts and the app's own startup checks pass under our security context. It is not a deployment test.Docs
docs/managed-app-examples.md: new section after "Validating a compose document" (master line 250) — validate, transform,docker compose up, and the non-equivalences above.docs/agents/build-and-test.md: add the flow as the expected step before opening a PR that touches a catalog compose.Acceptance
compose-to-dockeremits adocker compose config-valid document for every app indocs/managed-app-examples.md.read_only,cap_drop,security_optandusermatchingcontainer_security_context_for.docker compose up— the tool reproduces a bug we already know is real.compose-validatetests.Filed by Alejandra (PM) on Kieran's account.