Skip to main content
  1. Blogs/

Rust: Optimización de Imagen de Docker con Construcciones de Múltiples Etapas

·1034 words·5 mins· loading · loading ·
tutorial docker rust

Cuando despliegas una aplicación usando Docker, if construyes la imagen que servirá para crear el contenedor, usando un Dockerfile, hay algunas buenas prácticas que deberíás seguir. En la documentación de Docker, hay una sección que puedes revisar para obtener más información.

Cada instrucción en un Dockerfile se traduce en una capa.

Image Layers

Por ejemplo. en un Dockerfile que tiene el siguiente contenido:

...

RUN apt-get update 
RUN apt-get install -y python3 python3-pip curl git
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl https://pyenv.run | bash

...

Cada instrucción RUN creará una nueva capa. Una forma de optimizarlo sería combinar los comandos siempre que sea posible. Al tener pocas capas, hay menos que reconstruir después de hacer cambios al Dockerfile. Esas líneas pueden modificarse como sigue:

RUN apt-get update \
    && apt-get install -y python3 python3-pip curl git \
    && curl -sSL https://install.python-poetry.org | python3 - \
    && curl https://pyenv.run | bash

De esa forma, el número de capas se reduce a uno.

Docker presentó las construcciones de múltiples etapas en Docker 17.06 CE. Entre otras buenas prácticas, esta funcionalidad podría ayudar a optimizar las imágenes de Docker cuando se conteneriza una aplicación.

A través de este artículo, aprenderás a optimizar una aplicación de Rust contenerizada con las construcciones de multiples etapas.

Construcciones de Múltiples Etapas
#

Es momento de crear un ejemplo de Hello, world! con Rocket.

Crea un nuevo proyecto:

$ cargo new hello_rocket

Cámbiate al directorio del proyecto:

$ cd hello_rocket

Reemplaza el contenido de src/main.rs con lo siguiente:

#[macro_use] extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

Edita el archivo Cargo.toml y agrega la dependencia correspondiente:

[package]
name = "hello_rocket"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "=0.5.0-rc.3"

El código anterior mostrará el texto Hello, world! en el navegador.

Para desplegar esta aplicación usando Docker, crea un Dockerfile con el siguiente contenido:

FROM rust:latest

WORKDIR /app

COPY . .

RUN cargo build --release

ENV ROCKET_ADDRESS=0.0.0.0
ENV ROCKET_PORT=8000
EXPOSE 8000

CMD ["./target/release/hello_rocket"]
  1. La imagen rust:latest es usada como base y contiene la última versión de Rust
  2. Establece el directorio de trabajo a /app
  3. El código y el manifiesto (Cargo.toml) son copiados
  4. Se construye la aplicación
  5. Se define la variable de entorno ROCKET_ADDRESS
  6. Se define la variable de entorno ROCKET_PORT
  7. Se expone el puerto 8000 en el host
  8. Se especifica el comando a ejecutarse cuando el contenedor se inicie

Ahora es momento de construir la imagen. Escribe lo siguiente en la terminal:

$ docker build . -t hello-rocket

Una vez que el proceso de construcción ha terminado, la imagen estará disponible en el sistema. Ahora ejecuta el siguiente comando:

$ docker image ls hello-rocket

Devolverá la siguiente salida:

REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
hello-rocket   latest    a3bb2fe01630   24 seconds ago   2GB

Presta atención a la columna SIZE, el tamaño de la imagen es 2GB. ¿Por qué? La imagen incluye el binario de la aplicación, las dependencias y cada archivo generado durante el proceso de construcción.

¿Cómo reducir el tamaño de la imagen? Dividiendo el proceso en dos etapas. En la primera etapa, la aplicación es construida, y se obtiene el binario. Las dependencias y cualquier otro archivo generado no se requieren, excepto el binario. En la segunda etapa, se construirá la imagen final, se copia el binario generado en la primer etapa y es el único archivo que se incluye. Así es como funcionan las construcciones de múltiples etapas. Cada instrucción FROM puede usar una imagen base diferente.

El archivo Dockerfile debe modificarse como se muestra a continuación:

FROM rust:latest AS builder
WORKDIR /app

COPY Cargo.toml .
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release

COPY src src
RUN touch src/main.rs
RUN cargo build --release

RUN strip target/release/hello_rocket

FROM alpine:latest as release
WORKDIR /app
COPY --from=builder /app/target/release/hello_rocket .

ENV ROCKET_ADDRESS=0.0.0.0
ENV ROCKET_PORT=8000
EXPOSE 8000

CMD ["./hello_rocket"]

Durante la primera etapa:

  1. La imagen rust:latest es usada como base y la etapa es nombrada builder
  2. Establece el directorio de trabajo a /app
  3. Se copia el manifiesto (Cargo.toml)
  4. Se crean el directorio temporal src y el archivo main.rs
  5. Se inicia el proceso de construcción para generar la caché de las dependencias
  6. Se copia el código de la aplicación
  7. La marca de tiempo del acceso y modificación del archivo main.rs se ajustan a la hora actual
  8. Se construye la aplicación
  9. Se remueve la información innecesaria del binario, reduciendo su tamaño y haciendo que sea más difícil realizar ingeniería inversa

Durante la segunda etapa:

  1. La imagen alpine:latest es usada como base y la etapa es nombrada release
  2. Establece el directorio de trabajo a /app
  3. Se copia el binario generado en la primera etapa
  4. Se define la variable de entorno ROCKET_ADDRESS
  5. Se define la variable de entorno ROCKET_PORT
  6. Se expone el puerto 8000 en el host
  7. Se especifica el comando a ejecutarse cuando el contenedor se inicie

Ahora es momento de construir la imagen. Escribe lo siguiente en la terminal:

$ docker build . -t hello-rocket

Después de la creación de la imagen, ejecuta:

$ docker image ls hello-rocket

Esta es la salida que obtendrás:

REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
hello-rocket   latest    7f81a51e9b19   9 seconds ago   11.2MB

El tamaño de la imagen se redujo de 2GB a 11.2MB. Así es como optimizas una aplicación de Rust contenerizada a través de las construcciones de múltiples etapas, y esta funcionalidad puede usarse con cualquier lenguaje compilado.

Nota
#

Si intentas crear un contenedor usando la imagen contruida previamente, obtendrás el siguiente error:

exec ./hello_rocket: no such file or directory

Como se menciona aquí, sucede porque el binario de Rust generado está vinculado dinámicamente con libc, y no se encuentra en las bibliotecas compartidas dentro de la imagen alpine. Alpine Linux está usando musl libc en lugar de la biblioteca por defecto libc.

Tienes dos opciones:

  • Construir el binario de Rust para la arquitectura x86_64-unknown-linux-musl y enlazarlo con la biblioteca musl
  • Usar imágenes distroless de Google

Recomendaría usar una imagen distroless, reemplazr la imagen alpine:latest, de la instrucción FROM de la segunda etapa, con la imagen gcr.io/distroless/cc-debian12, y ejecutar el siguiente comando otra vez:

$ docker build . -t hello-rocket

Ahora, cuando ejecutes el contenedor, no obtendrás error alguno.

Related

Almacenamiento de Objetos con MinIO en Docker
·637 words·3 mins· loading · loading
tutorial docker linux
Ejecutar Percona Backup for MongoDB con Docker
·969 words·5 mins· loading · loading
tutorial docker linux mongodb
Desplegar un Clúster de MongoDB con Docker
·962 words·5 mins· loading · loading
tutorial docker linux mongodb
Docker: Configura un Auxiliar de Credenciales
·324 words·2 mins· loading · loading
tutorial docker linux
Git: Copia de Seguridad y Restauración de Llaves GPG y SSH
·293 words·2 mins· loading · loading
tutorial git linux