Desplegar una aplicación Rust consiste en compilar un binario de producción y ejecutarlo con la configuración adecuada.
Esa frase suena simple, y en parte lo es. Rust genera ejecutables nativos, así que no necesitamos llevarnos medio runtime detrás. Pero sí conviene cuidar el build, las variables de entorno, la base de datos y la imagen Docker.
Compilar en modo release
Durante el desarrollo usamos:
cargo runPara producción usamos:
cargo build --releaseEl binario queda en:
target/release/nombre_del_proyectoEl modo release activa optimizaciones. Tarda más en compilar, pero el ejecutable suele ser mucho más rápido.
Configuración por variables de entorno
Una aplicación desplegada no debería tener valores fijos como usuario, contraseña, host o puerto.
Lo normal es leer configuración desde variables de entorno:
use std::env;
fn puerto() -> u16 {
env::var("PORT")
.ok()
.and_then(|valor| valor.parse().ok())
.unwrap_or(3000)
}Y para la base de datos:
let database_url = std::env::var("DATABASE_URL")
.expect("Falta DATABASE_URL");Esto nos permite usar la misma imagen en local, staging y producción cambiando solo configuración.
Escuchar en 0.0.0.0
En local solemos escuchar en 127.0.0.1, porque solo queremos acceder desde nuestra máquina.
En Docker necesitamos escuchar en 0.0.0.0, para que el proceso acepte conexiones desde fuera del contenedor:
let addr = format!("0.0.0.0:{}", puerto());
let listener = tokio::net::TcpListener::bind(&addr)
.await
.unwrap();
axum::serve(listener, app).await.unwrap();Este detalle parece pequeño, pero es de los clásicos. Si el contenedor arranca y no podéis acceder desde fuera, mirad primero si la app está escuchando en 127.0.0.1.
Docker multi-stage
No queremos meter el compilador de Rust en producción. Queremos compilar en una imagen y copiar solo el binario final a otra más pequeña.
FROM rust:1-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/target/release/mi_api /app/mi_api
ENV PORT=3000
EXPOSE 3000
CMD ["/app/mi_api"]Esto se llama build multi-stage. La primera etapa tiene todo lo necesario para compilar. La segunda solo tiene lo necesario para ejecutar.
.dockerignore
Conviene evitar copiar basura al contexto de Docker:
target
.git
.envNo queremos mandar target a Docker, porque puede ocupar muchísimo. Y tampoco queremos meter secretos por accidente.
Si usamos las macros query! de SQLx en modo offline, la carpeta .sqlx sí debe entrar en el contexto de construcción. El compilador la necesita dentro de la etapa builder cuando no tiene acceso a la base de datos.
Migraciones
Si usamos SQLx, hay que decidir cuándo se ejecutan las migraciones.
Tenemos varias opciones:
- Ejecutarlas manualmente antes de desplegar.
- Ejecutarlas en CI/CD.
- Ejecutarlas al arrancar la aplicación.
Para proyectos pequeños, ejecutarlas al inicio puede ser cómodo:
sqlx::migrate!()
.run(&pool)
.await?;En proyectos grandes, suele ser mejor que las migraciones formen parte del pipeline de despliegue, para tener más control.
Si usamos SQLite, el fichero de base de datos necesita un volumen persistente. Cualquier dato escrito solo en la capa del contenedor puede desaparecer cuando este se sustituya.
Logs
En contenedores, lo normal es escribir logs a stdout/stderr. Es decir, println! sirve para empezar, pero en una aplicación real conviene usar tracing.
cargo add tracing tracing-subscriberY en el arranque:
tracing_subscriber::fmt::init();Así los logs se integran bien con Docker, Kubernetes, systemd o la plataforma donde despleguemos.