Deploy a Java + React app from its docker-compose.yml
Deploys an existing containerized app to Kubernetes with Harmony by
importing its docker-compose.yml — no hand-written manifests, no Argo.
The compose file stays the source of truth for the app's base shape;
deploy-only concerns are typed Rust.
app/ the "customer" project (their existing repo)
docker-compose.yml ← source of truth: images, ports, env, volumes
backend/ (Java + SQLite, Dockerfile)
frontend/ (React + nginx, Dockerfile)
Harmony.toml identity only — the app name (ADR-026 §3)
.harmony/contexts.toml deploy targets (local / prod), checked in
src/
compose.rs import docker-compose → typed model (loud on the unsupported)
chart.rs model + profile knobs → a hydrated helm chart (typed k8s)
publish.rs build images + (push to a registry | import to k3d)
score.rs ComposeAppScore: helm upgrade --install + Ingress
deploy.rs ComposeDeploy — the declarative builder (impls HarmonyApp)
main.rs the whole app, declared
The app, declared (main.rs)
let app = ComposeDeploy::from_dir("timesheet", "./app")?
.expose("frontend", "timesheet.example.harmony.mcd")
.with(Postgres::managed()); // a managed CNPG database, wired in
harmony_cli::app::app_main(app).await
That declaration gets you ship / deploy / status / logs over any
--context, all from harmony_app. See
Application CLI and
Application Capabilities.
Design
- compose = base, profile = deploy knobs. The importer reads only images,
ports, env, volumes. Replicas, storage class, TLS, rolling strategy come
from the
Profilethe context carries (ADR-026 §6/§7), not from compose orHarmony.toml. A behavioral knob in either is a defect. - Capabilities compose upward.
.with(Postgres::managed())/.with(Monitoring::new()...)/.with(ZitadelAuth::oidc(...))each deploy their own Scores and wire into the app by reference (a DB URL viasecretKeyRef, a client_id viaconfigMapKeyRef) — noApplicationScore, no ArgoCD. - Same definition, local → prod. Only the context changes; the same
ComposeAppScoreconverges on local k3d or a tenant cluster. - Loud, never lossy. Bind mounts and unparseable ports are hard errors;
depends_on/command/restartare warned, never silently dropped.
Run it (local k3d)
k3d cluster create compose-local --servers 1 --wait
kubectl --context k3d-compose-local create namespace timesheet
cd examples/compose_java_react # so the CLI finds .harmony/contexts.toml
cargo run --bin compose-deploy -- ship --context local # build → k3d import → deploy (+ Postgres)
cargo run --bin compose-deploy -- status --context local
cargo run --bin compose-deploy -- logs --context local --tail 50
Running from elsewhere? Point at the contexts file with
--config <path>/.harmony/contexts.toml (a context is always required — there
is no default, so you never hit the wrong cluster).
Production uses the same verbs against a prod context, with cluster credentials brokered from OpenBao.
Storage note (SQLite)
Profile::Local deploys single-replica with Recreate on a ReadWriteOnce
volume — safe for the demo's single-writer SQLite (one pod at a time). Prod
(Profile::Prod) replicates with RollingUpdate on ReadWriteMany; for a
real multi-writer store use .with(Postgres::managed()) (the demo already
provisions it) and point the app at DATABASE_URL.