pull once.
serve forever.
Stashito is a small pull-through cache that sits between your machines and Docker Hub, GHCR, Quay — any OCI registry. The first pull comes from upstream and every layer lands on your disk. After that, pulls never leave your network.
free · AGPL-3.0 open source · single Go binary · buy me a coffee
#the same layers, over and over
Every CI job, every homelab node, every rebuilt VM re-downloads the same base images. Wasted bandwidth, slower builds. Stashito fetches each image once and keeps it.
Rate limits stop hurting
Docker Hub caps anonymous pulls. Behind Stashito, upstream only ever sees one pull per image, however often your machines ask.
LAN speed, not WAN speed
Cached layers come off local disk over your own network. Pulls move at whatever speed your LAN moves, not your uplink.
Survives a dead internet
Images you've pulled before keep working when your uplink or the registry doesn't. Your deploys stop depending on someone else's uptime.
Own your supply chain
An image that vanishes upstream (a yanked tag, a deleted repo, a left-pad moment) keeps deploying from your cache. What you pulled stays yours.
#a registry that remembers
Stashito speaks the OCI Distribution Spec, the same HTTP API every registry speaks. Docker, containerd and podman talk to it unchanged; Stashito only talks to upstream when it has to.
Every stage of an image is cached on disk. A pull asks for a manifest; the manifest lists layers; the layers are blobs. Digests and blobs are immutable, so once cached they're never re-fetched.
Tags are the one thing that move. Within TAG_TTL a tag is served
straight from cache; after it expires, the next request checks upstream's
digest and only fetches what actually changed.
#two minutes to a warm cache
One container, a volume, and at least one upstream. If something's missing or mistyped, Stashito tells you at startup instead of guessing.
01 start stashito
# docker-compose.yml services: stashito: image: rcm7/stashito environment: PORT: "8080" STORAGE_PATH: "/data/stashito" LOG_LEVEL: "info" LOG_FORMAT: "text" TAG_TTL: "60s" UPSTREAM_DOCKERHUB_HOST: "registry-1.docker.io" volumes: - stashito_data:/data/stashito ports: - "8080:8080" volumes: stashito_data:
02 pull through it
# prefix the image path with the upstream docker pull localhost:8080/dockerhub/library/postgres:16 docker pull localhost:8080/dockerhub/library/redis:7
The first segment names an upstream you configured:
dockerhub here maps to UPSTREAM_DOCKERHUB_HOST.
Everything after it is the ordinary image path. The first pull fills the
cache, and every later pull, from any machine that can reach Stashito,
stays local.
03 verify
curl -f http://localhost:8080/healthz
Returns configured upstreams and storage health as JSON. Then pull the same image twice: the first is a MISS that fills the cache, the second a HIT off your own disk.
From here the docs cover private registries, containerd and podman clients, metrics and troubleshooting.
#a handful of variables. that's it.
Everything is explicit — no hidden defaults to discover the hard way. Metrics are the one opt-in: off unless you switch them on.
- PORT
- HTTP port Stashito listens on.
- STORAGE_PATH
- Directory where cached manifests and blobs live. Point it at a volume you're happy to let grow.
- LOG_LEVEL
debug,info,warnorerror.- LOG_FORMAT
textorjson— structured logs your tooling (or your agent) can parse.- TAG_TTL
- How long a cached tag is served without checking upstream for a newer
digest. Go duration, e.g.
60s. Digests and blobs are immutable and never revalidated. - UPSTREAM_<ALIAS>_HOST
- One per upstream registry. The lowercased alias becomes the image-path
prefix. Add
_USERNAME/_PASSWORDfor private registries — GHCR, Quay, GAR and ACR all work. - METRICS_ENABLED
- Optional.
trueexposes Prometheus metrics atGET /metrics— cache hits and misses, upstream requests, HTTP latency, storage size. A ready-made Grafana dashboard ships in the repo. - METRICS_PORT
- Optional. Serve
/metricson a separate port instead of the main one — keep it off the port you expose to Docker.