blog.rust.gay

Running a private Cargo registry mirror

Teams run private Cargo registries or mirrors to make builds repeatable, reduce external network dependence, and serve crates in air-gapped or high-throughput environments. Cargo does not expose a single “mirror” switch. Instead, it offers alternate registries, source replacement, and filesystem-based sources such as vendored directories and local registries. The right choice depends on whether you need to publish internal crates, cache crates.io, or carry exact dependency sources alongside a project.

The operational details matter more than the label. A caching proxy, a replacement source, and an internal registry can all look like “a mirror” from the outside, but Cargo treats them differently for dependency resolution, publishing, authentication, and consistency checks. A setup that works for local development can fail in CI if it does not account for Cargo.lock, registry tokens, the --registry flag on commands that contact a registry directly, or stale metadata. This post focuses on the mechanisms documented in the Cargo Book and the configuration and failure modes that usually matter in infrastructure work.

Why teams mirror or replace Cargo sources

Most mirror setups start with one of a few constraints. An air-gapped network may prohibit build machines from reaching crates.io at all. A large CI fleet may repeatedly fetch the same index metadata and crate files, making bandwidth and latency visible in build times. A regulated environment may require internal control over where dependencies come from and how artifacts are retained. Some teams also want a controlled publication path for internal crates that should never be pushed to crates.io.

These goals do not all map to the same Cargo feature. If the requirement is internal package distribution with its own index and publication workflow, an alternate registry is usually the right model. If the requirement is to keep using crates.io package names and versions but serve them from a controlled copy, source replacement is closer to the problem. If the requirement is deterministic offline builds for a specific workspace, vendoring or a local registry prepared ahead of time is often simpler and easier to audit than operating a live mirror service.

Cargo’s documentation draws an important boundary: source replacement assumes the replacement contains the same source code as the original source. It is not intended for patching dependencies and not intended for private registries whose contents differ from the original. That assumption is the reason mirror consistency is an infrastructure concern, not just a networking concern.

Choose the right source model

Cargo’s source model is easier to operate when you separate three decisions: where dependency metadata comes from, where crate contents come from, and whether the build environment is allowed to contact external services during normal operation.

Alternate registries

An alternate registry is a separate registry with its own index and, optionally, a web API. A crate can depend on another registry by naming it in Cargo.toml, and Cargo can publish to a registry that supports web API access. Alternate registries are appropriate for internal crates or curated package sets that are intentionally distinct from crates.io.

The Cargo Book also notes a key constraint: crates.io does not accept packages that depend on crates from other registries. That rule affects how you split public and private code. If a crate is meant to be published to crates.io, its dependency graph must be compatible with crates.io’s rules; an internal registry can still exist alongside it, but you cannot make crates.io packages depend on that private registry.

Source replacement

Source replacement redirects Cargo’s communication with an existing source to another data source, such as a mirror or a local copy. It is configured in .cargo/config.toml using [source] tables and a replace-with key. Cargo treats the replacement as equivalent to the original source. Because of that equivalence assumption, replacement crates that are absent from the original source are not allowed.

This makes source replacement a good fit for controlled mirrors of crates.io or for redirecting crates.io to a prepared local source. It is a poor fit when you want to change dependency contents. For dependency patches, Cargo’s [patch] mechanism is the documented tool; for private registries with different packages, use an alternate registry.

Directory sources and local registries

Directory sources and local registries are filesystem-based subsets used for vendoring. A directory source contains unpacked crate sources, with each crate carrying an associated .cargo-checksum.json metadata file. Cargo’s cargo vendor command manages directory sources and prints the configuration needed to use the vendored output.

A local registry source is different: it is intended to be a subset of another registry source available on the local filesystem and contains downloaded *.crate files plus an index subset in the same format as a normal registry index. The Cargo Book points to the third-party cargo-local-registry subcommand for creating and managing local registries.

For air-gapped builds, both approaches require preparation before the build machine loses network access. The practical difference is operational shape: vendored directories are easy to keep next to a workspace and inspect as files, while local registries preserve more of the registry/index model. In either case, the prepared source must match what the project’s Cargo.lock and manifests expect.

Configure alternate registries and publishing

To use a registry other than crates.io, define it in .cargo/config.toml. Cargo’s registry configuration uses the registries table, with a key for each registry name:

[registries]
my-registry = { index = "https://my-intranet:8080/git/index" }

The index key can point to a git repository containing the registry index or to a Cargo sparse registry URL with the sparse+ prefix. As with most Cargo config values, the index may also be specified through an environment variable. The documented form is CARGO_REGISTRIES_<NAME>_INDEX, for example:

export CARGO_REGISTRIES_MY_REGISTRY_INDEX=https://my-intranet:8080/git/index

A dependency can select a registry explicitly in Cargo.toml:

[dependencies]
other-crate = { version = "1.0", registry = "my-registry" }

Publishing to a registry that supports web API access uses the same registry name. Cargo’s documented flow is to authenticate once with cargo login --registry=my-registry, then publish with cargo publish --registry=my-registry. The token may also be passed directly to cargo publish with --token, or supplied through a registry-specific environment variable such as CARGO_REGISTRIES_MY_REGISTRY_TOKEN. cargo login stores tokens in credentials.toml under the Cargo home directory, which defaults to $HOME/.cargo.

Registry selection should be explicit in automation. You can set a default registry with registry.default in .cargo/config.toml, but explicit --registry flags make CI behavior easier to audit. You can also restrict accidental publication by setting package.publish in Cargo.toml to an allowlist of registry names, or to false to block all publishing. For internal packages that must never reach crates.io, an allowlist or publish = false is a simple guardrail that catches mistakes before they become release incidents.

Cargo’s command-line overrides and configuration hierarchy also matter in shared environments. Configuration is merged from .cargo/config.toml files in parent directories and from $CARGO_HOME/config.toml, with more specific locations taking precedence for scalar values and arrays being joined according to Cargo’s merge rules. Environment variables and --config overrides can take precedence in specific ways. For mirror infrastructure, keep the effective configuration visible in CI logs or debug output policies that do not leak secrets, so operators can tell which registry or replacement source was actually used.

Mirror crates.io with source replacement

Source replacement is the Cargo mechanism most people mean when they say “mirror crates.io.” The configuration lives in .cargo/config.toml. A common pattern replaces the default crates-io source with a prepared local directory:

[source.my-vendor-source]
directory = "vendor"

[source.crates-io]
replace-with = "my-vendor-source"

The same pattern can replace crates-io with another named source. A replace-with key may reference an alternate registry name defined in [registries]. Cargo also supports several source kinds in [source.<name>] tables, including registry, local-registry, directory, and git-based sources. Registry sources may be specified as git, HTTPS, or sparse HTTP indexes such as sparse+https://....

Because source replacement changes where Cargo talks, some commands that contact a registry directly require --registry to make the target explicit and to select the correct authentication token. The Cargo Book calls this out in the source replacement and registry workflows. In practice, publish-related automation should not rely on ambient defaults when multiple registries or replacement sources exist.

Source replacement has two common infrastructure patterns. One is an offline or near-offline pattern: prepare a directory or local registry, commit or ship it through an artifact pipeline, and replace crates-io with that local source. The other is an online controlled-mirror pattern: operate an index and download endpoint that mirror crates.io contents, then point Cargo at that registry as a replacement. The online pattern can reduce external traffic, but it inherits all the consistency and availability responsibilities of a registry service.

Sparse indexes, git indexes, and mirror performance

Cargo supports two remote registry protocols: git and sparse. If a registry index URL starts with sparse+, Cargo uses the sparse protocol. Otherwise Cargo uses the git protocol. The git protocol stores index metadata in a git repository and requires Cargo to clone the repository. The sparse protocol fetches metadata for relevant crates using plain HTTP requests.

This distinction matters for mirror design. A full git index clone can become expensive for large dependency graphs, busy CI runners, or environments where many machines need fresh metadata. Sparse indexes reduce metadata transfer by fetching only what is needed. The crates.io registry supports both protocols. For crates.io, the protocol is controlled by the registries.crates-io.protocol config key.

Sparse registries also have HTTP-specific behavior documented in Cargo’s registry index reference. The download endpoint can serve .crate files over HTTP(S), and Cargo supports redirects and modern HTTP behavior depending on platform and build. If a registry’s index configuration sets auth-required: true, Cargo includes an Authorization header with HTTP(S) download requests and treats API requests, crate downloads, and sparse index updates as authenticated operations. On sparse authentication failures, a server may include a WWW-Authenticate header with a Cargo login_url="..." challenge telling the user where to obtain a token.

Caching is another operational difference. Cargo caches crate metadata files and uses HTTP cache validation headers such as ETag or Last-Modified with conditional requests. When a registry sits behind a CDN or proxy, stale caches can prevent users from seeing newly published crates or updated metadata. The Cargo Book recommends cache invalidation when index files change. For internal mirrors of crates.io, stale cache behavior and stale mirror refresh jobs can produce the same symptom: Cargo cannot find a version that the lockfile or manifest expects.

Vendoring for offline and reproducible builds

cargo vendor is the straightforward tool when a project needs all crates.io and git dependencies available locally. The command vendors dependencies into a specified directory and prints the configuration needed to use those vendored sources. After vendoring, Cargo treats vendored sources as read-only, the same way it treats registry and git sources. If you need to modify a crate from a remote source, Cargo’s documentation recommends using [patch] or a path dependency pointing to a local copy rather than editing vendored files in place.

The command has several options that matter in infrastructure workflows. --sync can vendor additional manifests beyond the default one, which is useful when a repository contains multiple workspaces or related manifests that should share a vendor directory. --respect-source-config tells Cargo to read [source] configuration from .cargo/config.toml instead of ignoring it by default when downloading crates from crates.io. --no-delete preserves existing contents of the vendor directory. --versioned-dirs adds version information to all directories in the vendor output, not only when needed to disambiguate multiple versions of the same package. That can make history easier to track and can help performance when only some packages change between re-vendor runs.

Vendoring is not only a network control. It is also a reproducibility artifact. The vendor directory should be generated from manifests and lockfiles that represent the intended build, and the resulting Cargo configuration should be checked into the right place for the workflow. For local package workflows, Cargo often expects the printed configuration in .cargo/config.toml. In CI, you may generate or inject that configuration as part of the build image or job setup. Either way, treat the configuration as code: review changes, keep them close to the build definition, and avoid manual edits on release machines.

Filesystem-based sources include integrity metadata. Directory sources carry .cargo-checksum.json files, and Cargo’s index entries include checksums for crate files. These mechanisms help detect accidental modification and keep Cargo’s view of source contents consistent. They are not a substitute for supply-chain controls, signature verification policies, or artifact provenance, but they do mean that ad hoc edits inside a vendored tree can break builds in ways that look like corruption rather than intentional changes.

Operational tradeoffs and failure modes

The most common mirror failure is not “Cargo cannot reach the network.” It is “Cargo reached a source that does not match what this build expects.” A replacement source must match the original source’s contents. A stale mirror, an incomplete local registry, or a vendor directory generated from an older Cargo.lock can cause resolution failures, missing crate files, checksum problems, or confusing behavior that disappears when developers build against crates.io directly.

Plan the bootstrap path before you need it. Air-gapped and restricted-network builds require a reliable way to obtain crate files, index metadata, or unpacked sources ahead of time. The bootstrap artifact should include the exact inputs your automation uses: manifests, lockfiles, vendor or local-registry data, and the Cargo configuration that points builds at those sources. If CI refreshes dependencies, refresh the prepared sources in the same controlled process.

Authentication and publishing controls should be handled as normal infrastructure secrets. cargo login writes tokens to credentials.toml, which is convenient for developer machines. CI systems often prefer environment variables such as CARGO_REGISTRIES_<NAME>_TOKEN or explicit tokens passed to commands in controlled contexts. Avoid exposing tokens in logs. When using multiple registries, make registry selection explicit and use package.publish restrictions to prevent publishing internal code to the wrong destination.

Do not test only cargo build. A mirror or registry setup touches several Cargo paths: dependency fetch, lockfile resolution, index protocol behavior, publishing, and registry-specific commands. Validate the workflow you actually run: clean fetches, offline builds where required, cargo vendor or source preparation, publish dry runs or real publishes to the intended registry, and commands that need --registry. For sparse HTTP registries behind proxies or CDNs, also validate authentication and cache invalidation behavior. Infrastructure that passes a single warm-cache laptop build can still fail under cold CI runners.

Conclusion

For most organizations, the practical question is not “how do I mirror Cargo?” but “which Cargo source model matches this constraint?” Use an alternate registry when you need a separate package namespace and publication workflow. Use source replacement when you need Cargo to use an equivalent, controlled copy of an existing source such as crates.io. Use cargo vendor, directory sources, or local registries when builds must run without external access or when you want dependency contents prepared as an explicit build input.

Whatever path you choose, keep the source contents consistent with your lockfiles, make registry and source configuration explicit in automation, and verify the full fetch, build, and publish workflow. A Cargo mirror is build infrastructure. Its configuration should be versioned, reviewed, and tested in the same environments that produce release artifacts.

Sources


← back to posts