Schedule icon
List icon
Script icon
If icon
Switch icon
Loop icon
Delete icon
Log icon
SlackIncomingWebhook icon

Docker Model Runner Stale Model Sentinel

Audit Docker Model Runner for stale local AI models, alert Slack on breach, and optionally delete them to reclaim disk space.

Categories
AIInfrastructure

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.

How it works

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  6. The errors block alerts Slack separately if the flow itself fails - an unreachable DMR instance must not read as "no breach".

What you get

  • A concrete, named list of stale local models with their size and age, computed from Docker Model Runner's own metadata, not a guess from disk usage alone.
  • Remediation that only ever deletes the specific models flagged as stale; any model younger than the threshold is never touched.
  • Two independent gates (auto_remediate, dry_run) between "stale model found" and "model actually deleted".
  • A final status log every run, breach or not, so the execution history doubles as a local-model hygiene trend for the host.

Who it's for

  • Platform and MLOps teams managing developer workstations or CI runners that pull local models through Docker Model Runner, who need a routine disk-reclaim pass instead of someone noticing "docker is full" manually.
  • Teams evaluating multiple embedding or small language models locally who accumulate pulled-but-abandoned models across experiments.
  • Anyone evaluating this plugin who wants a worked example of List and Delete used together as a governance pair, mirroring this repo's existing docker-host-image-retention-reclaim.yaml pattern for container images.

Why orchestrate this with Kestra

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.

Prerequisites

  • Docker Desktop or Docker Engine with Model Runner enabled, reachable from the Kestra Worker at dmr_host.
  • A Slack incoming webhook for alerts.

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.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_stale_models_risk and the errors block.
  • This blueprint's dmr_host carries no credentials, matching Docker Model Runner's own unauthenticated local-only REST API.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed 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.

Inputs

  • 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

  • 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.

Quick start

  1. Enable Docker Model Runner and pull a test model (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.
  2. Add the SLACK_WEBHOOK_URL secret.
  3. Run the flow manually with the defaults (auto_remediate: "false") and confirm alert_stale_models_risk fires naming the expected model, and that log_remediation_disabled logs without deleting anything.
  4. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the deletion without running it.
  5. Run once with auto_remediate: "true" and dry_run: "false" and confirm with docker model ls that the flagged model is gone.
  6. Enable 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.

How to extend

  • Extend 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).
  • Loop over several 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.
  • Add an allowlist input of tags that should never be deleted regardless of age (a model pinned for a specific demo or benchmark), and exclude them in evaluate_model_age.
  • Route the remediation branch through a Pause approval task instead of (or alongside) the static auto_remediate input, for a human-reviewed deletion on a shared CI runner.

Pitfalls

  • This plugin's exact schema is not fully documented on its own doc pages - the property names used here (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.
  • There is no dry-run mode on 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.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.