Image search-and-replace — Compose
A compose component whose compose.yaml is runnable locally as-is against a dev-local image.
margot build swaps that dev-local reference for the target registry image — no manual editing,
no drift between what you run locally and what gets pushed.
Project tree
myapp-compose/
├── margo.yaml
├── margo/
│ └── app.yaml.jinja
└── compose/
└── compose.yaml
Before: run it locally
Build the dev-local image and run the stack exactly as checked in:
podman build -t localhost/myapp:dev .
docker compose -f compose/compose.yaml up
compose/compose.yaml
services:
myapp:
image: localhost/myapp:dev
ports:
- "8080:8080"
localhost/myapp:dev is a literal, runnable image reference — not a placeholder. This is the contract:
source files under compose/ must work locally without any margot involvement.
margo.yaml
apiVersion: v1
id: com-example-myapp
name: myapp
version: "1.0.0"
appVersion: "2.3.1"
description: "myapp deployed via Compose"
repository: public.ecr.aws/g2n4p2m7/margo
compose:
version: 1.0.0
image:
search: "localhost/myapp:dev"
replace: "public.ecr.aws/g2n4p2m7/myapp:{{ manifest.appVersion }}"
search— the literal string as it appears incompose/compose.yaml.replace— a Jinja2 template rendered from the same template context used forapp.yaml.jinja.{{ manifest.appVersion }}resolves to2.3.1.
Because replace is a template rather than a static string, bumping appVersion in margo.yaml
is the only edit needed to point every artifact at the new image tag — the image block itself
never needs to change.
Build
margot build
margot copies compose/ to a temp dir, substitutes manifest placeholders, renders and applies the
image search-and-replace, and writes the tarball to .dist/:
.dist/
└── 1.0.0/
└── myapp-1.0.0.tgz
After: what's inside .dist
The archived compose.yaml inside myapp-1.0.0.tgz — note localhost/myapp:dev is gone:
services:
myapp:
image: public.ecr.aws/g2n4p2m7/myapp:2.3.1
ports:
- "8080:8080"
The source file on disk (compose/compose.yaml) is untouched — the substitution happens only in
the temp copy that gets packaged. Your local dev loop (podman build + docker compose up
against localhost/myapp:dev) keeps working after margot build runs.
Push
margot push reads the same .dist/ layout margot build produced — no separate step needed:
margot push
This pushes the tarball to public.ecr.aws/g2n4p2m7/margo:1.0.0 with artifact type
application/vnd.org.margo.component.compose+json.
Bumping the version — no image edits needed
Release 2.0.0 of the deployed application requires touching only appVersion:
appVersion: "2.4.0"
Re-running margot build renders replace again with the new context — the packaged
compose.yaml now points at public.ecr.aws/g2n4p2m7/myapp:2.4.0. The image.search /
image.replace block in margo.yaml is unchanged.
Per-variant override
If the component declares variants, a variant's image block fully replaces the
component-level one — it does not merge:
compose:
version: 1.0.0
image:
search: "localhost/myapp:dev"
replace: "public.ecr.aws/g2n4p2m7/myapp:{{ manifest.appVersion }}"
variants:
- name: default # inherits the block above
- name: addon-mosquitto
image: # replaces it entirely for this variant
search: "localhost/mosquitto:dev"
replace: "public.ecr.aws/g2n4p2m7/mosquitto:{{ manifest.appVersion }}"
Building --variant addon-mosquitto searches compose/addon-mosquitto/ for localhost/mosquitto:dev
only — the localhost/myapp:dev search from the component level does not apply to that variant.
Note
An unmatched search string (declared but not found in any source file) produces a warning,
not a build failure — the same posture as other unresolved placeholders. An undefined Jinja
variable in replace, however, is a hard error at build time.