How to Use Private Registries with Cargo

Configure a private registry in Cargo config and specify it in Cargo.toml dependencies to fetch internal crates.

The internal crate problem

You have a crate called billing-utils. It contains logic for calculating taxes based on your company's specific rules. You cannot publish this to crates.io because it contains internal business logic. You need to share it with the frontend team, the mobile team, and your CI pipeline. Copy-pasting code creates a maintenance nightmare. Forking the repository for every project breaks versioning. You need a private registry.

A private registry lets you host crates internally while keeping the same dependency resolution workflow you use for public crates. Cargo treats your private registry just like crates.io, except the index is private and requires authentication.

Cargo speaks index, not URL

Cargo does not download crates from a random URL. It uses a registry index. The index is a database of metadata for every crate. It lists versions, dependencies, and the download URL for each crate. For crates.io, this index is a public git repository. A private registry is a private git repository or a sparse-compatible server that holds this index.

Think of the index as a catalog. The catalog tells you where the book is and what it weighs. You do not browse the warehouse directly. You check the catalog. Cargo fetches the index, finds the metadata for the crate you requested, downloads the crate from the URL in the metadata, and verifies the hash.

This design keeps the index small and fast. The index contains only text metadata. The actual crate files are stored separately. When you configure a private registry, you are telling Cargo where to find the catalog and how to authenticate.

Configuring the registry

Cargo reads configuration from config.toml files. You can place this file in your project at .cargo/config.toml to scope the configuration to that project, or in ~/.cargo/config.toml to apply it globally. The project-level file takes precedence.

The configuration has two parts. The registries section defines the index location. The registry section defines authentication for that registry name.

# .cargo/config.toml

# Define the registry name and its index location.
# The sparse protocol is the modern standard.
[registries.my-private]
index = "sparse+https://registry.example.com/index/"

# Configure authentication for the registry.
# Use environment variables to keep tokens out of config files.
[registry.my-private]
token = "${CARGO_REGISTRY_TOKEN}"

The index value must point to the root of the registry index. If you are using a sparse-compatible server, the URL should end with /. If you are using a git-based index, the URL points to the git repository.

Convention aside: Always use the sparse+https:// prefix for modern registries. The sparse protocol fetches only the metadata for the crates you need. Git-based indices require cloning the entire repository, which becomes slow as the registry grows. The community has moved to sparse protocol for performance.

Adding the dependency

Once the registry is configured, you reference it in your Cargo.toml. You specify the registry name in the dependency definition. Cargo uses this name to look up the index and authentication details in your config.toml.

# Cargo.toml

[dependencies]
# Pull this crate from the private registry.
# Cargo matches "my-private" to the config section.
billing-utils = { version = "0.1.0", registry = "my-private" }

# Public crates still come from crates.io by default.
serde = { version = "1.0", features = ["derive"] }

When you run cargo build, Cargo resolves dependencies. It sees registry = "my-private" and switches context. It fetches the index for my-private, finds version 0.1.0, downloads the crate, and verifies the hash. If the hash does not match, Cargo aborts the build. The index guarantees integrity.

The sparse protocol advantage

The sparse protocol is a key improvement over the old git-based index. A git-based index requires Cargo to clone the entire index repository. If your private registry hosts hundreds of crates, the index can grow large. Cloning it every time you build wastes bandwidth and time.

The sparse protocol allows Cargo to fetch only the specific directory for the crate you need. The index is organized by crate name hash. Cargo calculates the hash, requests that specific path, and gets the metadata instantly. This makes dependency resolution fast even for large registries.

If your registry server supports sparse protocol, use sparse+https://. If you are stuck with a legacy git-based index, use git+https://. The git protocol still works, but it is slower.

Convention aside: Check your registry provider's documentation. Most modern solutions like GitHub Packages, GitLab, or self-hosted solutions like cargo-registry support sparse protocol. If your provider offers sparse, enable it. The performance gain is immediate.

Authentication and secrets

Private registries require authentication. Cargo supports token-based auth. You provide a token in the registry section of your config. The token is sent with requests to the index and the download URL.

Never hardcode tokens in your config file. Use environment variables. Cargo expands ${VAR_NAME} syntax in config values. This keeps secrets out of version control and allows different environments to use different tokens.

# Set the token in your shell or CI environment.
export CARGO_REGISTRY_TOKEN="ghp_1234567890abcdef"

# Cargo reads the env var and uses it for auth.
cargo build

In CI pipelines, inject the token as a secret variable. The CI system sets the environment variable before running cargo. The config file references the variable. The token never appears in logs or artifacts.

Convention aside: Use CARGO_REGISTRY_TOKEN as the environment variable name. This is the standard name recognized by many tools and documentation. If your registry requires a different name, document it clearly for your team. Consistency reduces friction.

Real-world workflow

A typical workflow involves publishing crates to the private registry and consuming them in projects. Publishing usually happens in a CI pipeline. The pipeline builds the crate, runs tests, and then publishes it to the registry.

# CI script to publish a crate.
# The token is injected by the CI system.
export CARGO_REGISTRY_TOKEN="$PRIVATE_REGISTRY_TOKEN"

# Publish to the private registry.
# The --registry flag matches the config name.
cargo publish --registry my-private

Consuming the crate is straightforward. Developers add the dependency to Cargo.toml and run cargo build. Cargo handles the rest. The developer does not need to know the registry URL or the token. The project config handles that.

If you work on multiple projects with different private registries, use project-level .cargo/config.toml files. Each project defines its own registries. This avoids conflicts and keeps configuration isolated.

Common pitfalls

Cargo errors can be cryptic when registry configuration is wrong. Understanding the common failure modes helps you debug quickly.

If you forget to add the registry to config.toml, Cargo rejects the dependency with an error like error: failed to get billing-utils as a dependency of package. The error mentions the registry name is not found. Check your config file for typos in the registry name.

If the index URL is incorrect, Cargo fails to fetch metadata. You see error: failed to fetch https://.... Verify the URL in config.toml. Ensure it ends with / for sparse protocol. Check that the server is reachable.

If the token is missing or invalid, authentication fails. Cargo reports error: failed to authenticate. Ensure the environment variable is set. Check that the token has not expired. Some registries require specific scopes or permissions. Verify your token has read access to the registry.

If you mix sparse and git protocols incorrectly, Cargo may fail. Use sparse+https:// for sparse servers. Use git+https:// for git repositories. Do not mix them. The protocol prefix tells Cargo how to communicate with the index.

Convention aside: Run cargo fetch to test registry access without building. This command resolves dependencies and downloads metadata. It fails fast if the registry is unreachable or authentication is broken. Use it to verify your setup before running a full build.

Choosing your dependency strategy

Private registries are not the only way to share code. Rust offers several dependency mechanisms. Pick the right tool for the job.

Use a private registry when you need versioned, stable dependencies shared across multiple projects or CI pipelines. Registries provide versioning, integrity checks, and centralized management. They are the standard for internal libraries.

Use a path dependency when you are actively developing the crate alongside your project and need instant feedback. Path dependencies link directly to a local directory. Changes are reflected immediately without publishing. They are ideal for local development and monorepos.

Use a git dependency when you need a specific commit or branch from a public repository that has not been published yet. Git dependencies point to a git repository and a revision. They are useful for prototyping or consuming external forks. They lack the stability and caching of registries.

Use crates.io for public, reusable crates. Crates.io is the default registry. It provides a vast ecosystem of libraries. Publish here when your crate is useful to the broader community.

Where to go next