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.
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"]
- La imagen
rust:latest
es usada como base y contiene la última versión de Rust - Establece el directorio de trabajo a
/app
- El código y el manifiesto (
Cargo.toml
) son copiados - Se construye la aplicación
- Se define la variable de entorno
ROCKET_ADDRESS
- Se define la variable de entorno
ROCKET_PORT
- Se expone el puerto
8000
en el host - 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:
- La imagen
rust:latest
es usada como base y la etapa es nombradabuilder
- Establece el directorio de trabajo a
/app
- Se copia el manifiesto (
Cargo.toml
) - Se crean el directorio temporal
src
y el archivomain.rs
- Se inicia el proceso de construcción para generar la caché de las dependencias
- Se copia el código de la aplicación
- La marca de tiempo del acceso y modificación del archivo
main.rs
se ajustan a la hora actual - Se construye la aplicación
- 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:
- La imagen
alpine:latest
es usada como base y la etapa es nombradarelease
- Establece el directorio de trabajo a
/app
- Se copia el binario generado en la primera etapa
- Se define la variable de entorno
ROCKET_ADDRESS
- Se define la variable de entorno
ROCKET_PORT
- Se expone el puerto
8000
en el host - 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 bibliotecamusl
- 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.