mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
The ubuntu CI image build (job 103277644636) hung on Canonical's archive: apt fetched 26 MB at 417 kB/s, then 61.6 kB in 29 s, then sat on the package download until the job was cancelled after 23 minutes, while download.docker.com delivered 42.8 MB in under a second on the same runner. Locally archive.ubuntu.com and security.ubuntu.com took 20-26 s to the first byte against 2 s for a university mirror. INFINITO_APT_UBUNTU_MIRRORS in default.env is now the one list of Ubuntu mirrors, in failover order: de.archive.ubuntu.com, ftp.fau.de, ftp.halifax.rwth-aachen.de, archive.ubuntu.com. - Image builds: the Dockerfile sources the list alongside the install scripts and runs scripts/install/apt-mirrors.sh first. In an image with ubuntu.sources it points both stanzas at apt's mirror+file method, priorities in list order, and sets Acquire::http::Timeout to 10 s, so apt moves on from a mirror that fails or goes silent; other distros are untouched. In ubuntu:latest apt-get update took 8 s against 110 s on the stock sources, a dead first mirror was left behind within 25 s against 62 s on apt's defaults, and the swarm node packages (80 debs) came in 17 s. The Dockerfile's own RUN lines ran clean on ubuntu, debian, fedora and arch. - Package cache: package.sh proxies the first entry as the primary Ubuntu repos and the second as their fallback. Nexus revalidates metadata on every request here (metadataMaxAge 0), which costs a full download from a mirror without an ETag; both de.archive and ftp.fau.de send one, and the 15 MB universe index now comes back from the cache in 0.02 s. - tests/external/repository/test_apt_ubuntu_mirrors.py keeps the list honest: the first mirror must answer InRelease within 5 s (median of three) or the test names a faster later one, no mirror's -updates may lag the freshest by more than 24 h, and every entry must be the primary archive, a Canonical country alias or an enabled official mirror in Launchpad's registry. It caught the first draft of the list: mirrors.edge.kernel.org is not an official Ubuntu mirror. After pulling, an existing checkout needs `make dotenv-force`: plain `make dotenv` re-reads the old .env through BASH_ENV and keeps its values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
11 KiB
Markdown
128 lines
11 KiB
Markdown
# Local Caches 📦
|
||
|
||
Three local cache services accelerate re-deploys, reduce upstream traffic, and harden the development workflow against transient upstream outages. They share one compose profile (`cache`) and one override file ([compose/cache.override.yml](../../../compose/cache.override.yml)) and are gated together by [profile.py](../../../cli/administration/deploy/development/profile.py): active on developer machines, inactive on CI runners.
|
||
|
||
## Services 🧩
|
||
|
||
### Registry Cache 🐳
|
||
|
||
[`registry-cache`](../../../compose/registry-cache/README.md) runs `rpardini/docker-registry-proxy` and transparently MITMs every Docker image pull from the inner `dockerd`, regardless of upstream registry (`docker.io`, `ghcr.io`, `mcr.microsoft.com`, …). The runner trusts the proxy CA via [registry-ca.sh](../../../scripts/docker/cache/registry-ca.sh).
|
||
|
||
Pulls only. Pushes are not intercepted.
|
||
|
||
### Package Cache 📚
|
||
|
||
[`package-cache`](../../../compose/package-cache/README.md) runs `sonatype/nexus3` and exposes pull-through proxies for package-manager downloads:
|
||
|
||
| Format | Repo names | Upstream |
|
||
|---|---|---|
|
||
| `apt` | `apt-debian`, `apt-debian-security`, `apt-ubuntu`, `apt-ubuntu-security` | `deb.debian.org`; both Ubuntu repos take the first entry of `INFINITO_APT_UBUNTU_MIRRORS` |
|
||
| `apt` (mirrors) | `apt-debian-mirror`, `apt-debian-security-mirror`, `apt-ubuntu-mirror`, `apt-ubuntu-security-mirror` | `ftp.debian.org`, `security.debian.org`; both Ubuntu repos take the second entry of `INFINITO_APT_UBUNTU_MIRRORS` |
|
||
| `pypi` | `pypi-proxy` | `pypi.org` (incl. `files.pythonhosted.org`) |
|
||
| `npm` | `npm-proxy` | `registry.npmjs.org` |
|
||
| `rubygems` | `gem-proxy` | `rubygems.org` |
|
||
| `go` | `go-proxy` | `proxy.golang.org` |
|
||
| `helm` | `helm-bitnami` | `charts.bitnami.com/bitnami` |
|
||
| `yum` | `yum-rocky`, `yum-fedora` | `download.rockylinux.org`, `download.fedoraproject.org` |
|
||
| `raw` | `raw-githubusercontent`, `raw-codeload-github`, `raw-packagist`, `raw-alpine` | `raw.githubusercontent.com`, `codeload.github.com`, `repo.packagist.org`, `dl-cdn.alpinelinux.org` |
|
||
|
||
A proxy repo that holds a cached copy serves it when its remote is unreachable, regardless of `metadataMaxAge` (measured). `autoBlock` is therefore off: while a remote is auto-blocked Nexus answers `404 Remote Auto Blocked` instead of falling back to that cached copy. The mirror repos below only come into play when nothing is cached either.
|
||
|
||
Every apt suite exists twice, once per upstream mirror. Nexus has no group repository type for `apt` (only maven, raw, docker, yum, npm, pypi, rubygems, go and friends), so the failover lives in the frontend: a request that the primary repo answers with `502`/`503`/`504` is re-run against the `-mirror` repo, which proxies a different host. `404` deliberately does not retry, because a missing file is missing on both mirrors and apt probes for missing files often. The frontend waits at most 8 s for a primary before it re-runs the request against the mirror; the `-mirror` locations keep the global 300 s.
|
||
|
||
Bootstrap is idempotent and runs from [package.sh](../../../scripts/docker/cache/package.sh) once the stack is healthy.
|
||
|
||
### Package Cache Frontend 🔐
|
||
|
||
[`package-cache-frontend`](../../../compose/package-cache-frontend/README.md) runs `nginx:alpine` and reverse-proxies upstream package-manager hostnames onto the matching Nexus repo path. Combined with `extra_hosts` DNS-hijack on consumers, package managers can hit their real upstream URL and still flow through the cache.
|
||
|
||
Two listener layers:
|
||
|
||
- HTTPS (port 443): per-hostname server certs signed by a dedicated CA. Used by the `infinito` runner (Ansible-driven `pip install`, `gem install`, `composer install`, `curl https://…`). The runner trusts the CA via [package-frontend-ca.sh](../../../scripts/docker/cache/package-frontend/ca.sh).
|
||
- HTTP (port 80): plain mirrors for `deb.debian.org`, `archive.ubuntu.com`, `security.ubuntu.com`, `dl-cdn.alpinelinux.org`. Used by inner-`dockerd` Dockerfile builds via `build.extra_hosts` DNS-hijack. No CA-trust required in the build container.
|
||
|
||
Cert generation runs in a throw-away alpine container driven by [package-frontend-certs.sh](../../../scripts/docker/cache/package-frontend/certs.sh) before the frontend starts.
|
||
|
||
## Activation 🎚️
|
||
|
||
The `cache` decision is exposed via `Profile.cache_stack_enabled()` in [profile.py](../../../cli/administration/deploy/development/profile.py). When active:
|
||
|
||
- [compose/cache.override.yml](../../../compose/cache.override.yml) is layered on top of the base [compose.yml](../../../compose.yml) by [compose.py](../../../cli/administration/deploy/development/compose.py) and [down.py](../../../cli/administration/deploy/development/down.py) via [common.py](../../../cli/administration/deploy/development/common.py)`compose_file_args`.
|
||
- The cache services are added.
|
||
- The runner's `infinito` service receives:
|
||
- bind-mounts for the registry-cache CA, the package-cache client snippets (`pip.conf`, `npmrc`, `apt/${INFINITO_DISTRO}.list`), and the frontend CA file
|
||
- `extra_hosts` entries DNS-hijacking the HTTPS upstream hostnames to the frontend's static IP
|
||
- `INFINITO_CACHE_PACKAGE_FRONTEND_IP` env var for the inner compose wrapper
|
||
- Cert generation, Nexus repo bootstrap, and runner trust-store install run from [compose.py](../../../cli/administration/deploy/development/compose.py).
|
||
|
||
When inactive, the override is omitted: cache services do not exist, the runner has no cache mounts or DNS-hijack, package managers go direct to upstream.
|
||
|
||
CI signals (`GITHUB_ACTIONS=true`, `INFINITO_RUNNING_ON_GITHUB=true`, `CI=true`) deactivate. Fresh runner disks per CI job give no cross-run amortization.
|
||
|
||
## Coverage Matrix 📋
|
||
|
||
| Traffic | Mechanism | Cached |
|
||
|---|---|---|
|
||
| Image pulls (any registry, inner `dockerd`) | `registry-cache` MITM via `HTTP_PROXY` env on `dockerd` | ✓ |
|
||
| `apt-get install` (Ansible task in runner) | `apt/${INFINITO_DISTRO}.list` URL-rewrite to `package-cache:8081` | ✓ |
|
||
| `pip install` (Ansible task in runner) | `pip.conf` URL-rewrite | ✓ |
|
||
| `npm install` (Ansible task in runner) | `.npmrc` URL-rewrite | ✓ |
|
||
| `gem`, `composer`, `go`, `curl https://…` (Ansible task in runner) | DNS-hijack + frontend HTTPS + runner CA-trust | ✓ |
|
||
| `RUN apt-get install` (inner Dockerfile build, Debian/Ubuntu) | `build.extra_hosts` DNS-hijack + frontend HTTP | ✓ |
|
||
| `RUN apk add` (inner Dockerfile build, Alpine ≤3.17 HTTP) | `build.extra_hosts` DNS-hijack + frontend HTTP | ✓ |
|
||
| `RUN pip/npm/gem/composer/go install` (inner Dockerfile build) | none today | ✗ |
|
||
| `RUN apk add` (Alpine ≥3.18 HTTPS) | none today | ✗ |
|
||
| App container runtime traffic (HTTPS to external API) | none today | ✗ |
|
||
|
||
The HTTPS-only inner-build gap requires per-image CA-trust bootstrap, which is out of scope for the current minimal-invasive design.
|
||
|
||
## Compose Wrapper Auto-Detection 🪄
|
||
|
||
The runner's `compose` wrapper at [roles/sys-svc-compose/files/python/compose.py](../../../roles/sys-svc-compose/files/python/compose.py) auto-detects compose files when invoked from per-app directories under `/opt/compose/<app>/`:
|
||
|
||
| File | When |
|
||
|---|---|
|
||
| `compose.yml` | always |
|
||
| `compose.override.yml` | when present (role provides it) |
|
||
| `compose.ca.override.yml` | when present (TLS self-signed CA-inject runs) |
|
||
| `compose.cache.override.yml` | generated on the fly when `INFINITO_CACHE_PACKAGE_FRONTEND_IP` is set; emits `build.extra_hosts` for every service that has a `build:` key |
|
||
|
||
[pull.py](../../../roles/sys-svc-compose/files/python/pull.py) delegates to the same wrapper so `pull` and `build --pull` operations see the identical `-f` set.
|
||
|
||
## Environment Variables 🌳
|
||
|
||
The host-side env-vars (`INFINITO_CACHE_REGISTRY_*`, `INFINITO_CACHE_PACKAGE_*`) are declared in [default.env](../../../default.env) and the dynamic ones (cache sizes from `df`/`/proc/meminfo`, sha256 admin password) are computed by [utils/env/builder.py](../../../utils/env/builder.py). `make dotenv` writes them into `.env`, which Compose auto-loads and which `BASH_ENV` makes available to every Makefile recipe through [scripts/meta/env/load.sh](../../../scripts/meta/env/load.sh). See [variables.md](variables.md).
|
||
|
||
Per-variable defaults and purposes are in [compose.yml.md](../artefact/files/compose.yml.md) under the cache section.
|
||
|
||
`INFINITO_CACHE_PACKAGE_MAX_AGE_MIN` (default `8640` = 6 days) controls how long every Nexus proxy repo holds an upstream response before revalidating. The bootstrap helper applies it as `contentMaxAge`, `metadataMaxAge`, and `negativeCache.timeToLive`. The default is intentionally one day below the 7-day `Valid-Until` window Debian and Ubuntu generate in their apt `Release` files, so Nexus always refreshes the manifest before `apt-get update` would see it expired. Raise it for slow-moving upstreams when staleness is preferable to upstream load; lower it further for fast-moving feeds (`apt-debian-security`, branch-pinned tarballs) when staleness becomes visible.
|
||
|
||
## Operations 🛠️
|
||
|
||
| Action | Command |
|
||
|---|---|
|
||
| Start the stack with caches | `make compose-up` |
|
||
| Stop the stack | `make compose-down` |
|
||
| Wipe local cache state | `make clean-cache` |
|
||
| Manually re-bootstrap Nexus repos | `bash scripts/docker/cache/package.sh` (after `make dotenv` or sourcing `scripts/meta/env/load.sh`) |
|
||
| Manually regenerate frontend certs | `bash scripts/docker/cache/package-frontend/certs.sh` |
|
||
| Reload nginx in the frontend | `docker exec infinito-package-cache-frontend nginx -s reload` |
|
||
| Inspect cache hits | `docker logs -f infinito-package-cache` and `docker logs -f infinito-package-cache-frontend` |
|
||
|
||
Cache state persists under `/var/cache/infinito/core/cache/`. Paths are configurable via the env scripts above.
|
||
|
||
## Adding a New Upstream ➕
|
||
|
||
When a new package manager or upstream needs caching:
|
||
|
||
1. Register a Nexus proxy repo in [package.sh](../../../scripts/docker/cache/package.sh).
|
||
2. If the upstream uses HTTPS, add it to `HOSTNAMES` in [package-frontend-certs.sh](../../../scripts/docker/cache/package-frontend/certs.sh) so a leaf cert is issued.
|
||
3. Add a server-block in [upstreams.conf](../../../compose/package-cache-frontend/upstreams.conf) that reverse-proxies onto the new Nexus repo path (rewrite if upstream URL prefix differs from the Nexus repo path).
|
||
4. Add an `extra_hosts` entry on the `infinito` service in [compose/cache.override.yml](../../../compose/cache.override.yml) for runner-side traffic.
|
||
5. If the upstream is HTTP-only and inner-`dockerd` builds need it, also add it to `_CACHE_HTTP_HOSTNAMES` in [compose.py](../../../roles/sys-svc-compose/files/python/compose.py) so the per-app `compose.cache.override.yml` includes it in `build.extra_hosts`.
|
||
|
||
## Background 📚
|
||
|
||
- Compose-file env-var contract: [compose.yml.md](../artefact/files/compose.yml.md)
|
||
- Profile gating mechanics: [profile.py](../../../cli/administration/deploy/development/profile.py)
|