Cómo almacenar en caché la instrucción de instalación RUN npm cuando Docker compila un Dockerfile

86

Actualmente estoy desarrollando un backend de Node para mi aplicación. Al dockerizarlo ( docker build .), la fase más larga es RUN npm install. La RUN npm installinstrucción se ejecuta en cada pequeño cambio de código del servidor, lo que impide la productividad a través de un mayor tiempo de construcción.

Descubrí que ejecutar npm install donde vive el código de la aplicación y agregar node_modules al contenedor con la instrucción ADD resuelve este problema, pero está lejos de ser la mejor práctica. De alguna manera rompe la idea de acoplarlo y hace que el contenedor pese mucho más.

¿Alguna otra solución?

ohadgk
fuente

Respuestas:

124

Ok, encontré este gran artículo sobre la eficiencia al escribir un archivo de ventana acoplable.

Este es un ejemplo de un archivo docker incorrecto que agrega el código de la aplicación antes de ejecutar la RUN npm installinstrucción:

FROM ubuntu

RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y install python-software-properties git build-essential
RUN add-apt-repository -y ppa:chris-lea/node.js
RUN apt-get update
RUN apt-get -y install nodejs

WORKDIR /opt/app

COPY . /opt/app
RUN npm install
EXPOSE 3001

CMD ["node", "server.js"]

Al dividir la copia de la aplicación en 2 instrucciones COPY (una para el archivo package.json y la otra para el resto de los archivos) y ejecutar la instrucción de instalación npm antes de agregar el código real, cualquier cambio de código no activará la instalación de RUN npm instrucción, solo los cambios del package.json lo activarán. Archivo acoplable de mejores prácticas:

FROM ubuntu
MAINTAINER David Weinstein <[email protected]>

# install our dependencies and nodejs
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y install python-software-properties git build-essential
RUN add-apt-repository -y ppa:chris-lea/node.js
RUN apt-get update
RUN apt-get -y install nodejs

# use changes to package.json to force Docker not to use the cache
# when we change our application's nodejs dependencies:
COPY package.json /tmp/package.json
RUN cd /tmp && npm install
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /opt/app/

# From here we load our application's code in, therefore the previous docker
# "layer" thats been cached will be used if possible
WORKDIR /opt/app
COPY . /opt/app

EXPOSE 3000

CMD ["node", "server.js"]

Aquí es donde se agregó el archivo package.json, instale sus dependencias y cópielas en el contenedor WORKDIR, donde reside la aplicación:

ADD package.json /tmp/package.json
RUN cd /tmp && npm install
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /opt/app/

Para evitar la fase de instalación de npm en cada compilación de la ventana acoplable, simplemente copie esas líneas y cambie ^ / opt / app ^ a la ubicación de su aplicación dentro del contenedor.

ohadgk
fuente
2
Eso funciona. Aunque algunos puntos. ADDse desanima a favor de COPY, afaik. COPYes aún más eficaz. En mi opinión, los dos últimos párrafos no son necesarios, ya que son duplicados y también desde el punto de vista de la aplicación, no importa en qué parte del sistema de archivos viva la aplicación, siempre que WORKDIResté configurado.
eljefedelrodeodeljefe
2
Mejor aún es combinar todos los comandos apt-get en una EJECUCIÓN, incluido un apt-get clean. Además, agregue ./node_modules a su .dockerignore, para evitar copiar su directorio de trabajo en su contenedor construido y para acelerar el paso de copia de contexto de compilación de la compilación.
Simétrico
1
El mismo enfoque, pero solo agregar package.jsona la posición de descanso final, también funciona bien (eliminando cualquier cp / mv).
J. Fritz Barnes
27
No lo entiendo. ¿Por qué lo instala en un directorio temporal y luego lo mueve al directorio de la aplicación? ¿Por qué no instalarlo en el directorio de la aplicación? ¿Que me estoy perdiendo aqui?
Joniba
1
Esto probablemente esté muerto, pero pensé que lo mencionaría para futuros lectores. @joniba una razón para hacer esto sería montar la carpeta temporal como un volumen persistente en componer sin interferir con los módulos node_modules del sistema de archivos del host local. Es decir, es posible que desee ejecutar mi aplicación localmente, pero también en un contenedor y aún así mantener la capacidad de que mis node_modules no se vuelvan a descargar constantemente cuando cambia package.json
dancypants
41

¡Extraño! Nadie menciona la construcción de varias etapas .

# ---- Base Node ----
FROM alpine:3.5 AS base
# install node
RUN apk add --no-cache nodejs-current tini
# set working directory
WORKDIR /root/chat
# Set tini as entrypoint
ENTRYPOINT ["/sbin/tini", "--"]
# copy project file
COPY package.json .

#
# ---- Dependencies ----
FROM base AS dependencies
# install node packages
RUN npm set progress=false && npm config set depth 0
RUN npm install --only=production 
# copy production node_modules aside
RUN cp -R node_modules prod_node_modules
# install ALL node_modules, including 'devDependencies'
RUN npm install

#
# ---- Test ----
# run linters, setup and tests
FROM dependencies AS test
COPY . .
RUN  npm run lint && npm run setup && npm run test

#
# ---- Release ----
FROM base AS release
# copy production node_modules
COPY --from=dependencies /root/chat/prod_node_modules ./node_modules
# copy app sources
COPY . .
# expose port and define CMD
EXPOSE 5000
CMD npm run start

Impresionante tuto aquí: https://codefresh.io/docker-tutorial/node_docker_multistage/

Abdennour TOUMI
fuente
2
¿Qué pasa con tener una COPYdeclaración después ENTRYPOINT?
lindhe
Genial, eso también proporciona una buena ventaja cuando está probando su Dockerfile sin reinstalar dependencias cada vez que edita su Dockerfile
Xavier Brassoud
30

Descubrí que el enfoque más simple es aprovechar la semántica de copia de Docker:

La instrucción COPY copia nuevos archivos o directorios y los agrega al sistema de archivos del contenedor en la ruta.

Esto significa que si primero copia explícitamente el package.jsonarchivo y luego ejecuta elnpm install paso, se puede almacenar en caché y luego puede copiar el resto del directorio de origen. Si el package.jsonarchivo ha cambiado, será nuevo y volverá a ejecutar el almacenamiento en caché de npm install para futuras compilaciones.

Un fragmento del final de un Dockerfile se vería así:

# install node modules
WORKDIR  /usr/app
COPY     package.json /usr/app/package.json
RUN      npm install

# install application
COPY     . /usr/app
J. Fritz Barnes
fuente
6
En lugar de cd /usr/apppuede / debe usar WORKDIR /usr/app.
Vladimir Vukanac
1
@VladimirVukanac: +1: sobre el uso de WORKDIR; Actualicé la respuesta anterior para tener eso en cuenta.
J. Fritz Barnes
¿Se ejecuta la instalación de npm en el directorio / usr / app o. ?
user557657
1
@ user557657 WORKDIR establece el directorio dentro de la imagen futura desde el cual se ejecutará el comando. Entonces, en este caso, está ejecutando npm install desde /usr/appdentro de la imagen que creará una /usr/app/node_modulescon las dependencias instaladas desde npm install.
J. Fritz Barnes
1
@ J.FritzBarnes muchas gracias. ¿No COPY . /usr/appvolvería a copiar el package.jsonarchivo /usr/appcon el resto de los archivos?
user557657
3

Imagino que ya lo sabe, pero podría incluir un archivo .dockerignore en la misma carpeta que contiene

node_modules
npm-debug.log

para evitar que la imagen se hinche cuando empuja al hub de Docker

usrrname
fuente
1

no necesita usar la carpeta tmp, simplemente copie package.json en la carpeta de la aplicación de su contenedor, haga un trabajo de instalación y copie todos los archivos más tarde.

COPY app/package.json /opt/app/package.json
RUN cd /opt/app && npm install
COPY app /opt/app
Mike Zhang
fuente
Entonces, ¿está ejecutando npm install en el directorio contenedor / opt / app y luego copiando todos los archivos de la máquina local a / opt / app?
user557657