En este artículo veremos un comportamiento importante a la hora de crear nuestras imágenes.
Organización en capas
Como veremos más adelante en nuestros Dockerfile cuando configuramos nuestra imagen ejecutamos ciertos comandos y cada comando RUN
, COPY
o ADD
es considerado una capa. Con cada nueva instrucción de este conjunto mencionado tenemos una nueva capa que se pone sobre la anterior. Cuando nosotros creamos una imagen basada en Node
, por ejemplo, esta imagen que creamos está basada en la capa de node que viene de DockerHub de node. Y, sobre de ella, nosotros decidimos que más ponerle. Como puede ser nuestros ficheros fuentes, instalar nuestras dependencias, etc.
Lo bueno de que funcione de esta forma es que la primera vez que creamos la imagen tardará un poco en instalar todas las dependencias de nuestro proyecto y ejecutar todos los comandos que le hemos indicado. Pero, una vez esto por primera vez, Docker guarda en caché las capas y automáticamente comprobará que si la capa que vamos a crear es la misma que ya teníamos antes y la estamos creando sobre la misma que estaba antes el resultado será el mismo de siempre. Y Docker usará la misma que tiene guardada ahorrando tiempo en la creación de la nueva imagen.
Imagen 1
Como vimos en como crear mi primer contenedor podemos partir de una imagen creada de la siguiente forma.
# Dockerfile
FROM node
WORKDIR /app
COPY . /app
RUN npm install
EXPOSE 3000
CMD node index.js
El problema que presenta esta primera versión es que dado el funcionamiento en capas con el que trabaja Docker esta imagen cada vez que hagamos un cambio en el código fuente instalará de nuevo todas las dependencias del proyecto. Dado que las dependencias no es algo que cambie con frecuencia nos interesa que al construir la imagen estas capas se usen desde el caché. Para ello podemos hacer la siguiente modificación.
FROM node
WORKDIR /app
COPY ./package.json ./package.json
COPY ./package-lock.json ./package-lock.json
RUN npm install
COPY ./src ./src
CMD node index.js
De esta forma la primera mitad del fichero Dockerfile
hasta RUN npm install
será usado desde caché, ya que no cambia cuando hacemos nuevos desarrollos en nuestra aplicación. Como puede ser: añadir un nuevo endpoint.
Agrupar capas
Otra opción muy común cuando trabajamos con imágenes que parten de un sistema linux, por ejemplo, y queremos instalar ciertas dependencias es agrupar en una misma capa toda la instalación de paquetes. Esta idea está dentro de la documentación de buenas prácticas
FROM ubuntu
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
node
WORKDIR /app
COPY ./src ./src
CMD node index.js
Multi stage building
Otra opción común si queremos evitar tener dependencias en el despliegue las cuales son necesarias en la construcción de nuestra aplicación podemos usar multi stage build (construcción de imágenes multi-estado)
Esta técnica también se encuentra dentro del manual de buenas prácticas de Docker. Con ella podemos crear varias imágenes en un solo fichero Dockerfile
las cuales construyen parte de nuestra aplicación y copiar en la imagen final solamente los ficheros que necesitamos para funcionar.
En este caso partimos de una imagen de node a la que le damos el nombre de builder
, instalamos las dependencias y construimos la aplicación. Esta aplicación queda guardada en el directorio /build
dentro de la primera imagen. A continuación, creamos una nueva imagen y copiamos solo la parte que hemos construido necesaria para que nuestra app funcione al público y la arrancamos.
FROM node:10-alpine as builder
WORKDIR /app
COPY ./package.json ./package.json
RUN npm install
COPY ./src /app/src
RUN npm run build
FROM node:10-alpine
WORKDIR /app
COPY --from=builder /app/build/index.js ./index.js
COPY --from=builder /app/node_modules ./node_modules/
EXPOSE 8080
CMD node ./index.js
Con esta forma de construir imágenes docker nos ahorramos en la imagen final todos los datos y ficheros necesarios en la parte de construcción. Esto hace que la imagen final sea menos pesada al tener menos información.