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

stashito :8080 — layer store
hits 1,204 upstream pulls 37 on disk 4.2 GB demo feed

#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, warn or error.
LOG_FORMAT
text or json — 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 / _PASSWORD for private registries — GHCR, Quay, GAR and ACR all work.
METRICS_ENABLED
Optional. true exposes Prometheus metrics at GET /metrics — cache hits and misses, upstream requests, HTTP latency, storage size. A ready-made Grafana dashboard ships in the repo.
METRICS_PORT
Optional. Serve /metrics on a separate port instead of the main one — keep it off the port you expose to Docker.