How to Vendor Dependencies in Rust with Cargo

Download dependencies to a local vendor directory using cargo vendor and configure Cargo.toml to use them.

The offline build problem

You are building a CLI tool for a company that sits behind a strict firewall. The developers have no internet access. You hand them the source code and tell them to run cargo build. The build fails immediately. Cargo tries to reach crates.io, gets blocked, and gives up. You could zip up the .cargo/registry folder, but that breaks the moment someone adds a new dependency or updates a version. The clean solution is vendoring. You pull every dependency into the repository itself, so the code builds anywhere, anytime, with zero network calls.

What vendoring actually does

Vendoring means copying your project's external dependencies directly into your repository. Instead of telling Cargo to fetch crates from the internet, you point it at a local folder that already contains the source code. Think of it like packing a fully stocked kitchen in a camper van. When you park in the middle of nowhere, you do not need a grocery store. Everything you need is already in the cabinets. In Rust, that cabinet is the vendor directory.

Cargo handles this through a source replacement mechanism. By default, Cargo trusts crates.io as the primary source. You override that trust by telling Cargo to ignore the network source and read from a local directory instead. The crates inside that directory are treated exactly like regular dependencies. The only difference is where Cargo looks for the .crate files.

There is a community convention worth noting here. The standard folder name is vendor. You could call it deps or lib, but vendor is what every Rust developer expects. It also matches the output of cargo vendor by default. Stick with it to avoid confusing your teammates and to keep your CI scripts predictable.

The minimal setup

The process takes two steps. First, you run the vendoring command. Second, you update your configuration.

# Downloads every dependency into a local vendor folder
cargo vendor vendor

That command creates a vendor directory filled with subdirectories for each crate. Each subdirectory contains the exact source code, Cargo.toml, and build scripts for that specific version. You then add a source override to your Cargo.toml.

[source.crates-io]
# Tell Cargo to stop looking at the network registry
replace-with = "vendored-sources"

[source.vendored-sources]
# Point to the local folder you just created
directory = "vendor"

Run cargo build again. Cargo skips the network entirely and compiles straight from the local files. The build works offline.

How Cargo resolves vendored crates

Here is what happens under the hood when you run that build. Cargo reads your Cargo.toml and sees the [source.crates-io] override. It immediately drops the default crates.io registry from its resolution graph. When it needs to resolve serde or tokio, it checks the vendored-sources directory instead. It finds the matching version folder, reads the Cargo.toml inside it, and continues dependency resolution locally.

The resolution process remains identical to a normal build. Feature flags still activate. Build scripts still run. The only change is the file path. Cargo treats the vendored directory as a local registry. It even handles transitive dependencies correctly because cargo vendor pulls the entire dependency tree, not just the direct ones.

Cargo also respects the lockfile. When you run cargo vendor, it reads Cargo.lock to determine exactly which versions to download. This guarantees that the vendored folder matches your current build state. If you delete Cargo.lock and run cargo vendor again, you might get different versions if upstream crates have released updates. Always keep Cargo.lock committed alongside your vendor directory.

There is a subtle distinction between [source] and [patch]. The [source] section replaces an entire registry. The [patch] section swaps individual crates. Beginners often confuse them. If you want to replace the entire crates.io registry with a local folder, you use [source]. If you want to override just one crate with a local path or a git repository, you use [patch]. They serve different purposes. Do not mix them unless you understand the resolution order.

A realistic project structure

In a real project, you rarely vendor just for offline builds. You usually vendor for reproducible CI pipelines or strict compliance environments. Here is how a production setup looks.

# Cargo.toml
[package]
name = "internal-tool"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
serde = { version = "1.0", features = ["derive"] }

[source.crates-io]
# Override the default registry to prevent network calls
replace-with = "vendored-sources"

[source.vendored-sources]
# Reference the local directory containing all crate sources
directory = "vendor"

You commit the vendor directory to git. This makes the repository larger, but it guarantees that every clone builds identically. No more "it works on my machine because I have a cached registry" issues. Every developer and every CI runner gets the exact same source code for every dependency.

You also need to handle the gitignore correctly. By default, .gitignore excludes vendor/. You must remove that line or add an explicit override.

# .gitignore
target/
.cargo/
# Remove or comment out the default vendor exclusion
# vendor/

Git also struggles with large vendor directories. A typical Rust project might pull in fifty to one hundred crates. That adds megabytes to your repository. If your CI system clones the full history, build times will slow down. The standard workaround is to use shallow clones in CI or switch to a monorepo strategy where you only vendor the crates that actually change. You can also use git lfs for the vendor directory if your repository grows too large for standard git storage.

Pitfalls and compiler friction

Vendoring introduces a few friction points. The most common is stale dependencies. If you update a version in Cargo.toml and run cargo build without re-vendoring, Cargo will fail. It cannot find the new version in the local folder. Cargo will complain that it cannot find a valid source for the updated version. Always run cargo vendor again after changing your dependency graph.

Another trap is build scripts. Some crates use build.rs to fetch external assets or compile C code. Vendoring does not change how those scripts run. If a build script tries to download a header file from GitHub, your offline build will still fail. Vendoring only covers Rust source code and the Cargo.toml metadata. It does not vendor external network calls inside build scripts. You have to fix those scripts separately or accept that they require network access.

There is also a subtle interaction with [patch]. The [patch] section overrides specific crates with local paths or git repositories. It does not replace the entire registry. If you mix [patch] with [source.crates-io] replacement, Cargo applies the patch after resolving the vendored sources. This works fine, but it can confuse beginners who expect [patch] to override the vendoring configuration. They are separate mechanisms. One replaces the registry. The other swaps individual crates.

Cargo also caches compiled artifacts in the target directory. When you switch between vendored and non-vendored builds, you should clear the target folder. Otherwise, Cargo might reuse stale artifacts and produce confusing linker errors. Run cargo clean when toggling the source override. This forces a fresh compilation and eliminates hidden state.

Treat the vendor directory like a snapshot. It captures your dependency tree at a specific moment. Update it deliberately, commit it consistently, and never assume it stays in sync with your Cargo.toml on its own.

When to vendor and when not to

Use vendoring when you need guaranteed offline builds for air-gapped machines or strict firewall environments. Use vendoring when you want reproducible CI pipelines that do not depend on crates.io availability or network latency. Use vendoring when compliance policies require every dependency to be stored and audited inside your repository. Reach for standard crates.io resolution when you are building a public library or an open-source tool where repository size and clone speed matter more than offline capability. Reach for [patch] when you only need to override a single crate with a local fork or a git branch. Keep the registry intact for everything else.

Where to go next