Hasta ahora has visto como construir APIs REST, servicios HTTP y herramientas CLI en Rust. Las has ejecutado con cargo run, las has probado con curl y has visto que funcionan. Pero hay un problema: solo funcionan en tu máquina.
Si quieres que crustaceo-tasks se ejecute en un servidor, en la nube, o en la máquina de un colega, necesitas empaquetarlo de forma que sea portable, reproducible y ligero. Necesitas Docker.
En este capítulo dockerizas crustaceo-tasks, la API de tareas del capítulo anterior. Aprendes a:
- Compilar Rust de forma estática con musl
- Construir imágenes Docker multi-stage (builder → runner)
- Usar imágenes distroless (~10 MB para el binario)
- Configurar healthchecks, .dockerignore y etiquetas
- Publicar imágenes en Docker Hub o GHCR
- Usar docker-compose para levantar el servicio con un solo comando
Si vienes de Bash, piensa en esto como pasar de:
# Script que solo funciona si tienes Rust instalado y las dependencias correctas
cargo run --release
A esto:
# Ejecutable universal, sin dependencias, corriendo en un contenedor
docker run -d -p 3000:3000 crustaceo-tasks:latest
El binario compilado pesa ~8 MB. La imagen final, con distroless, pesa ~13 MB. Comparable a un script de Bash, pero con tipado, concurrencia y seguridad de memoria.
Vamos allá.
¿Por qué contenerizar Rust?
Antes de escribir una línea de Dockerfile, entiende qué problema resuelve la contenerización para una app Rust.
El binario ya es autónomo… hasta que no lo es
Una de las gracias de Rust es que compila a un solo binario estático. En teoría, copias ese binario a cualquier máquina Linux con la misma arquitectura y funciona.
En la práctica:
- Dependencias dinámicas: si usas OpenSSL, systemd, o cualquier biblioteca C, necesitas que la libc del sistema coincida. Alpine usa musl, Debian usa glibc, y no siempre son compatibles.
- Versiones de libc: un binario compilado en Ubuntu 24.04 con glibc 2.39 no funciona en CentOS 7 con glibc 2.17.
- Certificados CA: si haces peticiones HTTPS, necesitas los certificados del sistema.
- Zonas horarias: si usas chrono con timezones, necesitas la base de datos tzdata.
Docker resuelve todo esto de golpe: el binario se compila dentro de un contenedor con todas las dependencias, y se ejecuta dentro de otro contenedor que tiene exactamente el runtime necesario. Nada más, nada menos.
Tamaño de imagen: Rust es imbatible
Compara el tamaño de una imagen Docker para distintas tecnologías:
| Tecnología | Tamaño típico (imagen mínima) | ¿Binario único? |
|---|---|---|
| Python + Flask | ~150 MB (python:3-slim) | No |
| Node.js + Express | ~130 MB (node:20-alpine) | No |
| Go | ~10-15 MB (scratch) | Sí |
| Rust (musl + distroless) | ~10-15 MB | Sí |
| Bash script | ~5 MB (alpine) | No |
Rust compite directamente con Go en tamaño mínimo. Y supera a Python y Node.js por un orden de magnitud.
La promesa de Rust + Docker
Un binario compilado con x86_64-unknown-linux-musl:
- No necesita ninguna biblioteca del host (ni siquiera glibc)
- Funciona en cualquier Linux (x86_64)
- Pesa ~8 MB para una API completa con Axum, Tokio, Serde, UUID, Chrono
- Se ejecuta en un contenedor distroless de ~5 MB
Total: ~13 MB para una API REST completa con autenticación, logging, CORS y CRUD.
Preparar crustaceo-tasks para Docker
El crustaceo-tasks del capítulo anterior tiene dos problemas para producción:
- La API Key está hardcodeada como constante
- El puerto está hardcodeado
Vamos a arreglarlo antes de dockerizar.
Leer configuración de variables de entorno
Añade dos funciones al src/main.rs que lean del entorno con valores por defecto:
fn api_key_from_env() -> String {
std::env::var("API_KEY")
.unwrap_or_else(|_| "crustaceo-secreto-2026".to_string())
}
fn port_from_env() -> u16 {
std::env::var("PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(3000)
}
Luego usa api_key_from_env() en el middleware de autenticación (donde antes estaba const API_KEY: &str = "...") y port_from_env() en el TcpListener::bind().
El cambio es mínimo pero crucial: la misma imagen Docker funciona en desarrollo, staging y producción solo cambiando variables de entorno.
Multi-stage builds: el patrón builder
El concepto es sencillo pero poderoso:
- Stage 1 (builder): imagen grande (
rust:alpine, ~300 MB) con todo el toolchain de Rust. Aquí compilas el binario. - Stage 2 (runner): imagen mínima (
gcr.io/distroless/cc, ~5 MB). Solo contiene el runtime C (musl). Aquí copias el binario compilado.
El resultado: la imagen final no contiene Rust, ni cargo, ni las dependencias de compilación. Solo el binario y la libc mínima.
El Dockerfile
Crea Dockerfile en la raíz de crustaceo-tasks:
# ─── Stage 1: Builder ─────────────────────────────────────────────────────────
FROM rust:alpine AS builder
# Dependencias de compilación para target musl
RUN apk add --no-cache musl-dev pkgconfig
# Añadir target de compilación estática
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
# Copiar manifiestos primero (caching de dependencias)
COPY Cargo.toml Cargo.lock* ./
# Dummy main para compilar dependencias sin el código fuente
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl 2>/dev/null; \
cargo clean --release --target x86_64-unknown-linux-musl -p crustaceo-tasks \
2>/dev/null; \
true
# Copiar código fuente real y compilar
COPY src/ ./src/
RUN cargo build --release --target x86_64-unknown-linux-musl
# ─── Stage 2: Runner (Distroless) ─────────────────────────────────────────────
FROM gcr.io/distroless/cc:latest
WORKDIR /app
COPY --from=builder \
/app/target/x86_64-unknown-linux-musl/release/crustaceo-tasks \
/app/crustaceo-tasks
EXPOSE 3000
ENV API_KEY=crusta...nENV RUST_LOG=crustaceo_tasks=info,tower_http=info
CMD ["/app/crustaceo-tasks"]
Desglose: voy línea por línea para que no te pierdas.
Builder stage:
FROM rust:alpine AS builder— La imagen oficial de Rust sobre Alpine Linux. Unas ~300 MB comprimidos.RUN apk add --no-cache musl-dev pkgconfig— Instala musl-dev (headers de musl libc) y pkgconfig. Sin esto, el linker no encuentra las bibliotecas C.RUN rustup target add x86_64-unknown-linux-musl— Añade el target de compilación estática.musles la implementación de libc de Alpine, que se enlaza estáticamente.COPY Cargo.toml Cargo.lock* ./— Copia solo los manifiestos. Docker cachea esta capa: si no cambias las dependencias, no se recompilan.RUN mkdir src && echo "fn main() {}" > src/main.rs— Crea un main mínimo para que cargo pueda resolver el árbol de dependencias.RUN cargo build ...— Compila todas las dependencias. El2>/dev/nulloculta los warnings de que el main está vacío. Elcargo cleanelimina el artefacto del dummy para que la siguiente compilación no intente reutilizarlo.COPY src/ ./src/— Ahora sí, copia el código fuente real.RUN cargo build --release --target x86_64-unknown-linux-musl— Compila el binario final en modo release con target musl.
Runner stage:
FROM gcr.io/distroless/cc:latest— La imagen distroless de Google. Solo contiene musl libc. Sin shell, sin package manager, sin nada. Pesa ~5 MB.COPY --from=builder— Copia solo el binario compilado desde el builder.EXPOSE 3000— Documenta el puerto (es informativo, no abre nada).ENV API_KEY=...— Variables de entorno que la app lee constd::env::var().CMD ["/app/crustaceo-tasks"]— Punto de entrada. Ejecuta el binario directamente.
La magia del caching de dependencias
El truco de copiar Cargo.toml primero, compilar un dummy, y luego copiar el código fuente, es la clave para builds rápidos.
Sin este patrón:
COPY src/ ./src/ ← cada cambio en src/ invalida esta capa
RUN cargo build ... ← recompila TODAS las dependencias (~5 min)
Con el patrón:
COPY Cargo.toml ... ← solo se invalida si cambian dependencias
RUN cargo build dummy ← las dependencias se compilan UNA VEZ
COPY src/ ./src/ ← se invalida con cada cambio, pero...
RUN cargo build ... ← solo recompila TU código (~10 seg)
En desarrollo, el segundo build tarda segundos en lugar de minutos.
Compilación estática con musl
El target x86_64-unknown-linux-musl es el secreto de los binarios Rust portables.
¿Qué es musl?
musl es una implementación de la biblioteca estándar de C (libc). Es ligera, rápida y, lo más importante, se enlaza estáticamente por defecto.
Cuando compilas con el target por defecto (x86_64-unknown-linux-gnu), tu binario se enlaza dinámicamente con glibc del sistema:
$ ldd target/release/crustaceo-tasks
linux-vdso.so.1
libgcc_s.so.1
libc.so.6 # ← glibc, ~2 MB, debe estar en el sistema
Cuando compilas con x86_64-unknown-linux-musl:
$ ldd target/x86_64-unknown-linux-musl/release/crustaceo-tasks
not a dynamic executable # ← binario 100% estático
¿Cómo se usa?
# Añadir el target una vez
rustup target add x86_64-unknown-linux-musl
# Compilar
cargo build --release --target x86_64-unknown-linux-musl
El binario generado funciona en cualquier Linux x86_64, independientemente de la libc que tenga instalada.
Limitaciones
No todo funciona con musl. Algunas crates dependen de bibliotecas C que solo existen en glibc:
- OpenSSL: necesita
openssl-sysyopenssl-dev. Hay una variante musl:openssl-sysconvendored. - Systemd/journald: no funciona en Alpine. Usa
tracing-journaldsolo en imágenes glibc. - PostgreSQL (rusqlite con SQLite): SQLite funciona perfectamente con musl porque es una biblioteca C autocontenida.
Para la mayoría de APIs HTTP (Axum, Tokio, Serde, Tower), musl funciona sin problemas.
.dockerignore: no copies basura
Por defecto, Docker envía todo el contenido del directorio al daemon como contexto de build. Si tienes target/ (~1-3 GB), cada build transfiere gigas.
.dockerignore evita que ciertos archivos entren al contexto de build:
# Directorio de compilación local (enorme, invalida caché)
target/
# Control de versiones
.git/
.gitignore
.gitattributes
# Entorno y secretos
.env
.env.*
*.local
# IDE y editor
.vscode/
.idea/
*.swp
*~
# Sistema
.DS_Store
Thumbs.db
# Documentación (no necesaria en la imagen)
*.md
LICENSE
# Tests (no necesarios en producción)
tests/
benches/
Sin .dockerignore, el primer build transfiere ~1-3 GB. Con .dockerignore, transfiere ~50 KB. La diferencia es abismal.
Imágenes distroless
Las imágenes distroless de Google son contenedores diseñados para contener solo tu aplicación y sus dependencias runtime. Sin shell, sin package manager, sin utilidades.
gcr.io/distroless/cc
La imagen que usas en el Dockerfile es gcr.io/distroless/cc. «cc» significa «C compiler runtime»: contiene musl libc y poco más.
$ docker run --rm -it gcr.io/distroless/cc:latest /bin/sh
docker: Error response from daemon: No such file or directory.
No hay shell. No puedes hacer docker exec -it <container> sh. No hay curl, wget, ping, ls, cat. Nada.
Esto es intencionado y ventajoso:
- Menor superficie de ataque: si no hay shell, no hay shell injection.
- Menor tamaño: ~5 MB frente a ~50 MB de Alpine.
- Menos vulnerabilidades: menos paquetes, menos CVEs.
- Comportamiento predecible: el binario es lo único que se ejecuta.
El problema del healthcheck
Distroless no incluye curl ni wget. Por tanto, HEALTHCHECK con un comando HTTP no funciona dentro del contenedor.
Soluciones:
- Usar Alpine para desarrollo, distroless para producción (recomendado): en desarrollo necesitas shell para depurar; en producción quieres la mínima superficie.
- Orquestación externa: docker-compose, Kubernetes o Traefik hacen healthchecks desde el host, donde hay curl.
- Healthcheck TCP: Docker soporta healthchecks TCP nativos… pero no con
HEALTHCHECKdel Dockerfile. Tendrías que usar el orquestador.
Mi recomendación: usa Dockerfile (distroless) para producción y Dockerfile.alpine para desarrollo local. La sección de Alpine a continuación cubre esta variante.
Variante Alpine Slim
Si necesitas shell, healthcheck funcional y herramientas de depuración, Alpine es la opción:
# ─── Stage 1: Builder (idéntico al anterior) ───────────────────────────────
FROM rust:alpine AS builder
RUN apk add --no-cache musl-dev pkgconfig
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /app
COPY Cargo.toml Cargo.lock* ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl 2>/dev/null; \
cargo clean --release --target x86_64-unknown-linux-musl -p crustaceo-tasks \
2>/dev/null; \
true
COPY src/ ./src/
RUN cargo build --release --target x86_64-unknown-linux-musl
# ─── Stage 2: Alpine runner ───────────────────────────────────────────────────
FROM alpine:3.20
RUN apk add --no-cache ca-certificates wget
WORKDIR /app
COPY --from=builder \
/app/target/x86_64-unknown-linux-musl/release/crustaceo-tasks \
/app/crustaceo-tasks
EXPOSE 3000
ENV API_KEY=crusta...nENV RUST_LOG=crustaceo_tasks=info,tower_http=info
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["/app/crustaceo-tasks"]
Las diferencias:
FROM alpine:3.20en lugar degcr.io/distroless/ccapk add ca-certificates wget— los certificados CA son necesarios si haces peticiones HTTPS; wget es para el healthcheckHEALTHCHECKconwget— funcional, verifica que el endpoint/healthresponde 200
El tamaño aumenta de ~13 MB a ~18 MB, pero ganas shell y healthcheck.
docker-compose.yml
El docker-compose.yml orquesta el servicio. Un solo comando y tienes la API funcionando:
services:
api:
build:
context: .
dockerfile: Dockerfile
image: crustaceo-tasks:latest
container_name: crustaceo-tasks
restart: unless-stopped
ports:
- "3000:3000"
environment:
- API_KEY=crusta...26
- RUST_LOG=${RUST_LOG:-crustaceo_tasks=info,tower_http=info}
labels:
- "app=crustaceo-tasks"
- "tutorial=rust-crustaceo"
- "capitulo=10"
Puntos clave:
build.context: .usa el directorio actual como contexto de build (respeta .dockerignore)build.dockerfile: Dockerfileusa el Dockerfile distroless. Cambia aDockerfile.alpinepara desarrollo.image: crustaceo-tasks:latestetiqueta la imagen. Luego puedes referenciarla condocker compose push.restart: unless-stoppedreinicia el contenedor si falla, a menos que lo detengas explícitamente.ports: "3000:3000"mapea el puerto 3000 del host al 3000 del contenedor.environment:pasa variables de entorno a la app.RUST_LOGusa${RUST_LOG:-...}con valor por defecto (sintaxis de shell variable expansion).labels:etiquetas para identificar el contenedor endocker ps --filter "label=capitulo=10".
Uso básico
# Construir la imagen
docker compose build
# Levantar el servicio en segundo plano
docker compose up -d
# Ver logs
docker compose logs -f
# Ver estado
docker compose ps
# Detener
docker compose down
Etiquetas de imagen (tagging)
Las etiquetas identifican versiones de tu imagen. Un buen esquema de tagging es esencial para despliegues reproducibles.
Buenas prácticas
# Versionado semántico (recomendado para releases)
docker tag crustaceo-tasks:latest crustaceo-tasks:1.0.0
docker tag crustaceo-tasks:latest crustaceo-tasks:1.0
# Commit SHA (para trazabilidad)
docker tag crustaceo-tasks:latest crustaceo-tasks:abc1234
# Entorno
docker tag crustaceo-tasks:latest crustaceo-tasks:staging
| Etiqueta | Uso | Descripción |
|---|---|---|
latest | Desarrollo local | Última versión estable. Peligrosa en producción. |
1.0.0 | Releases | Semver estricto. Reproducible. |
1.0 | Versión mayor | Apunta a la última patch de la rama 1.x. |
abc1234 | SHA del commit | Trazabilidad: sabes exactamente qué código contiene. |
staging | Entornos | Útil para despliegues por entorno. |
Script de tagging automático
En CI/CD puedes generar etiquetas automáticamente:
#!/bin/bash
# tag.sh — genera etiquetas para la imagen
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +%Y%m%d)
docker build -t crustaceo-tasks:latest \
-t "crustaceo-tasks:${VERSION}" \
-t "crustaceo-tasks:${COMMIT}" \
-t "crustaceo-tasks:${DATE}" \
.
Publicar en Docker Hub o GHCR
Una vez que tienes la imagen, la publicas para que otros la usen sin reconstruirla.
Docker Hub
# Iniciar sesión
docker login
# Etiquetar con tu usuario (obligatorio para Docker Hub)
docker tag crustaceo-tasks:latest tuusuario/crustaceo-tasks:latest
# Publicar
docker push tuusuario/crustaceo-tasks:latest
GitHub Container Registry (GHCR)
# Iniciar sesión con token de GitHub
echo $GITHUB_TOKEN | docker login ghcr.io -u tuusuario --password-stdin
# Etiquetar
docker tag crustaceo-tasks:latest ghcr.io/tuusuario/crustaceo-tasks:latest
# Publicar
docker push ghcr.io/tuusuario/crustaceo-tasks:latest
GHCR tiene ventajas sobre Docker Hub:
- Imágenes privadas gratuitas (Docker Hub solo da un repositorio privado gratis)
- Integración con GitHub Packages: la imagen se asocia a tu repositorio
- Sin rate limiting: Docker Hub limita pulls anónimos
Publicar con GitHub Actions (CI/CD)
Si tienes el código en GitHub, puedes automatizar la publicación con un workflow de GitHub Actions. Cada vez que hagas push a main, se construye la imagen y se publica en GHCR automáticamente:
# .github/workflows/docker-publish.yml
name: Build and publish Docker image
on:
push:
branches: [main]
tags: ["v*.*.*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=,format=short
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Este workflow:
- Se activa en push a
mainy en tags semánticos (v1.0.0,v2.1.3, etc.) - Hace login en GHCR con el token automático de GitHub
- Genera etiquetas automáticas: latest, semver, commit SHA
- Construye con Dockerfile (distroless) y publica
El resultado: cada vez que haces un release, la imagen se publica automáticamente con todas las etiquetas que necesitas.
Healthcheck en el contenedor
El healthcheck le dice a Docker si la aplicación está funcionando correctamente. No es lo mismo que «el proceso está vivo» — el proceso puede estar ejecutándose pero la app puede estar en un estado inconsistente.
En el Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
--interval=30s: cada 30 segundos se ejecuta el check--timeout=3s: si el check tarda más de 3s, se considera fallido--start-period=5s: espera 5s antes del primer check (tiempo para que la app arranque)--retries=3: tras 3 fallos consecutivos, el contenedor se marca como unhealthyCMD ...: el comando que se ejecuta.exit 1indica fallo,exit 0indica éxito
Sin curl en distroless
Si usas distroless, no puedes hacer healthcheck HTTP dentro del contenedor porque no hay ni curl ni wget. Esto no es un bug, es una feature de seguridad: nada puede ejecutarse dentro del contenedor excepto tu aplicación.
Alternativas:
- Usa Alpine para desarrollo, distroless para producción — es la estrategia que recomiendo. En desarrollo usas
Dockerfile.alpinecon wget y shell para depurar. En producción usasDockerfile(distroless) con healthcheck externo. - Externaliza el healthcheck al orquestador — docker-compose, Kubernetes, Nomad o Traefik hacen healthchecks HTTP desde el host, donde hay curl.
- Crea un endpoint /health que el binario mismo verifique — el healthcheck ejecuta el propio binario con un flag (ej:
crustaceo-tasks health), pero eso requiere modificar la app para que tenga un modo CLI además del modo servidor.
Para docker-compose, el healthcheck externo se configura en el compose en lugar del Dockerfile:
services:
api:
image: crustaceo-tasks:latest
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
Si tu imagen es distroless, este healthcheck lo ejecuta el motor de Docker desde el host, donde curl sí existe. Es la solución más limpia y la que usa Kubernetes bajo el capó con sus livenessProbe y readinessProbe.
En resumen:
| Imagen | Healthcheck interno | Healthcheck externo | Uso recomendado |
|---|---|---|---|
| Alpine | ✅ (wget/curl) | ✅ | Desarrollo local |
| Distroless | ❌ (sin shell) | ✅ | Producción |
| Debian-slim | ✅ (curl) | ✅ | Compatibilidad |
Verificación
Construye la imagen, ejecuta el contenedor y haz peticiones. Todo desde la terminal.
Construir
# Asegúrate de estar en el directorio del proyecto
cd ~/crustaceo-tasks
# Construir con distroless
docker build -t crustaceo-tasks:latest .
# O construir con alpine (para healthcheck)
docker build -f Dockerfile.alpine -t crustaceo-tasks:alpine .
La primera build descarga las imágenes base y compila las dependencias. Tarda unos 5 minutos. Las siguientes builds son casi instantáneas (gracias al caching).
$ docker images crustaceo-tasks
REPOSITORY TAG IMAGE ID CREATED SIZE
crustaceo-tasks latest a1b2c3d4e5f6 10 seconds ago 13.2 MB
crustaceo-tasks alpine f6e5d4c3b2a1 30 seconds ago 18.7 MB
13.2 MB para una API REST completa. Comprobado.
Ejecutar
# Con distroless
docker run -d \
--name crustaceo-tasks \
-p 3000:3000 \
-e API_KEY=mi-clave-secreta \
crustaceo-tasks:latest
# O con alpine (y healthcheck)
docker run -d \
--name crustaceo-tasks \
-p 3000:3000 \
-e API_KEY=mi-clave-secreta \
crustaceo-tasks:alpine
Probar con curl
# Health check (sin autenticación)
curl -s http://localhost:3000/health | python3 -m json.tool
Respuesta esperada:
{
"status": "ok",
"service": "crustaceo-tasks"
}
# Crear una tarea
curl -s -X POST http://localhost:3000/tasks \
-H "X-API-Key: mi-clave-secreta" \
-H "Content-Type: application/json" \
-d '{"title": "Dockerizar crustaceo-tasks"}' | python3 -m json.tool
Respuesta:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Dockerizar crustaceo-tasks",
"description": "",
"completed": false,
"created_at": "2026-06-24T08:00:00.000000Z",
"updated_at": "2026-06-24T08:00:00.000000Z"
}
# Listar tareas
curl -s http://localhost:3000/tasks \
-H "X-API-Key: mi-clave-secreta" | python3 -m json.tool
# Verificar error 401 (sin API Key)
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:3000/tasks
# Debe responder: HTTP 401
Ver logs
docker logs crustaceo-tasks
Con docker-compose
# Construir y levantar
docker compose up -d
# Verificar estado
docker compose ps
# Probar healthcheck (solo Alpine)
docker inspect --format='{{json .State.Health}}' crustaceo-tasks | python3 -m json.tool
# Detener
docker compose down
Verificar el tamaño
docker images crustaceo-tasks
Si has seguido los pasos correctamente, verás un tamaño inferior a 15 MB para la versión distroless. Si ves más de 100 MB, revisa el Dockerfile: probablemente estás copiando toda la imagen builder en lugar de solo el binario.
Conclusión
En este capítulo hemos visto,
- Compilar Rust estáticamente con
x86_64-unknown-linux-muslpara binarios portables - Multi-stage builds en Docker: builder pesado → runner ligero (distroless o alpine)
- Distroless images de Google: mínima superficie de ataque, tamaño reducido, solo runtime C
- Imágenes Alpine slim: saludables para desarrollo local con healthcheck funcional
- .dockerignore: evitar que
target/y basura entren al contexto de build - docker-compose.yml: orquestar el servicio con variables de entorno y etiquetas
- Healthcheck: verificar que la app responde correctamente, no solo que el proceso está vivo
- Tagging semántico: latest, semver, commit SHA para trazabilidad
- Publicación en registries: Docker Hub y GHCR
Tu crustaceo-tasks ahora es una imagen Docker de ~13 MB que se despliega con un solo comando:
docker run -d -p 3000:3000 -e API_KEY=mi-clave ghcr.io/tu-usuario/crustaceo-tasks:latest
Más información,
- Distroless images — GoogleContainerTools — documentación oficial
- x86_64-unknown-linux-musl — Rust targets — target de compilación estática
- Docker multi-stage builds — documentación oficial
- Docker HEALTHCHECK — referencia del Dockerfile
- Docker .dockerignore — qué excluir del contexto
- Docker Compose — Environment variables — cómo pasar vars
- GHCR — GitHub Container Registry — publicar imágenes en GitHub