Skip to content

Building without direct egress

Nothing that touches the network is hardcoded here. Every fetch reads its location from the environment, with a public default. No internal hostname or credential appears in this repository. The tables give the shape, and the values are yours.

Prefer a mirror over a proxy

A mirror is a normal HTTPS endpoint. A proxy sits in every connection, and 407 and TLS-interception failures come from it. A 407 usually means you are proxying something that has a mirror.

Placeholder Means
<host> your artifact repository
<pypi> <conda> <crates> <nix-cache> <generic> the corresponding remote repo
<docker-mirror> registry mirror, reached as a hostname prefix

Nix

# ~/.config/nix/nix.conf, or /etc/nix/nix.conf for the daemon
substituters = https://<host>/<path>/<nix-cache>
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=

Two traps:

  • Keep upstream's key. A mirror relays NARs signed by upstream, so its own key would reject every path. Only a cache that re-signs needs one.
  • In multi-user mode the daemon fetches, so editing ~/.config/nix/nix.conf changes nothing. Use /etc/nix/nix.conf and restart the daemon, or add yourself to trusted-users.

If the cache needs credentials, Nix uses netrc rather than a URL with a password in it:

netrc-file = /home/<you>/.config/nix/netrc
machine <host>
  login <user>
  password <api-token>

A missing netrc shows up as HTTP error 401 on a .narinfo while nix-cache-info succeeds, because cache-info is often anonymous. Use a scoped token, mode 0600.

Flake inputs come from their forge. Once locked, the unpacked source of an input is a store path. A mirror usually serves that path, so a locked build often needs no forge access. flake.lock pins by hash, so a mirror cannot substitute different content undetected.

Rust

# ~/.cargo/config.toml - global, so this repo's .cargo/config.toml keeps owning linker settings
[source.crates-io]
replace-with = "mirror"

[source.mirror]
registry = "sparse+https://<host>/<path>/<crates>/index/"

sparse+ is required and the trailing slash matters. Without them cargo expects a git index and fails with a confusing clone error.

rustup is not involved in the Nix path. flake.nix and devenv.nix resolve the single pinned toolchain through rust-overlay from devco/rust-toolchain-nightly.toml. The top-level rust-toolchain.toml is the rustup-facing copy of the same pin. The compilers come from the Nix cache, so the crates mirror above is all this section needs.

If you do use rustup, set RUSTUP_DIST_SERVER and RUSTUP_UPDATE_ROOT. An exact version pin needs that version to exist on the mirror. A lazily-caching remote answers 404 until something asks, and that reads as "no such version".

Conda, via pixi

pixi is the one resolver for Python and conda here, and pixi.toml declares one channel:

channels = ["conda-forge"]

That name is public because this repository is public. The redirect to a mirror goes in pixi's global configuration instead. Then the manifest resolves identically for everybody, and no internal URL is ever committed:

# ~/.pixi/config.toml
[mirrors]
"https://conda.anaconda.org" = ["https://<host>/<path>/<conda>"]

The key is the upstream URL and the value is a list. pixi matches the longest key prefix, so the entry above covers every channel on that host. A longer key overrides it for one channel.

Two things follow from the value being a list. First, pixi does not try the original URL unless you repeat it. Second, pixi fetches repodata from the first entry. That file carries the SHA256 of every package, so it decides what all the later downloads are checked against.

Check what pixi reads rather than what you wrote:

pixi config list      # the merged values, all layers
pixi info -vvv        # every location searched, in priority order

Where that file lives

The layer with the highest priority wins. A project-local .pixi/config.toml is merged on top of all of it. That is the one file where a mirror URL must not go, because it is inside the repository.

Linux macOS Windows
Global $PIXI_HOME/config.toml, else ~/.pixi/config.toml the same %PIXI_HOME%\config.toml, else %USERPROFILE%\.pixi\config.toml
User $XDG_CONFIG_HOME/pixi/config.toml, else ~/.config/pixi/config.toml ~/Library/Application Support/pixi/config.toml %APPDATA%\pixi\config.toml
System /etc/pixi/config.toml /etc/pixi/config.toml C:\ProgramData\pixi\config.toml

--no-config skips the system and user layers; --config-file <path> replaces them with one file. Both reproduce a resolve without your machine's settings in it.

pip

This repository installs nothing from PyPI. pixi.toml has [dependencies] only - conda packages from conda-forge - and no [pypi-dependencies] table. So no pip, no uv and no PyPI index takes part in any build, gate or docs render. A pip.conf on your machine changes nothing here.

If you add a [pypi-dependencies] entry, pixi resolves it with uv. One thing first:

# ~/.pixi/config.toml
[pypi-config]
index-url = "https://<host>/<path>/<pypi>/simple"

That does not redirect anything. Per pixi's documented behaviour, pixi init writes index-url and extra-index-urls from the global config into a manifest. Pixi does not interpret them otherwise, because the manifest is meant to be complete on its own. Only keyring-provider and allow-insecure-host apply globally.

[mirrors] redirects uv. It needs two entries, because the index and the files come from different hosts:

# ~/.pixi/config.toml
[mirrors]
"https://pypi.org/simple" = ["https://<host>/<path>/<pypi>/simple"]
"https://files.pythonhosted.org/packages" = [
  "https://<host>/<path>/<pypi>/packages",
]

The failure that looks like a hang

With only the first entry, the resolve succeeds against the mirror. Then every download goes to the public host.

If you run plain pip or conda on the same machine for other work, they read their own files. This repository uses neither:

Tool File Key
pip, Linux ~/.config/pip/pip.conf ($XDG_CONFIG_HOME honoured) index-url under [global]
pip, macOS ~/Library/Application Support/pip/pip.conf where that directory exists, else ~/.config/pip/pip.conf the same
pip, Windows %APPDATA%\pip\pip.ini the same
conda ~/.condarc channel_alias

~/.pip/pip.conf is the legacy path and still works. pip config debug prints the exact list. In .condarc, channel_alias defaults to anaconda.org, so without it a bare conda-forge resolves there no matter what else you set. conda config --show-sources prints what is in effect.

Docker

Registry mirrors are a hostname prefix, not a setting:

<docker-mirror>/nixos/nix:2.35.2      instead of   nixos/nix:2.35.2

On such a network an unprefixed reference does not fall back, it fails. So every image is a build argument:

docker build --target dev -t sutura-dev \
  --build-arg BASE_IMAGE=<docker-mirror>/nixos/nix:2.35.2 \
  --build-arg NIX_SUBSTITUTER=https://<host>/<path>/<nix-cache> \
  --build-arg CARGO_REGISTRY_URL=https://<host>/<path>/<crates>/index .

.env.example has the full list; compose.dev.yaml reads them from a gitignored .env.

TLS interception

Installing your CA in the OS store is necessary but not sufficient, because several toolchains ship their own bundle:

Tool Variable
curl, git CURL_CA_BUNDLE, GIT_SSL_CAINFO
Python, pip SSL_CERT_FILE, REQUESTS_CA_BUNDLE
Node NODE_EXTRA_CA_CERTS
Nix NIX_SSL_CERT_FILE - in the daemon's environment for multi-user mode

Proxy, where there is no mirror

Set both cases. Always set no_proxy, or your tools send internal hosts to the proxy and those requests fail:

export https_proxy=http://<proxy>:<port>  HTTPS_PROXY=$https_proxy
export no_proxy="localhost,127.0.0.1,::1,<host>"  NO_PROXY=$no_proxy

Nix in multi-user mode is the exception: the daemon downloads, so a proxy exported in your shell changes nothing. It belongs in the daemon's environment.

Check it

curl -fsS https://<host>/<path>/<nix-cache>/nix-cache-info
nix build --print-out-paths nixpkgs#hello        # the part that needs credentials
curl -fsS https://<host>/<path>/<crates>/index/config.json
pixi run --frozen python -c "print('ok')"
docker pull <docker-mirror>/nixos/nix:2.35.2

nix-cache-info returning 200 while a build reports 401 means artifacts need credentials. Go back to the netrc step.

Why none of these values are in the repository

They configure a network, not a project. A contributor on a different network needs different ones. Both would be wrong for the public CI runner, which needs none at all. This repository is also public. Committing them would publish the shape of an internal estate to everybody who clones it.

Hence the split: public defaults in the manifests, file locations here, values on your machine. .env.example documents each build argument the container build takes, and the trap that goes with it.