Configuring rust-analyzer in a DevContainer

When rust-analyzer analyzes against a different toolchain than cargo build, its errors stop matching reality. This page installs and routes rust-analyzer inside the devcontainer against the container's Rust, and tunes it so the editor and the compiler agree.

This matters because rust-analyzer is not a linter bolted onto your editor; it is a full incremental compiler front end that resolves types, expands macros, and evaluates cfg attributes exactly the way rustc does. If it runs against a host rustup while cargo build runs against the container's rustup, the two see different edition defaults, different feature resolvers, and sometimes entirely different crate versions from separate registry caches. The result is squiggly underlines on code that compiles cleanly, or a suspiciously green editor over code the container rejects. Neither failure is a bug in rust-analyzer — it is faithfully reporting what the toolchain it was pointed at believes, which is simply not the toolchain that ships your binary.

Reach for this configuration the moment your devcontainer's Rust stops being the only Rust on the machine. The mental model to hold is a single arrow: the extension process, the rustup proxy it invokes, the registry it reads sources from, and the target directory it indexes must all live on the same side of the container boundary. When VS Code installs the extension on the remote server rather than the local client, that arrow points entirely into the container, and cargo build and the editor become two views of one compilation rather than two independent compilations that happen to share a folder.

Prerequisites

You need a Rust devcontainer with the toolchain installed.

  • A Rust Feature pinned in the container.
  • The rust-analyzer extension installed in the container.
  • The Cargo registry/target available (ideally cached).

The pinned Rust Feature is the anchor for everything below: it is what puts a real rustup on the container's PATH, and rust-analyzer discovers its compiler by asking that rustup which rust-analyzer proxy to run. If the Feature only installs a bare cargo without the rustup shims, the rustup which resolution in step 2 has nothing to answer with and the extension quietly falls back to whatever binary it bundled. Pin a concrete channel (a dated nightly or a 1.x stable) rather than letting the Feature drift, so the editor and CI index the same standard library sources.

The detail people get wrong is thinking the extension list belongs to their user profile. An extension you installed locally does not automatically move into the container, and one added under a top-level extensions key rather than under customizations.vscode.extensions is silently ignored by the modern devcontainer spec. Put rust-lang.rust-analyzer in the container's manifest, rebuild, and confirm from the Extensions view that it shows as installed in the container and not merely locally — that badge is the difference between routing and not routing.

Analyzer prerequisitesInstall rust-analyzer in the container, route it, tune it, and verify.Install incontainerrust-analyzerRoute toolchaincontainer rustupTunecheck on saveVerifymatch cargo

Step-by-Step Implementation

  1. Install rust-analyzer into the container.
{
  "customizations": { "vscode": { "extensions": ["rust-lang.rust-analyzer"] } },
  "remoteUser": "vscode"
}

Listing the extension under customizations.vscode.extensions tells VS Code to install it on the remote server the first time the container is built, which is the whole point: the language server process then executes inside the container with the container's filesystem and PATH. The remoteUser line pairs with this because the extension writes its index and proc-macro server cache under that user's home; if the container runs the editor server as vscode but you later shell in as root, the two write to different target and cache paths and the analyzer re-indexes. Keeping a single remoteUser prevents that split and the redundant rebuilds it causes.

  1. Ensure it uses the container toolchain (rustup on PATH inside the container).
rustup which rust-analyzer   # resolves inside the container

Running rustup which rust-analyzer from a terminal inside the container is the fastest way to prove the routing is real, because rustup which prints the absolute path of the proxy target for the active toolchain. If it points somewhere under the container's ~/.rustup/toolchains/..., the server binary and the compiler binary come from the same rustup install and will agree on standard library and macro expansion. If the command errors or resolves to a host path, the extension is still running client-side and no amount of settings tuning will make its diagnostics match — you have to fix the install location first. This check prevents the most common failure mode, where the editor looks configured but is quietly analyzing against a stale host toolchain.

  1. Tune check-on-save to match your build command.
{ "customizations": { "vscode": { "settings": { "rust-analyzer.check.command": "clippy" } } } }

Setting rust-analyzer.check.command to clippy swaps the default cargo check invocation on save for cargo clippy, so the editor surfaces the exact lint set your build and CI enforce instead of a thinner subset. This is written as a container-scoped setting under customizations.vscode.settings rather than a user setting so that everyone who opens the project inherits it and no one has to remember to flip it locally. The failure it prevents is a quiet divergence where clippy passes in the editor because it never ran there, only for the same code to be rejected by cargo clippy in CI. If your build uses extra flags, match them with rust-analyzer.check.extraArgs so the on-save check and the pipeline stay byte-for-byte aligned rather than approximately aligned.

  1. Verify the editor's errors match cargo build.

With the extension installed in the container, the toolchain routed through the container's rustup, and the check command aligned, open a file with a known type error and confirm the squiggle appears, then remove it and confirm it clears within one save cycle. Run cargo build in the integrated terminal and compare: the diagnostic count and the specific messages should match line for line. If they do, the green checkmark in the editor is now load-bearing — it means the same compiler that ships your binary has already seen and accepted the code, so you are not deferring surprises to CI.

Analyzer routingrust-analyzer runs in the container against the container toolchain, matching cargo.In-container extensionruns on the serverContainer rustupsame toolchaincheck.commandmatches your buildResulteditor == compiler

Common Pitfalls

Analyzer mismatches come from a host toolchain or wrong check config.

When the analyzer's index and check outputs live on a cached target volume, ownership is the pitfall that bites hardest. If the volume was first written by a root process — say a postCreateCommand that ran a build before the editor server attached as vscode — the analyzer running as vscode cannot write fresh artifacts into it and either stalls or silently indexes against stale .rmeta files. Fix it by chowning the cache path to remoteUser in a postCreateCommand, and keep every build, whether from a lifecycle hook or the editor, running as the same user so the target directory has one consistent owner. A cache that only one identity can write is worse than no cache, because the analyzer keeps half-reading it.

A subtler topic-specific pitfall is proc-macro and build-script drift. rust-analyzer runs a separate proc-macro server that must be built by the same toolchain as your crates; if it was compiled by a different rustup channel than the one cargo build uses, macro-heavy code lights up with spurious "proc-macro not expanded" errors even though the compiler is happy. The tell is that errors cluster around #[derive(...)] and attribute macros while plain functions analyze fine. Realign by making the analyzer's toolchain identical to the build's — the same rustup which resolution from step 2 — and, after any channel bump, reload the window so the proc-macro server is rebuilt against the new toolchain rather than reused from the old one.

Analyzer triageA triage path from analyzer/compiler disagreement to aligned diagnostics.Does the analyzer use the containerRust?NOInstall it in the containerDo errors match cargo?YEScheck.command alignedEditor matches build

SymptomRoot CauseRemediation
Editor errors differ from cargoAnalyzer on the wrong toolchainInstall/route it in the container
Clippy lints missing in editorcheck.command not set to clippySet rust-analyzer.check.command
Slow first analysisIndex rebuilt each attachCache target/registry so it warms fast
Proc-macro errorsToolchain/proc-macro mismatchMatch the analyzer toolchain to the build

Conclusion

rust-analyzer only helps when it sees what the compiler sees. Install it into the container, let it use the container's rustup toolchain, and align its check command with your build — then its diagnostics match cargo build and the green checkmark means something.

The strategic payoff is that a routed analyzer collapses the feedback loop that reproducibility work is meant to protect. Pinning a Rust Feature and caching the registry and target already guarantee that every contributor compiles against the same standard library and dependency graph; pointing the analyzer at that exact toolchain extends the guarantee up into the editor, so the code review someone does at a squiggle is a review of the real compiler's opinion, not a nearby approximation. The check.command alignment is the same idea applied to lints: clippy on save is CI's gate moved to the moment of typing.

Read alongside the caching page, this configuration is the last mile of a single pin-and-cache story. The pinned toolchain fixes which compiler runs, the cached target and registry make the analyzer's index warm on the second attach instead of recompiling from scratch, and the in-container routing makes sure the thing consuming that warm cache is the editor you actually look at. When all three hold, opening the project is deterministic: the same inputs produce the same diagnostics on every machine, and "works in my editor" stops being a caveat and becomes a fact about the toolchain.

Route and tuneRoute rust-analyzer to the container toolchain and tune its check command.RouteIn-container installContainer rustupSame registry/targetTunecheck.commandclippy on saveCached index

FAQ

Why does rust-analyzer disagree with cargo? Because it is resolving against a different toolchain — often a host rustup — than the compiler. Install the extension into the container so it runs on the server against the container's rustup, registry, and target, and the two converge. The disagreement is usually not random noise: it tracks whatever the host toolchain differs on, such as a newer edition default, a different feature resolver version, or a crate that resolved to another semver-compatible release from a separate registry cache. Once both sides read the same Cargo.lock through the same rustup, the mismatched diagnostics disappear because there is genuinely only one compilation being described.

How do I get clippy lints in the editor? Set rust-analyzer.check.command to clippy so on-save checks run clippy rather than plain cargo check. Then the editor surfaces the same lints your cargo clippy build enforces, keeping local and CI consistent. Scope the setting under customizations.vscode.settings in the devcontainer manifest so every contributor inherits it without touching their personal config. If your CI runs clippy with --all-targets or a -D warnings deny level, mirror those through rust-analyzer.check.extraArgs; otherwise the editor can stay green on a warning that the pipeline treats as a hard failure.

Why is the first analysis slow after a rebuild? rust-analyzer builds an index that depends on compiled artifacts. Cache the target and registry directories on named volumes so a rebuild reuses them, and the analyzer warms up quickly instead of recompiling to index. The slowness is real work: the first index has to build the proc-macro server and read metadata for every dependency, which on a cold target means recompiling much of the graph. A persisted, correctly-owned cache turns the second attach into a metadata read rather than a rebuild, which is why the ownership pitfall above matters so much for perceived speed.

Should I install rust-analyzer locally as well as in the container? Only the in-container install does the analysis for a container project, so a local copy is not required and can be actively confusing. If both are present, VS Code still runs the remote one for files inside the container, but the local one may light up on any file you open outside the workspace, which invites the "wrong toolchain" symptom right back. Keep the extension in the devcontainer manifest and let the local client stay thin; the badge in the Extensions view showing it installed in the container is all you need.

How do I point the analyzer at a non-default toolchain? If your project pins a channel through a rust-toolchain.toml, rust-analyzer honours it automatically because it invokes cargo and rustc through the same rustup proxies that read that file. When you need to override it explicitly — for example to index against nightly while building on stable — set rust-analyzer.rustc.source or the toolchain override in the container's settings rather than the host's, so the choice travels with the project. Verify the result the same way as always, with rustup which rust-analyzer and a diff against cargo build.

Why do proc-macro errors appear only in the editor? The analyzer's proc-macro server is a separate binary that must be built by the same toolchain as your crates; when it lags a channel bump it fails to expand derives and attribute macros even though cargo build expands them fine. The clue is that errors cluster on #[derive(...)] while ordinary code analyzes cleanly. Reload the window after any toolchain change so the proc-macro server is rebuilt against the current rustup, and confirm the analyzer's toolchain matches the build's through the step 2 resolution.