New to Kestra?
Use blueprints to kickstart your first workflows.
Audit Docker Model Runner for stale local AI models, alert Slack on breach, and optionally delete them to reclaim disk space.
Docker Model Runner lets a developer or CI machine pull local AI models the same way it pulls container images - and those models behave the same way stale images do: nobody deletes them, and they are multi-gigabyte files, not a few megabytes. A machine that has pulled a dozen embedding and language models over a few months of experimentation can be carrying tens of gigabytes nobody is using anymore. This blueprint lists every model cached on a DMR instance, flags any older than a configured age, alerts Slack on breach, and can delete the stale ones to reclaim disk space.
periodic_model_check (io.kestra.plugin.core.trigger.Schedule) runs daily, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.list_local_models (io.kestra.plugin.docker.model.List) fetches every cached model, each carrying id, tags, created (Unix epoch seconds), and config.{format,quantization,parameters,architecture,size} - verified directly from this plugin's own Java source, not just its doc page.evaluate_model_age (io.kestra.plugin.scripts.python.Script) compares each model's created timestamp against max_model_age_days, collecting the stale ones into stale_models with their tag, creation time, and on-disk size.evaluate_model_gate (io.kestra.plugin.core.flow.If) flags a breach whenever stale_count > max_stale_models.then (breach): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, delete_stale_models (io.kestra.plugin.core.flow.Loop) runs delete_stale_model (io.kestra.plugin.docker.model.Delete) for each flagged model. alert_stale_models_risk always fires to Slack regardless of which path ran.else (compliant): log_models_healthy records a clean check.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails - an unreachable DMR instance must not read as "no breach".auto_remediate, dry_run) between "stale model found" and "model actually deleted".List and Delete used together as a governance pair, mirroring this repo's existing docker-host-image-retention-reclaim.yaml pattern for container images.Docker Model Runner itself has no retention policy, no age-based cleanup, and no notification path - a model sits on disk until someone runs docker model rm by hand. Kestra supplies the schedule, the age-based risk classification as a first-class branch, two independent authorization gates before anything is deleted, and an execution history that shows exactly which models were reclaimed and when.
dmr_host.Local testing:
Docker Model Runner ships as part of Docker Desktop/Engine itself (version 4.40+), not as a separate container - enable it via docker desktop enable model-runner or the Docker Desktop settings, then pull at least one test model with docker model pull ai/smollm2 so there is something to audit.
docker network connect YOUR_KESTRA_NETWORK <container> does not apply here since DMR is not itself a container with a name to connect to; if the Kestra Worker runs inside a container, ensure it can reach the host's DMR port (12434 by default) via host.docker.internal:12434 or equivalent, rather than localhost.
SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_stale_models_risk and the errors block.dmr_host carries no credentials, matching Docker Model Runner's own unauthenticated local-only REST API.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_SLACK_WEBHOOK_URL=$(echo -n 'https://hooks.slack.com/...' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.dmr_host (STRING, default http://localhost:12434): Docker Model Runner REST API base URL.max_model_age_days (INT, default 30): age threshold for the stale-model check.max_stale_models (INT, default 0): breach threshold; 0 means any stale model at all breaches.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for deletion to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for a stale model to actually be deleted.outputs.list_local_models.models: the raw model listing (id, tags, created, config) for every run.outputs.evaluate_model_age.vars.stale_count / .stale_models: the computed policy result for every run.docker model pull ai/smollm2); if you need a guaranteed-stale entry immediately, lower max_model_age_days to 0 for the first test run instead of waiting for a real model to age.SLACK_WEBHOOK_URL secret.auto_remediate: "false") and confirm alert_stale_models_risk fires naming the expected model, and that log_remediation_disabled logs without deleting anything.auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the deletion without running it.auto_remediate: "true" and dry_run: "false" and confirm with docker model ls that the flagged model is gone.periodic_model_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.evaluate_model_age to also flag models above a total disk-size budget across the whole host, by summing config.size after parsing its human-readable units (e.g. 260.86MiB).dmr_host values with io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to sweep a fleet of CI runners or developer machines, each reporting to the same Slack channel.evaluate_model_age.Pause approval task instead of (or alongside) the static auto_remediate input, for a human-reviewed deletion on a shared CI runner.host, models, id, tags, created, config.{format,quantization,parameters,architecture,size}, and Delete's model property) were verified directly against the plugin's Java source (List.java, Delete.java, AbstractModel.java in kestra-io/plugin-docker), not from a doc page, since the doc pages only described these fields in prose.config.size is a human-readable string (e.g. 260.86MiB), not a byte count. Verified from the plugin's own source; summing or comparing sizes numerically requires parsing this string first, which this blueprint's age-only check does not need to do.Delete's model property expects a reference like ai/smollm2 or hf.co/org/repo, not the raw id content digest. This blueprint passes the first entry of each flagged model's tags array rather than its id, matching the plugin's own documented identifier format; a model with no tags at all would need its id instead, which this blueprint does not currently fall back to.Delete itself. The double gate (auto_remediate, dry_run) in this flow is what makes deletion safe to test; Delete itself deletes immediately and permanently once called.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for the delete Loop to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.