pyhusk traces what each service actually imports — including the Depends() chains its routes reach — and builds an image with only that.
pyhusk init · pyhusk build · pyhusk plan --json
Three steps, all automatic. No Dockerfiles to hand-maintain per service.
Imports the service's app, walks its routes and their Depends() trees to find every module a request can actually reach.
Follows the import graph from there, then resolves the transitive package closure from your lockfile — nothing unreferenced comes along.
Builds the image, boots it, and checks its OpenAPI schema (or a healthcheck) before calling the build done.
A two-service monorepo. One shared module, one service that doesn't use it.
The parts that are easy to get wrong by hand.
A route can reach code only through Depends(). pyhusk walks that tree at introspection time, not just the static AST.
Content-hash staleness, stamped as a registry label so it survives ephemeral CI runners — not a local file that resets every run.
Services with identical dependencies build one base image once, deterministically, regardless of which other services are in the same run.
Schema comparison when OpenAPI is on, a healthcheck poll when it isn't, boot-only as the last resort — and a failed check untags the image so it's retried, not silently kept.
pyhusk plan --json reports which services are stale, so a matrix job builds only those — no bespoke scripting.
--include forces a path into the slice; --compare builds a whole-repo image alongside it so you can see whether pruning actually helped.
Python 3.12+, a uv-managed monorepo, and Docker.
# from your monorepo root uv tool install pyhusk pyhusk init # writes pyhusk.yaml from detected services pyhusk list pyhusk build # builds every stale service