Docker Proxy
NORA can act as a pull-through cache for upstream Docker registries. When an image is not found locally, NORA fetches it from the configured upstream, caches it, and serves it to the client. Subsequent pulls are served from cache.
How it Works
Section titled “How it Works”docker pull nora:4000/library/nginx:latest │ ▼ ┌──────┐ cache hit ┌─────────┐ │ NORA │ ───────────────► │ storage │ └──┬───┘ └─────────┘ │ cache miss ▼ ┌────────────┐ fetch + async cache │ upstream │ ──────────────────────► storage │ (Docker Hub)│ └────────────┘- Client requests an image from NORA
- NORA checks local storage — if found, returns immediately (cache hit)
- If not found, tries each configured upstream sequentially
- First successful response is returned to the client
- Image is cached asynchronously in the background (fire-and-forget)
- Next pull of the same image is served from cache
Configuration
Section titled “Configuration”Two environment variables control the proxy behavior:
| Variable | Default | Description |
|---|---|---|
NORA_DOCKER_PROXIES | https://registry-1.docker.io | Comma-separated list of upstream registries |
Note:
NORA_DOCKER_UPSTREAMSis deprecated and works as a backward-compatible alias. UseNORA_DOCKER_PROXIES.
| NORA_DOCKER_PROXY_TIMEOUT | 300 | HTTP timeout for upstream requests (seconds) |
Single upstream (Docker Hub)
Section titled “Single upstream (Docker Hub)”Default — no configuration needed. NORA proxies to Docker Hub out of the box:
docker pull nora.internal:4000/library/nginx:latestMultiple upstreams with fallback
Section titled “Multiple upstreams with fallback”NORA tries upstreams in order. If Docker Hub is down or doesn’t have the image, it falls back to the next:
NORA_DOCKER_PROXIES="https://registry-1.docker.io,https://ghcr.io,https://quay.io"config.toml
Section titled “config.toml”[docker]proxy_timeout = 60
[[docker.upstreams]]url = "https://registry-1.docker.io"
[[docker.upstreams]]url = "https://ghcr.io"Docker Compose Example
Section titled “Docker Compose Example”services: nora: image: ghcr.io/getnora-io/nora:latest ports: - 4000:4000 volumes: - nora-data:/data environment: - NORA_HOST=0.0.0.0 - NORA_DOCKER_PROXIES=https://registry-1.docker.io,https://ghcr.io - NORA_DOCKER_PROXY_TIMEOUT=30 restart: unless-stopped
volumes: nora-data:Usage Examples
Section titled “Usage Examples”Pull from Docker Hub via NORA
Section titled “Pull from Docker Hub via NORA”# Instead of: docker pull nginx:latestdocker pull nora.internal:4000/library/nginx:latest
# Instead of: docker pull redis:7-alpinedocker pull nora.internal:4000/library/redis:7-alpine
# Non-library images (user repos):docker pull nora.internal:4000/grafana/grafana:latestPull from GHCR via NORA
Section titled “Pull from GHCR via NORA”If GHCR is in your upstreams list and the image is not on Docker Hub:
docker pull nora.internal:4000/getnora-io/nora:latestUse as a Docker mirror
Section titled “Use as a Docker mirror”Configure Docker daemon to use NORA as a registry mirror. Edit /etc/docker/daemon.json:
{ "registry-mirrors": ["http://nora.internal:4000"]}Restart Docker:
sudo systemctl restart dockerNow all docker pull commands automatically go through NORA:
# This now pulls via NORA cache:docker pull nginx:latestAuthentication
Section titled “Authentication”Public registries (no config needed)
Section titled “Public registries (no config needed)”For public images, NORA handles authentication automatically:
- Sends request to upstream
- On
401 Unauthorized, extractsWww-Authenticateheader - Fetches a bearer token from the upstream’s auth service
- Retries with the token
- Tokens are cached for 5 minutes per registry/repository
Docker Hub, GHCR, and Quay all support anonymous pulls for public repositories.
Private registries (Basic Auth)
Section titled “Private registries (Basic Auth)”For private upstream registries that require credentials, use the url|user:pass format:
NORA_DOCKER_PROXIES="https://registry.corp.com|admin:secret,https://registry-1.docker.io"Or in config.toml:
[[docker.upstreams]]url = "https://registry.corp.com"auth = "admin:secret"
[[docker.upstreams]]url = "https://registry-1.docker.io"When credentials are configured, NORA sends Basic Auth on the initial request (preemptive auth) and also uses them for bearer token acquisition.
Supported Upstreams
Section titled “Supported Upstreams”Any Docker Registry v2 API-compatible registry works:
| Registry | URL |
|---|---|
| Docker Hub | https://registry-1.docker.io |
| GitHub Container Registry | https://ghcr.io |
| Quay.io | https://quay.io |
| GitLab Container Registry | https://registry.gitlab.com |
| Google Artifact Registry | https://REGION-docker.pkg.dev |
| AWS ECR | https://ACCOUNT.dkr.ecr.REGION.amazonaws.com |
Monitoring
Section titled “Monitoring”NORA tracks cache performance via Prometheus metrics at /metrics:
nora_cache_requests_total{result="hit"}— images served from local cachenora_cache_requests_total{result="miss"}— images fetched from upstreamnora_http_requests_total{registry="docker"}— total Docker HTTP operations
Cache hit rate is also displayed on the Web UI dashboard.
Metadata TTL
Section titled “Metadata TTL”Controls how long cached manifests and tag lists are considered fresh before NORA rechecks the upstream.
| Value | Behavior |
|---|---|
-1 (default) | Cache forever — never refetch from upstream |
0 | Always refetch — check upstream on every request |
> 0 | Cache for N seconds, then refetch |
export NORA_DOCKER_METADATA_TTL=300 # 5 minutes[docker]metadata_ttl = 300When to use each mode:
-1(forever): Air-gapped environments, or when you only want images that are explicitly pushed to NORA.0(always refetch): Development environments where you need the latest upstream tags immediately.300-3600(seconds): Production proxy mode — balance freshness with upstream rate limits.
Stale-While-Error
Section titled “Stale-While-Error”When stale_while_error = true (default), NORA serves expired cached manifests if the upstream is unreachable, rather than returning an error to the client.
[docker]stale_while_error = true # defaultThis provides resilience against upstream outages — your builds continue working from cache even if Docker Hub is down.
Namespace Isolation
Section titled “Namespace Isolation”When proxying multiple upstream registries, NORA isolates cached images by namespace to avoid collisions. Each upstream gets a namespace derived from its hostname:
| Upstream URL | Derived Namespace |
|---|---|
https://registry-1.docker.io | docker.io |
https://ghcr.io | ghcr.io |
https://quay.io | quay.io |
Images pushed directly to NORA (not from an upstream) are stored without a namespace prefix.
Custom Namespace
Section titled “Custom Namespace”Override the derived namespace with an explicit value:
[[docker.upstreams]]url = "https://registry-1.docker.io"namespace = "dockerhub" # custom prefix instead of "docker.io"Storage Layout
Section titled “Storage Layout”data/storage/docker/ manifests/ myapp/v1.json # locally-pushed image docker.io/library/nginx/latest.json # proxied from Docker Hub ghcr.io/org/tool/v2.json # proxied from GHCRThis prevents tag collisions when different upstreams have images with the same name.
Streaming Read Timeout
Section titled “Streaming Read Timeout”For large blob downloads (multi-GB ML model layers), NORA uses chunked streaming from upstreams. The read_timeout controls the per-chunk timeout, not the total transfer time:
export NORA_DOCKER_READ_TIMEOUT=60 # seconds per chunk (default)[docker]read_timeout = 60If a single chunk takes longer than this timeout, the transfer is aborted. For slow or high-latency upstreams, increase this value:
[docker]read_timeout = 120 # 2 minutes per chunk for slow upstreamsCircuit Breaker
Section titled “Circuit Breaker”The circuit breaker prevents cascading failures when an upstream registry is down. Instead of waiting for every request to timeout, NORA “opens the circuit” after consecutive failures and returns 503 immediately.
[circuit_breaker]enabled = truefailure_threshold = 5 # failures before openingreset_timeout = 30 # seconds before trying upstream againexport NORA_CB_ENABLED=trueexport NORA_CB_THRESHOLD=5export NORA_CB_RESET_TIMEOUT=30States
Section titled “States”| State | Behavior |
|---|---|
| Closed | Normal operation, requests go to upstream |
| Open | Upstream is down — returns 503 with Retry-After header |
| Half-Open | After reset_timeout, allows one probe request through |
Per-Registry Overrides
Section titled “Per-Registry Overrides”Different upstreams may have different reliability profiles. Override thresholds per registry:
[circuit_breaker]enabled = truefailure_threshold = 5reset_timeout = 30
[circuit_breaker.overrides."docker:https://registry-1.docker.io"]failure_threshold = 10 # Docker Hub is flaky, be more patientreset_timeout = 60
[circuit_breaker.overrides."docker:https://internal.registry.corp"]failure_threshold = 2 # internal -- fail fast if it's downreset_timeout = 10The override key format is {registry_type}:{upstream_url}.
Limitations
Section titled “Limitations”- Pull only — proxy is read-only. Push goes directly to NORA’s local storage, not forwarded upstream.
- Sequential fallback — upstreams are tried one by one, not in parallel. If the first upstream times out, latency adds up.
- Async caching — cache writes happen in the background. If NORA restarts during caching, the image won’t be cached and will be fetched again on next pull.
See Also
Section titled “See Also”- Settings — all configuration options
- TLS / HTTPS — reverse proxy setup
- Production Guide — deployment best practices