# Signed nginx GeoIP2 APT repository The pipeline has two suite-local compiler services and one signing publisher. No service needs a container engine CLI, daemon socket, host package operations, or the live MMDB generator's data. The surrounding Compose deployment owns the service definitions. ## Container contract - `Dockerfile.builder`, build argument `SUITE=bookworm|trixie`, amd64. Entrypoint `python3 /opt/apt-repo/builder-worker.py`. `SUITE` is baked into each image. Give each builder its **own** `/state` and `/staging` volumes. Optionally mount the published `/repository` read-only for validated historical artifact reuse. Never mount the signing state into a builder. - `Dockerfile.publisher`: entrypoint `python3 /opt/apt-repo/scheduler.py`. Mount signing `/state` read-write, published `/repository` read-write, and each builder staging volume read-only at `/incoming/bookworm` or `/incoming/trixie`. The signing home remains `/state/gnupg`; retain the existing state volume and key on migration. Set `APT_PUBLIC_URL` to the deployment's public HTTPS repository URL. - `Dockerfile.scheduler` retains the publisher runtime and opt-in `test` target for existing offline test workflows. - Both entrypoints accept `--once`, `--trigger`, and `--healthcheck`. Trigger is an explicit state-file request, not a background host job. Run these commands with the deployment's container execution tool from the host; the containers never invoke it themselves. - Each daemon holds `/state/service.lock` for its whole lifetime. It starts immediately, retries failures after 60 and 120 seconds (three attempts maximum), and then waits until its next check or trigger. Builders check every 86400 seconds; the publisher checks every 60 seconds. Heartbeats and outcomes live in `/state/status.json`; the publisher additionally maintains public `scheduler.json`. A stale/dead/error worker fails its healthcheck. Signals stop the daemon; the next start checks immediately. ## Build identity and functional tests Stable and newest legacy releases are discovered separately from nginx.org. Official binary metadata is fetched through signature-enforcing APT and the pinned nginx signing fingerprint `8540A6F18833A80E9C1653A42FD21310B49F6B46`. The highest numeric Debian revision for the chosen upstream version and suite is selected using dpkg ordering; absence fails closed. Each module depends on the **exact full nginx Debian package version**. New module versions encode that revision: `UPSTREAM+nginxREVISION+gitCOMMITPREFIX-1~SUITE`. These sort after historical `UPSTREAM+gitCOMMITPREFIX-1~SUITE` versions. A legacy artifact can be reused only after validating its full dependency, commit, suite, architecture, source identity, functional evidence, extracted module hashes and package hash. The explicit client installer supports both naming schemes and selects only a module matching the exact verified nginx revision. No maintainer script invokes APT; clients upgrade manually. Compilation uses `--with-compat --with-stream`, pinned module Git commit from `config.json`, suite-native libraries, and official nginx binaries. The builder generates an isolated deterministic MMDB with reserved documentation addresses; it verifies HTTP/stream loading with `nginx -t`, category lookup, allowlisted category bypass and a missing-address lookup over loopback HTTP. It never consumes the production threat database. ## Atomic handoff and publication Builders copy a `.deb` plus its `.json` provenance into a hidden temporary directory, fsync the files/directory and atomically rename the directory into `/staging/ready/PACKAGE_ID/`. Hidden partial directories are ignored and cleaned by their owning worker on restart. Ready pairs are immutable and retained, allowing restart deduplication without publisher acknowledgments or queue writes by the signer. The publisher rejects incomplete, unexpected or symlinked ready pairs and validates package metadata, exact nginx dependency, provenance, trusted source identities, package SHA256 and extracted module SHA256 values before admitting modules. Helper rendering/build staging occurs under the publisher's `/state`, not the read-only incoming mounts. Signing private keys never enter builder images or public mounts. The publisher retains all historical pool bytes and by-hash indexes. A repository writer lock protects publishing; all incoming pairs and immutable conflicts are checked before pool writes. Signed generations are constructed off to the side and the `dists` symlink changes only after both suites have valid signatures. Failed signing leaves the previous active indexes intact. Identical retained inputs do not produce another generation. No garbage collection is automatic. ## Migration Keep the original signing state and published repository volumes. The publisher continues using `/state/gnupg` and refuses a public-key mismatch rather than silently rotating trust. Builders may read the old pool but never change it. Keep old unused volumes declared until an operator has explicitly planned their disposal. Do not copy signing state to a compiler or web container. This tooling performs no production migration automatically. ## Isolated tests From this directory, build `Dockerfile.scheduler --target test` using a unique `refactor-test-*` image name, then run with `--network none`. The test entrypoint builds its synthetic helper inside the disposable image and exercises canonical `tests/` and installer tests. Runtime layout checks live in `tests/check_runtime.py`. Real builds require separate disposable builder state/staging volumes for each suite. Start each builder with `--once`, then mount the resulting queues read-only into an isolated publisher with a new signing-state and repository volume. Never attach test containers to production volumes, never use the production signing key for tests, and never run installer or APT commands on the host. Functional build evidence is recorded in each package's JSON provenance; signed inventory is `repository.json`.