Local Docker registry with async upstream sync
Push images at local disk speed, sync to your central registry in the background.
docker push localhost:5050/myapp:latest
│
▼
┌─────────┐ background sync ┌─────────────────┐
│Switchyard│ ─────────────────────► │ Central Registry │
│ (local) │ │ (self-hosted) │
│ :5050 │ ◄── reverse proxy ───── │ │
└─────────┘ (pulls, cached) └─────────────────┘
- Push lands on your local machine instantly
- Background worker syncs blobs and manifests to the central registry with retry and backoff
- Pull checks local storage first, proxies to upstream on miss and caches the result
- Layer deduplication: blobs that already exist upstream are never re-pushed
Copy compose.example.yaml to compose.yaml and fill in your upstream registry URL:
services:
switchyard:
image: ghcr.io/alltuner/switchyard:latest
ports:
- "5050:5050"
volumes:
- switchyard-data:/data
environment:
SWITCHYARD_UPSTREAM: "https://your-central-registry:5000"
restart: unless-stopped
volumes:
switchyard-data:docker compose up -duv sync
uv run switchyardAdd localhost:5050 as an insecure registry in ~/.docker/daemon.json:
{
"insecure-registries": ["localhost:5050"]
}Restart Docker Desktop (or the daemon) after making this change.
docker tag alpine:latest localhost:5050/alpine:latest
docker push localhost:5050/alpine:latestAll configuration is via environment variables with the SWITCHYARD_ prefix:
| Variable | Default | Description |
|---|---|---|
SWITCHYARD_DATA_DIR |
./data |
Directory for blobs, manifests, and sync queue |
SWITCHYARD_UPSTREAM |
(empty) | Central registry URL. Empty = local-only mode |
SWITCHYARD_SYNC_INTERVAL |
10 |
Seconds between sync queue scans |
SWITCHYARD_MANIFEST_TTL |
300 |
Seconds before re-fetching a cached tag manifest from upstream |
Pending syncs are stored as JSON files in $SWITCHYARD_DATA_DIR/pending/. You can inspect them with ls:
ls data/pending/
# myapp/latest.jsonFailed syncs retry with exponential backoff (5s, 10s, 20s, ... capped at 5 minutes). Delete a marker file to cancel a pending sync.
uv sync --all-groups
uv run pytest -v
uv run ruff check src/ tests/- Python 3.14+, async throughout
- Starlette for the HTTP layer
- Granian (Rust ASGI server) with uvloop
- httpx for upstream communication (HTTP/2, brotli, zstd)
- File-based storage and sync queue (no database)
Switchyard is an open source project built by David Poblador i Garcia through All Tuner Labs.
If this project is useful to you, consider supporting its development.
❤️ Sponsor development https://github.com/sponsors/alltuner
☕ One-time support https://buymeacoffee.com/alltuner
Your support helps fund the continued development of Switchyard and other open source developer tools such as Factory Floor.
Built by David Poblador i Garcia with the support of All Tuner Labs.
Made with ❤️ in Poblenou, Barcelona.