Files
harmony/examples/compose_java_react

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 Profile the context carries (ADR-026 §6/§7), not from compose or Harmony.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 via secretKeyRef, a client_id via configMapKeyRef) — no ApplicationScore, no ArgoCD.
  • Same definition, local → prod. Only the context changes; the same ComposeAppScore converges on local k3d or a tenant cluster.
  • Loud, never lossy. Bind mounts and unparseable ports are hard errors; depends_on/command/restart are 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.