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)
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 app and its compiled deploy contexts
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
let contexts = ContextCatalog::new([Context {
name: context_name!("local"),
namespace: "timesheet".parse()?,
spec: ContextSpec::Local(LocalContext::ManagedK3d),
}])?;
harmony_cli::app::app_main(app, contexts).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)
cd examples/compose_java_react
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
The binary accepts only contexts compiled into main.rs. A context is always
required, so omitting --context and HARMONY_CONTEXT is an error.
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.