A provider managing containers on a remote or non-Docker runtime has always been able to declare build: or image: on its service, but getting the actual image bytes to wherever that runtime lives was left entirely to the provider: push to a registry it can reach, or shell out to docker save/scp and hope it matches what Compose actually resolved. Tip #61 covered provider services for exactly this kind of external dependency: databases, tunnels, Kubernetes intercepts. Compose 5.6.0 adds the piece that tip didn’t have yet: a provider can now opt into the same image phase every other service goes through.
The opt-in
A provider declares support by including a pull block in its metadata output, even an empty one:
{
"up": { "parameters": [] },
"pull": { "parameters": [] }
}
From then on, Compose invokes the provider during up’s image phase (after any build) and on docker compose pull:
my-provider compose --project-name myapp pull \
--image=myapp-worker --source=local --policy=missing \
--digest=sha256:abc123... --created=2026-10-01T12:00:00Z \
worker
--sourceis the authority Compose already computed:localmeans the local daemon’s image is the desired state (a build-only service, or one this run just built);registrymeans resolve the reference upstream instead. The provider never has to re-implement that arbitration.--policyismissingonup(any usable version will do) oralwaysondocker compose pull(verify freshness).--digest/--createddescribe the local daemon’s cache, present only when it exists there. They’re bookkeeping, not instructions: the provider compares them against what it already ingested.
Requesting the bytes
When the provider actually needs the image, it asks for it over stdout:
{"type": "get-image", "message": "myapp-worker"}
Compose answers on stdin with a JSON line announcing the stream ({"type": "image-stream", "encoding": "chunked", "media-type": "application/x-tar"}), then the chunked tar body itself, the same format docker image save produces. No registry round-trip required if the image only exists locally.
Why it matters
This is opt-in and additive. A provider without a pull block keeps managing images exactly as before, and regular (non-provider) services are untouched either way. For the providers that do opt in, the win is one less decision to get wrong by hand: the local-vs-registry call and the freshness policy are Compose’s job now, computed once and handed over as --source/--policy instead of being re-derived by every plugin author from scratch.
Pro tip: one provider’s pull failure doesn’t take down the others
Provider pulls run independently rather than under fail-fast cancellation. If your stack has three provider-backed services and one of them fails its pull, the other two still run to completion and report their own result, instead of being cut off mid-run and surfacing a confusing context canceled in their place. Worth knowing before you go looking for a second bug that isn’t actually there.