10

Rust y Docker para Linuxeros

Vistas: 0
Rust y Docker para Linuxeros

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íaTamañ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)
Rust (musl + distroless)~10-15 MB
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:

  1. La API Key está hardcodeada como constante
  2. 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. musl es 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. El 2>/dev/null oculta los warnings de que el main está vacío. El cargo clean elimina 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 con std::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-sys y openssl-dev. Hay una variante musl: openssl-sys con vendored.
  • Systemd/journald: no funciona en Alpine. Usa tracing-journald solo 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:

  1. Usar Alpine para desarrollo, distroless para producción (recomendado): en desarrollo necesitas shell para depurar; en producción quieres la mínima superficie.
  2. Orquestación externa: docker-compose, Kubernetes o Traefik hacen healthchecks desde el host, donde hay curl.
  3. Healthcheck TCP: Docker soporta healthchecks TCP nativos… pero no con HEALTHCHECK del 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.20 en lugar de gcr.io/distroless/cc
  • apk add ca-certificates wget — los certificados CA son necesarios si haces peticiones HTTPS; wget es para el healthcheck
  • HEALTHCHECK con wget — funcional, verifica que el endpoint /health responde 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: Dockerfile usa el Dockerfile distroless. Cambia a Dockerfile.alpine para desarrollo.
  • image: crustaceo-tasks:latest etiqueta la imagen. Luego puedes referenciarla con docker compose push.
  • restart: unless-stopped reinicia 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_LOG usa ${RUST_LOG:-...} con valor por defecto (sintaxis de shell variable expansion).
  • labels: etiquetas para identificar el contenedor en docker 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
EtiquetaUsoDescripción
latestDesarrollo localÚltima versión estable. Peligrosa en producción.
1.0.0ReleasesSemver estricto. Reproducible.
1.0Versión mayorApunta a la última patch de la rama 1.x.
abc1234SHA del commitTrazabilidad: sabes exactamente qué código contiene.
stagingEntornosÚ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 main y 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 unhealthy
  • CMD ...: el comando que se ejecuta. exit 1 indica fallo, exit 0 indica é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:

  1. Usa Alpine para desarrollo, distroless para producción — es la estrategia que recomiendo. En desarrollo usas Dockerfile.alpine con wget y shell para depurar. En producción usas Dockerfile (distroless) con healthcheck externo.
  2. Externaliza el healthcheck al orquestador — docker-compose, Kubernetes, Nomad o Traefik hacen healthchecks HTTP desde el host, donde hay curl.
  3. 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:

ImagenHealthcheck internoHealthcheck externoUso 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-musl para 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,

Deja una respuesta