Table of Contents
Nixify Your Leptos Website and Stop Compiling Your Tools
Every time I ran CI on our Leptos website, I watched 19 minutes of my life drain away. A big chunk of that time was spent compiling the tools that compile our code. cargo install cargo-leptos, cargo install wasm-bindgen-cli, downloading Node from a sketchy shell-pipe curl | bash, running apt-get update to install libssl-dev and friends. Every. Single. Build.
This is insane. These tools don't change between builds. They're the same binaries every time. Yet we were compiling them from source on every CI run and every Docker build like it was 2016 and we all had Ubuntu Xenial laptops.
So I ripped it all out and replaced it with a Nix flake. Build time went from 19 minutes to 5 minutes. And I haven't even started using Nix to cache compiled Rust crate dependencies yet.
The Before: A Horror Story in YAML and Dockerfile
The Dockerfile was grim:
FROM rust:1.83-slim-bookworm as builder
RUN rustup default nightly-2024-11-01
RUN apt-get update && apt-get install -y \
libssl-dev pkg-config g++ git-all curl \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs
RUN cargo install --locked cargo-leptos@0.2.29
RUN cargo install wasm-bindgen-cli@0.2.100 --locked
Piping a shell script from deb.nodesource.com into bash as root inside a Docker build. Compiling cargo-leptos and wasm-bindgen-cli from source every time the layer cache misses. This is how the industry works and it's embarrassing.
The After: A Clean flake.nix
Here's the entire flake.nix:
{
description = "videocall-rs - WebTransport video calling platform";
inputs = {
nixpkgs.url =
"github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
rustNightly = pkgs.rust-bin.nightly."2024-11-01".default.override {
targets = [ "wasm32-unknown-unknown" ];
extensions = [ "rust-src" "rust-analyzer" ];
};
in {
devShells.leptos-website = pkgs.mkShell {
nativeBuildInputs = [
rustNightly
pkgs.cargo-leptos
pkgs.wasm-bindgen-cli_0_2_100
pkgs.nodejs_20
pkgs.binaryen
pkgs.pkg-config
pkgs.openssl
pkgs.git
];
LEPTOS_HASH_FILES = "false";
LEPTOS_TAILWIND_VERSION = "v3.4.17";
};
devShells.default = self.devShells.${system}.leptos-website;
});
}
Every tool version is pinned. cargo-leptos, wasm-bindgen-cli, Node 20, the exact Rust nightly — all declared in one place, all pulled as pre-built binaries from the Nix binary cache. No compilation. No apt-get. No curl | bash.
The New CI: 38 Lines
The entire CI workflow collapsed from 99 lines to 38:
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Cache cargo dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
leptos-website/target
key: ${{ runner.os }}-cargo-leptos-${{ hashFiles('leptos-website/Cargo.lock') }}
- name: Build Leptos website
run: |
nix develop .#leptos-website --command bash -c "\
cd leptos-website && \
npm install && \
cargo leptos build --release"
Two Nix actions install Nix and set up the cache. One cargo cache for actual project dependencies. One build step. That's it.
The magic-nix-cache-action is the secret weapon here — it transparently caches all the Nix store paths that nix develop pulls, so subsequent CI runs get the entire toolchain in seconds instead of downloading from cache.nixos.org.
The New Dockerfile: No apt-get, No cargo install
FROM nixos/nix:2.33.2 AS builder
ENV NIX_CONFIG="experimental-features = nix-command flakes"
WORKDIR /app
COPY flake.nix flake.lock ./
RUN git init && git add flake.nix flake.lock
RUN nix develop .#leptos-website --command true
COPY leptos-website/ leptos-website/
RUN git add leptos-website/
RUN nix develop .#leptos-website --command bash -c "\
cd leptos-website && \
npm install && \
cargo leptos build --release"
FROM debian:bookworm-slim
COPY --from=builder /app/leptos-website/target/release/leptos_website /app/
COPY --from=builder /app/leptos-website/target/site /app/site
COPY --from=builder /app/leptos-website/Cargo.toml /app/
WORKDIR /app
ENV RUST_LOG="info"
ENV LEPTOS_SITE_ADDR="0.0.0.0:8080"
ENV LEPTOS_SITE_ROOT="site"
EXPOSE 8080
CMD ["/app/leptos_website"]
The COPY flake.nix flake.lock + RUN nix develop --command true pattern is the key move. It downloads all tools from the Nix binary cache and Docker caches this layer. As long as flake.nix and flake.lock don't change, this layer is instant. Your actual code changes only trigger the final build step.
The git init && git add is a quirk — Nix flakes require files to be tracked by Git to be visible. Small price to pay.
Why This Works: Nix Binary Cache vs. cargo install
cargo install cargo-leptos downloads the source code for cargo-leptos and all its dependencies, then compiles the whole thing from scratch. 10+ minutes on CI hardware. Every time.
Nix doesn't do this. When you declare pkgs.cargo-leptos in your flake, Nix checks cache.nixos.org for a pre-built binary that matches the exact nixpkgs revision you pinned. If it exists (it almost always does for packages in nixpkgs), it downloads the binary. Done. Seconds, not minutes.
apt-get works the same way — pre-built binaries. But apt-get can't give you cargo-leptos or wasm-bindgen-cli at specific versions. Nix can, because nixpkgs is a massive repository of build recipes that doubles as a binary cache. Same reproducibility as building from source, but you're downloading a binary.
What I Haven't Done Yet
The 5-minute build time is still mostly spent compiling our own Rust code. I haven't set up Nix to cache compiled crate dependencies yet. Tools like crane or naersk can build a Nix derivation of just your Cargo dependencies, cache that in the Nix store, and only recompile your actual source files on changes.
That's the next step. I expect it'll shave off another 2-3 minutes.
The Takeaway
If your Rust CI pipeline spends more time installing tools than compiling your code, you're doing it wrong. A single flake.nix replaces:
actions-rs/toolchain— Nix provides the exact Rust nightly with WASM targetscargo install cargo-leptos— pre-built binary from nixpkgscargo install wasm-bindgen-cli— pre-built binary from nixpkgssetup-node—pkgs.nodejs_20apt-get install libssl-dev pkg-config g++—pkgs.opensslandpkgs.pkg-configcurl | bashfor Node — gone forever
One file. All versions pinned. All tools cached as binaries. 19 minutes down to 5.
PR #631 has the full diff if you want to steal this for your own project.