Seven package ecosystems, one approval gate, one image builder. Here's how they fit together to give your nodes exactly the software they need — without ever touching the public internet.
Every Axiom node runs jobs inside a container built from a Node Image. That image needs Python packages, system libraries, container base layers, and sometimes npm modules, Conda packages, or PowerShell tooling. In production, you don't want every build pulling from pypi.org, Docker Hub, or npmjs.com — that's slow, unreliable, subject to rate limits, and a supply chain risk.
Internal mirrors solve all of this: packages are cached locally, builds are reproducible offline, and every package is explicitly approved before it enters your fleet.
| Ecosystem | Mirror Backend | How It Caches |
|---|---|---|
| PyPI Python | devpi | Caching proxy — fetches on first request, serves from cache after |
| APT Debian / Ubuntu | apt-cacher-ng | Transparent proxy — intercepts apt-get automatically |
| apk Alpine | nginx reverse cache | HTTP cache proxy for dl-cdn.alpinelinux.org |
| OCI Docker / Container Images | registry:2 pull-through | Pull-through cache — proxies Docker Hub, GHCR, etc. |
| npm Node.js | Verdaccio | Caching proxy — mirrors npmjs.com on demand |
| Conda Data Science | conda-mirror / HTTP cache | Proactive mirror of named packages + versions |
| NuGet PowerShell | BaGet | Lightweight NuGet v3 server — packages pushed manually |
Regardless of ecosystem, every package flows through the same controlled pipeline. Click Next Step to trace the path.
Smelter
Auto-fetch
CVE check
Compose image
Foundry
The operator experience is identical regardless of package type. In the Smelter Registry, you approve an ingredient — specifying its ecosystem (PyPI, APT, apk, OCI, npm, Conda, or NuGet), name, and version. The mirror service proactively fetches it using the appropriate backend. When you build a Node Image in the Foundry, only approved-and-mirrored packages are available.
The Smelter Registry, Foundry image builder, and BOM Explorer are Enterprise Edition features. The Admin page shows these tabs only when an EE licence is active.
Cache Python packages locally with devpi so image builds are fast, reproducible, and independent of pypi.org.
devpi is a caching proxy that sits between your image builds and pypi.org. The first time a package is requested, devpi fetches it from upstream and stores a local copy. Every subsequent request is served from cache.
It runs as a Docker container alongside the Axiom stack on port 3141.
If you deployed with the standard compose.server.yaml, devpi is already running. Check with: docker ps | grep devpi
If devpi isn't in your compose file yet, add this service:
devpi: image: devpi/devpi-server:latest restart: unless-stopped ports: - "3141:3141" volumes: - devpi-data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3141/+api"] interval: 30s retries: 3
Navigate to /admin and click the Smelter Registry tab.
In the Mirror Configuration section, enter http://devpi:3141/root/pypi/+simple/ and save.
The path must be /root/pypi/+simple/, not /+simple/. The root/pypi is devpi's default index name.
You don't add packages to devpi directly. Approve them in the Smelter Registry (Module 6) and the mirror service proactively runs pip download to cache them. When Foundry builds an image that includes those packages, it pulls from devpi — fast, cached, and verified.
For air-gapped environments, upload .whl files directly via the Smelter's upload button.
Cache Debian/Ubuntu and Alpine system packages so image builds don't depend on upstream archives.
apt-cacher-ng is a transparent caching proxy. All apt-get install calls during Foundry builds are routed through it. Packages cache on first fetch; subsequent builds are instant.
apt-mirror: image: sameersbn/apt-cacher-ng:latest restart: unless-stopped ports: - "3142:3142" volumes: - apt-cache-data:/var/cache/apt-cacher-ng environment: - DISABLE_IPFORWARD_CHECK=1
Configure the URL in Admin → Smelter Registry → Mirror Config: http://apt-mirror:3142
Foundry injects a temporary APT proxy directive into every Dockerfile, then removes it from the final image so the config doesn't leak to deployed nodes.
Inside Docker, the IP forwarding check fails spuriously. Without this flag, apt-cacher-ng refuses to start.
Alpine uses apk, not APT. There's no apt-cacher-ng equivalent, but an nginx reverse-proxy cache does the same job. It sits between your builds and dl-cdn.alpinelinux.org, caching .apk files on first fetch.
apk-cache: image: nginx:alpine restart: unless-stopped ports: - "3143:80" volumes: - ./apk-cache.conf:/etc/nginx/conf.d/default.conf:ro - apk-cache-data:/var/cache/apk-proxy
proxy_cache_path /var/cache/apk-proxy levels=1:2 keys_zone=apk:10m max_size=10g inactive=7d; server { listen 80; location / { proxy_pass http://dl-cdn.alpinelinux.org; proxy_cache apk; proxy_cache_valid 200 7d; proxy_cache_use_stale error timeout updating; } }
Foundry injects a temporary /etc/apk/repositories rewrite pointing at the cache during builds, then restores the default repos in the final image — same pattern as the APT proxy.
Both APT and apk mirrors can also work proactively. When you approve a system package in the Smelter, the mirror service fetches it ahead of time:
apt-get download <pkg> in a throwaway container to pre-warm the apt-cacher-ng cache
apk fetch <pkg> in a throwaway Alpine container to pre-warm the nginx cache
This means packages are already cached before any Foundry build runs — important for air-gapped environments where you pre-warm while you still have internet, then disconnect.
Every Foundry build starts with FROM python:3.12-slim or FROM alpine:3.20. A local registry cache ensures those pulls never fail and never hit Docker Hub rate limits.
Anonymous pulls: 100 per 6 hours. Authenticated: 200. A busy Foundry can hit this in minutes if images aren't cached.
If Docker Hub or GHCR is down, every Foundry build fails. In air-gapped environments, upstream pulls are impossible.
Base images are 50-200MB. Pulling the same image on every build wastes bandwidth and slows builds.
Docker's official registry:2 image has built-in pull-through cache support. One environment variable turns it into a transparent proxy for Docker Hub:
registry-cache: image: registry:2 restart: unless-stopped ports: - "5000:5000" volumes: - registry-cache-data:/var/lib/registry environment: - REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io
To also cache GHCR images, you can add a second registry instance pointing at https://ghcr.io.
When you approve a container base image in the Smelter (ecosystem: OCI), the mirror service proactively pulls it:
Name: python, Version: 3.12-slim, Ecosystem: OCI
Runs docker pull python:3.12-slim through the pull-through cache, warming it locally. Status goes PENDING → MIRRORED.
When a blueprint uses python:3.12-slim as its base, the FROM pull hits the local registry — instant, no rate limits.
Pre-warm the cache while you have internet. The registry-cache-data volume persists all cached layers. Disconnect the network and builds still work — every FROM resolves locally.
Three more ecosystems for specialised workloads: Node.js jobs, data science pipelines, and Windows automation.
If your node images run JavaScript/TypeScript jobs, Verdaccio provides a lightweight npm registry that caches packages from npmjs.com on demand.
verdaccio: image: verdaccio/verdaccio:5 restart: unless-stopped ports: - "4873:4873" volumes: - verdaccio-data:/verdaccio/storage
Verdaccio works like devpi: first install caches the package, subsequent installs serve from local storage. When you approve an npm ingredient in the Smelter, the mirror service proactively runs npm pack <pkg>@<version> to pre-warm the cache.
In Foundry blueprints, set the npm registry in the Dockerfile: npm config set registry http://verdaccio:4873
For data science workloads that depend on Conda packages (numpy, pandas, scikit-learn, etc.), you can mirror specific packages and versions locally.
Two approaches:
Mirrors named packages + versions from conda-forge or defaults channel to a local directory. Served by nginx or Python's http.server. Best for production with a defined package set.
Same pattern as the apk mirror — nginx reverse-proxies conda.anaconda.org and caches on first fetch. Simpler setup, but doesn't proactively warm.
When you approve a Conda ingredient in the Smelter, the mirror service runs conda create --download-only -n tmp <pkg>=<version> in a throwaway container to fetch the package into the local mirror. The Foundry injects a .condarc pointing at the local mirror during builds.
Packages from Anaconda's default channel (repo.anaconda.com) require a paid commercial licence for organisations with 200+ employees. This is a usage restriction from Anaconda, Inc. — not a code licence issue.
Recommended alternative: Use conda-forge (conda-forge.org) as your upstream channel instead. conda-forge is community-maintained, has no commercial restrictions, and covers the vast majority of data science packages. When approving Conda ingredients in the Smelter, you can choose which channel to mirror from — the Foundry will flag the Anaconda ToS if you select the default channel.
For Windows automation jobs that need PowerShell modules (Pester, PSScriptAnalyzer, custom internal modules), BaGet provides a lightweight NuGet v3 server.
baget: image: loicsharma/baget:latest restart: unless-stopped ports: - "5555:80" volumes: - baget-data:/var/baget environment: - ApiKey=AXIOM-INTERNAL-NUGET-KEY - Storage__Type=FileSystem - Storage__Path=/var/baget - Database__Type=Sqlite - Database__ConnectionString=Data Source=/var/baget/db/baget.db - Search__Type=Database
BaGet requires packages to be pushed to it. When you approve a NuGet ingredient in the Smelter, the mirror service downloads the .nupkg from PSGallery and pushes it to BaGet automatically. You can also upload .nupkg files manually via the Smelter upload button.
In Foundry tool recipes, register the repository:
RUN pwsh -Command "Register-PSRepository \ -Name 'AxiomInternal' \ -SourceLocation 'http://baget:5555/v3/index.json' \ -InstallationPolicy Trusted"
AXIOM-INTERNAL-NUGET-KEY is an example. Use a strong, unique key in production.
One dashboard, seven ecosystems. Approve packages, watch them mirror, scan for vulnerabilities, enforce compliance.
Only explicitly approved packages — across all seven ecosystems — can be built into images. No surprise dependencies.
One-click vulnerability scan using pip-audit. Vulnerable packages get a red badge.
See at a glance which packages are cached locally (MIRRORED), still downloading (PENDING), or failed (FAILED).
STRICT mode blocks builds with unapproved packages. WARNING mode allows them but marks the image non-compliant.
The workflow is identical whether you're approving a Python package, an Alpine system library, or a Docker base image.
Navigate to /admin and click the Smelter Registry tab.
Fill in: Package Name, Version, Ecosystem (PyPI, APT, apk, OCI, npm, Conda, or NuGet), and optionally a SHA256 hash.
The ingredient appears with a PENDING badge. The mirror service picks the right backend and proactively fetches the package. Within seconds, the badge changes to MIRRORED.
The package is now approved and cached in the appropriate mirror backend. It can be included in any Foundry blueprint. If the download fails, the badge shows FAILED — expand the row to see the mirror log.
| Ecosystem | Fetch Command | Runs In |
|---|---|---|
| PyPI | pip download <pkg> |
Host (direct) |
| APT | apt-get download <pkg> |
Throwaway Debian container |
| apk | apk fetch <pkg> |
Throwaway Alpine container |
| OCI | docker pull <image>:<tag> |
Host (through pull-through cache) |
| npm | npm pack <pkg>@<ver> |
Throwaway Node container |
| Conda | conda create --download-only |
Throwaway Conda container |
| NuGet | curl nupkg + push to BaGet |
Host (direct) |
If your environment has no internet, upload package files directly via the Smelter. Every ingredient row has an upload button. Upload the file (.whl, .deb, .apk, .nupkg, .tar.gz, .tgz) and the mirror status changes to MIRRORED immediately.
Builds with unapproved or unmirrored packages are rejected (HTTP 403). Use this in production.
Builds proceed but the template is marked non-compliant (amber badge). Use this during development.
Switch modes via the enforcement dropdown at the top of the Smelter Registry tab.
Combine approved packages from any ecosystem into a custom Docker image. The Foundry's guided wizard handles the Dockerfile — you never write one by hand.
Defines what goes in the image: base OS, Python/npm/Conda packages, system packages, and tools.
Defines egress rules — what network access the container has.
Combines one Runtime + one Network recipe into a buildable Docker image.
All of this happens in the dashboard at /templates.
The 5-step wizard opens.
Name your recipe (e.g. "Data Science Runtime").
Choose the base OS. This determines the OS family (DEBIAN/ALPINE) which affects available packages. Only Smelter-approved base images appear if enforcement is STRICT.
Add packages from the Smelter-approved list. Autocomplete shows only approved ingredients for your OS family and ecosystem.
Add optional tools from the Capability Matrix, review the recipe, and create.
Select your Runtime and Network recipes. Name the template.
Foundry generates a Dockerfile, injects all mirror configs (pip.conf, APT proxy, apk repos, .condarc, npm registry), builds the image, and pushes to your local registry.
An ephemeral container verifies the image starts correctly. On success: status → ACTIVE, Bill of Materials captured.
Change a template's status at any time: ACTIVE (nodes can use it), DEPRECATED (warning), or REVOKED (blocked at enrollment and work-pull).
After a successful build, click BOM on any template to see every installed package — pip, apt, apk, npm, conda. The BOM Explorer in Admin lets you search across all built images: "which nodes have numpy 1.26?"
Verify your mirrors are working, validate your images, and fix common problems.
The Smelter Registry tab shows a Repository Health card with live metrics for all configured mirrors:
Wrong devpi URL. Verify path is /root/pypi/+simple/. Also check: is the package approved and MIRRORED in the Smelter?
Expand the row and check the mirror log. Common causes: no internet, mirror container not running, package name typo. If air-gapped, use the upload button.
Check the mirror container is on the same Docker network. Verify with docker logs. APT: look for 200 (ca) entries. apk/nginx: check access.log for HIT/MISS.
The blueprint contains unapproved packages. The error message lists which ones. Approve them in the Smelter and wait for MIRRORED status.
Ensure registry-cache is running and Foundry builds pull through it. Check docker logs registry-cache for cache hit/miss rates.
The local mirror doesn't have the package yet. Approve it in the Smelter with ecosystem Conda and wait for MIRRORED. Or check the channel name — conda-forge vs defaults.
devpi (:3141), apt-cacher-ng (:3142), apk-cache (:3143), registry-cache (:5000), and optionally Verdaccio (:4873), conda-mirror, BaGet (:5555).
All mirror URLs set in Admin → Smelter Registry. Repository Health shows green badges.
All packages your blueprints need are in the Smelter with MIRRORED status across all ecosystems.
STRICT for production, WARNING for development.
Builds complete with Smelt-Check passing and BOM captured. Templates are ACTIVE.
You now know how to set up all seven package mirrors, approve packages through the Smelter, build custom node images in the Foundry, and troubleshoot common issues. For detailed reference, see the Package Mirrors Runbook and the Foundry Feature Guide.