Erstellen minimaler und sicherer Rust-Webanwendungen mit Docker
Olivia Novak
Dev Intern · Leapcell

Einleitung
In der Welt der Cloud-Native-Entwicklung ist Docker zu einem unverzichtbaren Werkzeug für das Verpacken und Bereitstellen von Anwendungen geworden. Für auf Rust basierende Webanwendungen ist das Streben nach schlanken, sicheren und effizienten Deployments besonders wichtig. Während Rust für seine Leistung und Speichersicherheit gefeiert wird, können die resultierenden Docker-Images manchmal größer sein als erwartet, was potenziell zu erhöhten Angriffsflächen und längeren Deployment-Zeiten führt. Dieser Artikel befasst sich damit, wie wir erweiterte Docker-Techniken – insbesondere Multi-Stage-Builds und Distroless-Images – nutzen können, um den Footprint unserer Rust-Webanwendungscontainer dramatisch zu verkleinern und ihre Sicherheit zu erhöhen. Dadurch optimieren wir nicht nur die Ressourcennutzung, sondern schaffen auch eine widerstandsfähigere und vertrauenswürdigere Deployment-Umgebung, die uns dem Ideal wirklich "produktionsreifer" Anwendungen näher bringt.
Die Grundlagen verstehen
Bevor wir uns mit den praktischen Aspekten befassen, wollen wir ein gemeinsames Verständnis der Kernkonzepte schaffen, die unserer Strategie für den Aufbau minimaler und sicherer Rust-Docker-Images zugrunde liegen.
Docker-Images und Layer: Ein Docker-Image ist ein leichtgewichtiges, eigenständiges, ausführbares Softwarepaket, das alles enthält, was zur Ausführung einer Anwendung benötigt wird: Code, Laufzeitumgebung, Systemwerkzeuge, Bibliotheken und Einstellungen. Images werden aus einer Reihe von Layern aufgebaut, wobei jede Anweisung in einem Dockerfile einen neuen Layer erstellt. Die Wiederverwendung von Layern kann den Build-Prozess beschleunigen und die Image-Größen reduzieren.
Rust Toolchain: Das Rust-Ökosystem stellt rustc (den Compiler), cargo (das Build-System und den Paketmanager) sowie verschiedene andere Werkzeuge bereit. Für die Erstellung von Anwendungen sind diese Werkzeuge unerlässlich, werden aber zur Laufzeit nicht benötigt.
Multi-Stage Builds: Diese Docker-Funktion ermöglicht die Verwendung mehrerer FROM-Anweisungen in Ihrem Dockerfile. Jede FROM-Anweisung startet eine neue Build-Phase, die als "FROM stage" benannt ist. Sie können Artefakte selektiv von einer Phase in eine andere kopieren und alles andere effektiv verwerfen. Dies ist eine leistungsstarke Technik, um Build-Zeit-Abhängigkeiten von Laufzeit-Abhängigkeiten zu trennen.
Distroless Images: Distroless-Images, entwickelt von Google, sind "sprachspezifische Basis-Images, die nur Ihre Anwendung und ihre Laufzeitabhängigkeiten enthalten." Sie enthalten fast keine Betriebssystemkomponenten, Paketmanager, Shells oder andere Dienstprogramme, die üblicherweise in Standard-Basis-Images wie Ubuntu oder Alpine zu finden sind. Der Hauptvorteil ist eine erhebliche Reduzierung der Image-Größe und eine viel kleinere Angriffsfläche.
Angriffsfläche: Dies bezieht sich auf die Summe aller Punkte, an denen ein unbefugter Benutzer versuchen kann, Daten in eine Umgebung einzugeben oder daraus zu extrahieren. Durch die Reduzierung der Anzahl unnötiger Komponenten in einem Docker-Image verkleinern wir inhärent die Angriffsfläche, was es für böswillige Akteure schwieriger macht, bekannte Schwachstellen in Systembibliotheken oder Dienstprogrammen auszunutzen.
Erstellen von minimalen und sicheren Docker-Images
Unser Ziel ist es, ein Docker-Image für eine einfache Rust-Webanwendung unter Verwendung des Actix Web Frameworks zu erstellen. Wir werden sowohl Multi-Stage-Builds als auch Distroless-Images demonstrieren, um unser Ziel zu erreichen.
Die Rust-Webanwendung
Beginnen wir mit einer einfachen Actix Web-Anwendung. Erstellen Sie ein neues Rust-Projekt:
car go new --bin my-rust-app cd my-rust-app
Fügen Sie actix-web zu Ihrer Cargo.toml hinzu:
# Cargo.toml [package] name = "my-rust-app" version = "0.1.0" edition = "2021" [dependencies] actix-web = "4" tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Ersetzen Sie nun den Inhalt von src/main.rs mit einem einfachen "Hello, world!"-Server:
// src/main.rs use actix_web::{get, App, HttpServer, Responder}; #[get("/")] async fn hello() -> impl Responder { "Hello from Rust Web App!" } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(hello) }) .bind(("0.0.0.0", 8080))? .run() .await }
Diese Anwendung lauscht auf Port 8080 und antwortet auf dem Root-Pfad mit "Hello from Rust Web App!".
Multi-Stage Build: Der erste Schritt zur Minimierung
Ein Multi-Stage-Build ist entscheidend, um die ressourcenintensive Build-Umgebung von der schlanken Laufzeitumgebung zu trennen.
Erstellen wir eine Dockerfile im Stammverzeichnis unseres Projekts:
# Dockerfile # Stage 1: Builder # Verwenden Sie ein spezifisches Rust-Image als unsere Build-Umgebung. # Wir wählen oft eine 'buster'- oder 'bullseye'-Variante für eine breitere Kompatibilität # während der Kompilierung, aber 'slim' oder 'alpine' können auch verwendet werden, wenn die Abhängigkeiten minimal sind. FROM rust:1.75-bookworm AS builder # Legen Sie das Arbeitsverzeichnis im Container fest WORKDIR /app # Kopieren Sie zuerst Cargo.toml und Cargo.lock, damit Docker Abhängigkeiten cachen kann # Wenn sich diese Dateien nicht ändern, können nachfolgende Builds den gecachten Layer wiederverwenden COPY Cargo.toml Cargo.lock ./ # Erstellen Sie Abhängigkeiten (leeres main.rs zum Erzwingen des reinen Abhängigkeits-Builds) # Dieser Schritt ist entscheidend für Caching-Layer-Optimierungen. # Wenn sich die Abhängigkeiten nicht geändert haben, wird dieser Layer wiederverwendet. RUN mkdir src \ && echo "fn main() {}" > src/main.rs \ && cargo build --release \ && rm -rf src # Kopieren Sie den tatsächlichen Quellcode COPY src ./src # Erstellen Sie die Release-Binärdatei # Wir verwenden target/release, da cargo build --release die Binärdatei dort platziert RUN cargo build --release # Stage 2: Runner # Dies ist unsere Laufzeit-Phase. Wir verwenden ein Debian 'buster-slim'-Image, # das viel kleiner ist als das vollständige Rust-Image, aber immer noch grundlegende libc enthält. FROM debian:bookworm-slim AS runner # Legen Sie das Arbeitsverzeichnis fest WORKDIR /app # Kopieren Sie die kompilierte Binärdatei aus der 'builder'-Phase # Stellen Sie sicher, dass der Binärname mit Ihrem Projektnamen übereinstimmt (my_rust_app) COPY /app/target/release/my-rust-app ./my-rust-app # Exponieren Sie den Port, auf dem die Anwendung lauscht EXPOSE 8080 # Führen Sie die Anwendung aus CMD ["./my-rust-app"]
Erläuterung:
-
builder-Phase:FROM rust:1.75-bookworm AS builder: Wir beginnen mit einem offiziellen Rust-Image, dasrustc,cargound alle notwendigen Build-Tools enthält. Wir nennen diese Phasebuilder.WORKDIR /app: Legt das Arbeitsverzeichnis fest.COPY Cargo.toml Cargo.lock ./: Kopiert nur die Manifestdateien. Dieser wichtige Schritt ermöglicht es Docker, den Befehlcargo build --releasefür Abhängigkeiten zu cachen, wennCargo.tomlundCargo.locknicht geändert wurden, was die Wiederaufbauzeiten erheblich beschleunigt.RUN mkdir src && echo "fn main() {}" > src/main.rs && cargo build --release && rm -rf src: Dies ist ein Trick, um nur Abhängigkeiten zu erstellen. Wir erstellen ein Dummy-main.rs, erstellen das Projekt (was Abhängigkeiten auflöst und kompiliert) und entfernen dann das Dummy. Dies stellt sicher, dass die Abhängigkeits-Schicht gecacht wird.COPY src ./src: Kopiert den tatsächlichen Quellcode.RUN cargo build --release: Kompiliert unsere Anwendung zu einer optimierten Release-Binärdatei.
-
runner-Phase:FROM debian:bookworm-slim AS runner: Wir wechseln zu einem viel kleineren Basis-Image,debian:bookworm-slim. Dieses Image bietet nur das Nötigste, hauptsächlichlibc, auf das Rust-Binärdateien angewiesen sind. Wir nennen diese Phaserunner.WORKDIR /app: Gleiches Arbeitsverzeichnis.COPY --from=builder /app/target/release/my-rust-app ./my-rust-app: Dies ist das Herzstück von Multi-Stage-Builds. Wir kopieren nur die kompilierte Binärdatei aus derbuilder-Phase in unsere schlankerunner-Phase. Alle Build-Tools, Quellcodes und Zwischenartefakte aus derbuilder-Phase werden verworfen.EXPOSE 8080: Dokumentiert den Port, auf dem die Anwendung lauscht.- `CMD [

