# distroless-mirror Public, read-only OCI mirror of the four Google distroless images, served from the VPS on : | Pull this | Equals upstream | | --- | --- | | `img.dieschoenewolke.de/distroless/static:` | `gcr.io/distroless/static:` | | `img.dieschoenewolke.de/distroless/base:` | `gcr.io/distroless/base:` | | `img.dieschoenewolke.de/distroless/cc:` | `gcr.io/distroless/cc:` | | `img.dieschoenewolke.de/distroless/python3:` | `gcr.io/distroless/python3:` | `` is one of `latest`, `nonroot`, `debug`, `debug-nonroot` — the four multi-arch tags upstream publishes for every repo. Digest-identical mirrors of the upstream index; `linux/amd64` and `linux/arm64` children are mirrored, the rare architectures (arm, ppc64le, riscv64, s390x) are not. Why: the home Forgejo registry is VPN-only, so nothing public could pull these images. Like the cgit mirror (`git.dieschoenewolke.de`) this one lives on the VPS and keeps serving while the home instance is down. ``` docker pull img.dieschoenewolke.de/distroless/static:nonroot docker pull img.dieschoenewolke.de/distroless/python3@sha256: incus remote add img https://img.dieschoenewolke.de --protocol oci ``` ## Layout | Path | Purpose | | --- | --- | | `deploy/docker-compose.yml` | VPS stack (`/root/Compose/Webserver/Registry`), zot bound to `127.0.0.1:5000` | | `deploy/config.json` | zot config: anonymous read-only, sync from `gcr.io` | | `deploy/nginx-edge-img.conf` | Server block for `edge-nginx` | | `scripts/verify-mirror.sh` | Digest comparison + pull verification + marker tags | | `.forgejo/workflows/mirror-sync.yml` | Scheduled auditor (every 6 h) | ## Why zot and not `distribution/registry:2` + CI `skopeo copy` Both were on the table. `registry:2` would have made the Forgejo workflow the only writer (`skopeo copy --all` per changed digest) and would have needed a push credential plus an edge carve-out for that credential, or a second VPN-only push port. zot wins on three points that matter here: 1. **No push surface at all.** With no `auth` block and `anonymousPolicy: ["read"]`, zot has no path for an authenticated write — not from the internet, not from the LAN, not from a leaked token. With `registry:2`, read-only can only be enforced by the edge (`limit_except GET`) plus a separate authenticated push route; the write capability exists in the process. 2. **Freshness does not depend on the home host.** zot pulls from gcr.io on the VPS. A `registry:2` + CI design stops updating whenever the home Forgejo (or its runner) is down, because the copy job runs there. 3. **`dedupe`.** The four repos share most layers; zot hardlinks identical blobs instead of storing them per repository. `registry:2` stores blobs globally too, but has no tag-level `dedupe` and no built-in upstream sync, so a stale mirror is a CI bug rather than a config value. The cost of that choice: zot is a second non-standard registry binary to keep updated, and the tag allowlist lives in `deploy/config.json` rather than in the workflow. The auditor workflow covers the second half by failing when a ref we already marked disappears. ## Deploy ```sh ssh root@vpn mkdir -p /root/Compose/Webserver/Registry # copy deploy/docker-compose.yml, deploy/config.json cd /root/Compose/Webserver/Registry && docker compose up -d ``` Edge: merge `deploy/nginx-edge-img.conf` into `/root/Compose/Webserver/NginxEdge/nginx/default.conf` **in place** (`cat new > default.conf` — it is a single-file bind mount), then ```sh docker exec edge-nginx nginx -t docker exec edge-nginx nginx -s reload ``` Rollback: remove the block again (`git checkout`/`cp` the backup, write in place, reload) and `docker compose -p registry down` in the Registry directory. The `*.dieschoenewolke.de` wildcard certificate already covers `img`, so no certificate change is involved. DNS: `A 45.142.177.86` and `AAAA 2a03:4000:4a:2ba::1` for `img.dieschoenewolke.de` (deSEC). ## Verify ```sh curl -sI https://img.dieschoenewolke.de/v2/ # 200 docker pull img.dieschoenewolke.de/distroless/static:nonroot docker run --rm img.dieschoenewolke.de/distroless/python3:nonroot -V ./scripts/verify-mirror.sh # digest comparison + markers ``` From the VPS itself the request hits CrowdSec AppSec and can be challenged — test from another host. ## Notes - The edge disables AppSec for this host (docker/skopeo user agents trip CRS bot rules); the CrowdSec IP bouncer still applies and `limit_except GET` rejects every write verb at the edge. - `distribution/registry`-style `/v2/` on `git.dieschoenewolke.de` is not usable: OCI clients derive `/` from the reference, so the same hostname would need a path-stripping vhost and `GET /v2/` would collide with cgit. Hence the dedicated name.