Como armazenar em cache a instrução de instalação RUN npm ao docker construir um Dockerfile

86

Atualmente, estou desenvolvendo um back-end do Node para meu aplicativo. Ao encaixá-lo ( docker build .), a fase mais longa é o RUN npm install. A RUN npm installinstrução é executada em cada pequena alteração no código do servidor, o que impede a produtividade por meio do aumento do tempo de construção.

Descobri que executar o npm install onde reside o código do aplicativo e adicionar node_modules ao contêiner com a instrução ADD resolve esse problema, mas está longe de ser a melhor prática. Isso meio que quebra toda a ideia de encaixá-lo e fazer com que o contêiner pese muito mais.

Alguma outra solução?

ohadgk
fonte

Respostas:

124

Ok, então encontrei este ótimo artigo sobre eficiência ao escrever um arquivo docker.

Este é um exemplo de arquivo docker inválido adicionando o código do aplicativo antes de executar a RUN npm installinstrução:

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"]

Dividindo a cópia do aplicativo em 2 instruções COPY (uma para o arquivo package.json e outra para o restante dos arquivos) e executando a instrução de instalação do npm antes de adicionar o código real, qualquer alteração de código não acionará a instalação RUN do npm instrução, apenas alterações do package.json irão acioná-lo. Arquivo docker de prática recomendada:

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"]

É aqui que o arquivo package.json adicionado, instale suas dependências e copie-as para o contêiner WORKDIR, onde o aplicativo reside:

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 a fase de instalação do npm em cada build do docker, apenas copie essas linhas e altere ^ / opt / app ^ para o local em que seu aplicativo está dentro do contêiner.

ohadgk
fonte
2
Isso funciona. Porém, alguns pontos. ADDé desencorajado em favor de COPY, afaik. COPYé ainda mais eficaz. IMO, os dois últimos parágrafos não são necessários, uma vez que são duplicados e também do ponto de vista do app não importa onde no sistema de arquivos o app está, desde que WORKDIResteja configurado.
eljefedelrodeodeljefe
2
Melhor ainda é combinar todos os comandos apt-get em um RUN, incluindo um apt-get clean. Além disso, adicione ./node_modules ao seu .dockerignore, para evitar copiar seu diretório de trabalho em seu contêiner construído e para acelerar a etapa de cópia do contexto de construção da construção.
Simétrico
1
A mesma abordagem, mas apenas adicionando package.jsonà posição de repouso final, também funciona bem (eliminando qualquer cp / mv).
J. Fritz Barnes,
27
Eu não entendo. Por que você instala em um diretório temporário e o move para o diretório do aplicativo? Por que não apenas instalar no diretório do aplicativo? O que estou perdendo aqui?
Joniba
1
Provavelmente está morto, mas pensei em mencioná-lo para futuros leitores. @joniba um motivo para fazer isso seria montar a pasta temporária como um volume persistente na composição sem interferir com os node_modules do sistema de arquivos host local. Ou seja, posso querer executar meu aplicativo localmente, mas também em um contêiner e ainda manter a capacidade de fazer o download de meus node_modules não constantemente quando o package.json mudar
dancypants
41

Esquisito! Ninguém menciona a construção em vários estágios .

# ---- 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

Tuto incrível aqui: https://codefresh.io/docker-tutorial/node_docker_multistage/

Abdennour TOUMI
fonte
2
O que há em ter uma COPYdeclaração depois ENTRYPOINT?
Lindhe
Ótimo, isso também fornece uma boa vantagem quando você está testando seu Dockerfile sem reinstalar dependências cada vez que você edita seu Dockerfile
Xavier Brassoud
30

Descobri que a abordagem mais simples é aproveitar a semântica de cópia do Docker:

A instrução COPY copia novos arquivos ou diretórios e os adiciona ao sistema de arquivos do contêiner no caminho.

Isso significa que se você primeiro copiar explicitamente o package.jsonarquivo e depois executar a npm installetapa, ele poderá ser armazenado em cache e, em seguida, poderá copiar o restante do diretório de origem. Se o package.jsonarquivo foi alterado, isso será novo e executará novamente o cache de instalação do npm para compilações futuras.

Um snippet do final de um Dockerfile ficaria assim:

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

# install application
COPY     . /usr/app
J. Fritz Barnes
fonte
6
Em vez de cd /usr/appvocê pode / deve usar WORKDIR /usr/app.
Vladimir Vukanac de
1
@VladimirVukanac: +1: ao usar o WORKDIR; Atualizei a resposta acima para levar isso em consideração.
J. Fritz Barnes
é npm install executado no diretório / usr / app ou. ?
user557657
1
@ user557657 O WORKDIR define o diretório na imagem futura a partir do qual o comando será executado. Portanto, neste caso, ele está executando a instalação do npm de /usr/appdentro da imagem que criará um /usr/app/node_modulescom dependências instaladas da instalação do npm.
J. Fritz Barnes
1
@ J.FritzBarnes muito obrigado. isnt COPY . /usr/appcopiaria o package.jsonarquivo novamente /usr/appcom o resto dos arquivos?
user557657
3

Imagino que você já saiba, mas poderia incluir um arquivo .dockerignore na mesma pasta contendo

node_modules
npm-debug.log

para evitar o inchaço de sua imagem ao empurrar para o hub do docker

usrrname
fonte
1

você não precisa usar a pasta tmp, apenas copie o package.json para a pasta do aplicativo do seu contêiner, faça algum trabalho de instalação e copie todos os arquivos mais tarde.

COPY app/package.json /opt/app/package.json
RUN cd /opt/app && npm install
COPY app /opt/app
Mike Zhang
fonte
então você está executando npm install no diretório container / opt / app e copiando todos os arquivos da máquina local para / opt / app?
user557657 01 de