Introducción: ¿por qué un desarrollador de Rust necesita Docker?
Rust es famoso por su rendimiento y seguridad de memoria, pero incluso el binario más eficiente requiere un entorno adecuado para ejecutarse. Docker resuelve este problema empaquetando la aplicación junto con todas sus dependencias en un contenedor aislado. En este artículo, analizaremos cómo contenerizar aplicaciones Rust con Docker, utilizando técnicas modernas, incluyendo la compilación multi-etapa y la minimización del tamaño de la imagen.
La contenerización de proyectos Rust es especialmente ventajosa porque Rust se compila en un binario estático — esto permite crear imágenes mínimas (alpine o incluso scratch) que se ejecutan más rápido y son más seguras que sus equivalentes en Python o Node.js.
1. Fundamentos: Dockerfile para una aplicación Rust simple
Empecemos con un servidor web simple en Rust utilizando el framework actix-web. Crearemos un Dockerfile básico.
1.1. Estructura del proyecto
my_rust_app/
├── Cargo.toml
├── Cargo.lock
└── src/
└── main.rs
1.2. Ejemplo de código (main.rs)
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("¡Hola desde el contenedor Rust!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(hello))
.bind("0.0.0.0:8080")?
.run()
.await
}
1.3. Dockerfile simple
FROM rust:1.77 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
COPY src/ src/
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/my_rust_app /usr/local/bin/
EXPOSE 8080
CMD ["my_rust_app"]
Explicación: la primera etapa (builder) compila la aplicación, la segunda etapa copia solo el binario a una imagen Debian limpia. Esto reduce el tamaño de ~2 GB a ~100 MB.
2. Compilación multi-etapa: optimización del tamaño de la imagen
La compilación multi-etapa (multi-stage build) es una técnica clave para Rust. Permite separar el entorno de compilación (con el compilador, dependencias) del entorno de ejecución.
2.1. Dockerfile avanzado con caché
Para acelerar las compilaciones repetidas, almacenamos en caché las dependencias:
FROM rust:1.77 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release 2>/dev/null || true
COPY src/ src/
RUN cargo build --release && \
cp target/release/my_rust_app /app/rust_app
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /app/rust_app /app/rust_app
EXPOSE 8080
CMD ["/app/rust_app"]
¿Por qué distroless? La imagen distroless contiene solo glibc y bibliotecas básicas — sin shell, apt, ls. Esto aumenta la seguridad y reduce el tamaño a ~30 MB.
2.2. Imagen mínima con scratch
Si tu aplicación no usa libc (por ejemplo, escrita con #![no_std]), puedes usar scratch:
FROM rust:1.77 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/my_rust_app /app
EXPOSE 8080
CMD ["/app"]
¡Esta imagen pesa menos de 10 MB!
3. Docker Compose: aplicación multi-contenedor
A menudo, una aplicación Rust funciona junto con PostgreSQL, Redis o Nginx. Usamos docker-compose.yml:
3.1. Ejemplo de docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
- RUST_LOG=info
depends_on:
- db
db:
image: postgres:16-alpine
```