How to Publish Documentation to docs.rs

Publish your crate to crates.io to automatically generate and host documentation on docs.rs.

The moment your code leaves your machine

You just finished a crate. It compiles. The tests pass. You push it to GitHub and feel that familiar surge of completion. Then you realize nobody will read your source code. Developers scan documentation. They skim examples. They copy paste snippets. If your crate lives only in a repository, it stays unused. Publishing to docs.rs turns your private notes into a public reference that the entire Rust ecosystem can search, link, and trust.

How the pipeline actually works

docs.rs is not a separate hosting service you configure manually. It is a read-only mirror that reacts to crates.io. When you publish a crate, crates.io sends a webhook to the docs.rs build farm. The farm pulls your source, spins up an isolated container, runs cargo doc, captures the generated HTML, and uploads it to a permanent URL. The process is automatic, versioned, and tied to your crate's release cycle.

Think of it like a public library that automatically prints and shelves books whenever an author releases a new edition. You do not walk into the library and hand over the manuscript. You publish the book. The library handles the rest. Your job is to make sure the manuscript is clean, the chapters are labeled, and the table of contents makes sense.

The build farm runs the latest stable Rust toolchain. It caches dependency downloads to keep build times under two minutes. It strips private items from the output. It generates search indexes so developers can jump straight to a function without scrolling. You do not configure any of this. You only control what goes into the source.

Treat the build farm as a strict auditor. It will not guess your intent. It will render exactly what you give it.

The local verification step

Never publish documentation you have not seen yourself. The command cargo doc --no-deps builds only your crate's documentation, skipping every external dependency. This keeps the build fast and the output focused on your public API.

// src/lib.rs
/// A simple counter that tracks how many times an event occurs.
///
/// # Examples
///
/// ```
/// use my_crate::Counter;
/// let mut c = Counter::new();
/// c.increment();
/// assert_eq!(c.value(), 1);
/// ```
pub struct Counter {
    count: u64,
}

impl Counter {
    /// Creates a new counter starting at zero.
    pub fn new() -> Self {
        // Initialize with zero to avoid uninitialized memory
        Self { count: 0 }
    }

    /// Increases the counter by one.
    pub fn increment(&mut self) {
        // Direct mutation is safe because we hold &mut self
        self.count += 1;
    }

    /// Returns the current count.
    pub fn value(&self) -> u64 {
        // Read-only access prevents accidental state changes
        self.count
    }
}

Run cargo doc --open in your terminal. The browser opens to file:///target/doc/my_crate/index.html. Scroll through the generated pages. Check that every public function has a doc comment. Verify that the code examples compile. If the examples fail, cargo test --doc will catch them before you publish. The documentation system treats examples as first-class tests. They run in a separate crate context, which means they must import everything explicitly.

Treat the local preview as your final proofread. The build farm will not fix missing explanations or broken links.

Structuring your documentation for the ecosystem

Rust's documentation system relies on doc comments that compile into HTML. The /// syntax attaches to public items. The //! syntax attaches to modules or crates. You can use markdown, code blocks, and special attributes to shape the output.

// src/lib.rs
//! A lightweight counter library for tracking discrete events.
//!
//! This crate provides a thread-safe counter when used with `Arc<Mutex<Counter>>`,
//! but the base type is intentionally simple and fast.

/// Represents a single counting operation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CountAction {
    Increment,
    Decrement,
    Reset,
}

/// Applies an action to the counter.
///
/// # Panics
///
/// Panics if the action is `Decrement` and the counter is already at zero.
pub fn apply_action(counter: &mut Counter, action: CountAction) {
    match action {
        CountAction::Increment => counter.increment(),
        CountAction::Decrement => {
            if counter.value() == 0 {
                panic!("Cannot decrement a counter at zero");
            }
            counter.count -= 1;
        }
        CountAction::Reset => counter.count = 0,
    }
}

The # Examples, # Panics, and # Safety sections are not just formatting. They are structural markers that rustdoc uses to generate collapsible sections and validation checks. If you mark a function as # Safety, you must wrap it in unsafe and provide a // SAFETY: comment explaining the invariants. The documentation system enforces this discipline.

Convention aside: use #![doc = include_str!("README.md")] at the top of lib.rs to pull your repository's README into the generated docs. It keeps your crate's landing page in sync with your GitHub page. The community expects this pattern for public crates. It also saves you from duplicating installation instructions and feature lists.

Use #[doc(alias = "old_name")] when you rename a public function. It preserves search results and IDE autocomplete for users still reading older tutorials. Use #[doc(hidden)] for internal helpers that must remain public for macro expansion or trait bounds but should not appear in the public API. The documentation system respects both attributes without breaking compilation.

Common pitfalls and how the compiler catches them

Documentation builds fail for predictable reasons. The most frequent issue is broken intra-doc links. If you write See [String] for details but String is not in scope, rustdoc emits a warning. Treat warnings as errors during the publish step by adding #![deny(rustdoc::broken_intra_doc_links)] to your crate root.

Another trap is version skew. Your local Rust version might be newer than the docs.rs build environment. If you use a nightly-only feature, the public build will fail. Stick to stable attributes for public crates. The docs.rs farm runs the latest stable toolchain, but it updates weekly. If your code requires a specific compiler feature, declare it in Cargo.toml with rust-version = "1.75".

[package]
name = "my_crate"
version = "0.1.0"
edition = "2021"
rust-version = "1.75"
license = "MIT OR Apache-2.0"
description = "A lightweight counter library"
documentation = "https://docs.rs/my_crate"
repository = "https://github.com/username/my_crate"

The documentation field does not trigger the build. It is a metadata pointer that IDEs and package managers read. Update it after your first successful publish. If you leave it blank, developers searching for your crate will find a dead link in their tooling.

Watch for E0599 (no method found) errors in doc examples. If your example references a private helper or forgets to import a trait, rustdoc compiles the example in a separate crate context. It does not inherit your module's private scope. Add explicit use statements inside the example blocks. The compiler will also reject examples with E0432 (unresolved import) if you typo a module path. Fix the import locally before pushing.

Convention aside: run cargo package before cargo publish. It creates a local tarball identical to what crates.io receives. Inspect the contents. Make sure .gitignore did not accidentally exclude a critical file. If the tarball is wrong, the published version is wrong.

When to use docs.rs versus alternatives

Use docs.rs when you publish a public crate to crates.io and want zero-maintenance hosting. Use cargo doc locally when you are iterating on API design and need instant feedback without uploading anything. Use a static site generator like MkDocs or Docusaurus when your project requires tutorials, blog posts, or interactive playgrounds that go beyond API reference. Use GitHub Pages when you want full control over the build pipeline and custom domain routing. Reach for docs.rs for the standard Rust ecosystem experience. It integrates with cargo search, IDE plugins, and the package registry automatically.

Trust the automation. Keep your doc comments precise. Let the build farm do the heavy lifting.

Where to go next