在不使Docker缓存失效的情况下提升package.json版本

kyks70gy  于 2023-01-16  发布在  Docker
关注(0)|答案(8)|浏览(169)

我使用一个非常标准的Dockerfile来容器化Node.js应用程序:

# Simplified version
FROM node:alpine

# Copy package.json first for docker build's layer caching
COPY package.json package-lock.json foo/
RUN npm install

COPY src/ foo/
RUN npm run build

将我的COPY分成两部分是有利的,因为它允许Docker缓存(长)npm install步骤。
然而,最近我开始使用semver来升级我的package.json版本,这样做的副作用是使npm install步骤的Docker缓存无效,从而大大延长了我的构建时间。
是否有一种替代的缓存策略可以让npm install只在依赖关系改变时运行?

kiz8lqtg

kiz8lqtg1#

下面是我对这个问题的看法,基于其他的答案,但是比较简短,并且使用了jq

停靠文件:

FROM endeveit/docker-jq AS deps

# https://stackoverflow.com/a/58487433
# To prevent cache invalidation from changes in fields other than dependencies

COPY package.json /tmp

RUN jq '{ dependencies, devDependencies }' < /tmp/package.json > /tmp/deps.json

FROM node:12-alpine

WORKDIR /app

COPY --from=deps /tmp/deps.json ./package.json
COPY package-lock.json .

RUN npm ci
# https://docs.npmjs.com/cli/ci.html#description

COPY . .

RUN npm run build

LABEL maintainer="Alexey Vishnyakov <n3tn0de@gmail.com>"

我将dependenciesdevDependencies字段提取到一个单独的文件中,然后在下一个构建步骤中,将其从上一个步骤复制为package.jsonCOPY --from=deps /tmp/deps.json ./package.json)。
RUN npm ci之后,COPY . .将用原始的package.json覆盖已删除的package.json(您可以通过在COPY . .命令之后添加RUN cat package.json来测试它。
请注意,npm-scripts commands(如postinstall)不会运行,因为在npm ci期间,它们不存在于文件中,如果npm ciis running from root且不存在--unsafe-perm,也不会运行
COPY . .之后运行命令或/和(如果需要)通过jq包含这些命令(更改命令将使缓存图层无效)或添加--unsafe-perm

停靠文件:

FROM endeveit/docker-jq AS deps

COPY package.json /tmp

RUN jq '{ dependencies, devDependencies, peerDependencies, scripts: (.scripts | { postinstall }) }' < /tmp/package.json > /tmp/deps.json
# keep postinstall script 

FROM node:12-alpine

WORKDIR /app

COPY --from=deps /tmp/deps.json ./package.json
COPY package-lock.json .

# RUN npm ci --unsafe-perm 
# allow postinstall to run from root (security risk)

RUN npm ci
# https://docs.npmjs.com/cli/ci.html#description

RUN npm run postinstall

...
gwbalxhn

gwbalxhn2#

您可以在Dockerfile中添加一个额外的"准备"步骤,创建一个临时的package.json,其中的"version"字段是固定的。这个文件随后在安装依赖项时使用,之后被"真正的" package.json替换。
由于所有这些都发生在Docker构建过程中,因此不会触及您的实际源代码库(因此您可以在构建过程中以及运行Docker脚本时使用环境变量npm_package_version,例如标记),并且解决方案是可移植的:

    • 停靠文件:**
# PREPARATION
FROM node:lts-alpine as preparation
COPY package.json package-lock.json ./
# Create temporary package.json where version is set to 0.0.0
# – this way the cache of the build step won't be invalidated
# if only the version changed.
RUN ["node", "-e", "\
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));\
const pkgLock = JSON.parse(fs.readFileSync('package-lock.json', 'utf-8'));\
fs.writeFileSync('package.json', JSON.stringify({ ...pkg, version: '0.0.0' }));\
fs.writeFileSync('package-lock.json', JSON.stringify({ ...pkgLock, version: '0.0.0' }));\
"]

# BUILD
FROM node:lts-alpine as build
# Install deps, using temporary package.json from preparation step
COPY --from=preparation package.json package-lock.json ./
RUN npm ci
# Copy source files (including "real" package.json) and build app
COPY . .
RUN npm run build

如果您认为内联Node脚本有问题(我喜欢它,因为这样可以在Dockerfile中找到整个Docker构建过程),当然可以将其提取到单独的JS文件中:

    • 创建临时文件包js:**
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
const pkgLock = JSON.parse(fs.readFileSync('package-lock.json', 'utf-8'));

fs.writeFileSync('package.json', JSON.stringify({ ...pkg, version: '0.0.0' }));
fs.writeFileSync('package-lock.json', JSON.stringify({ ...pkgLock, version: '0.0.0' }));

并将准备步骤更改为:

# PREPARATION
FROM node:lts-alpine as preparation
COPY package.json package-lock.json create-tmp-pkg.js ./
# Create temporary package.json where version is set to "0.0.0"
# – this way the cache of the build step won't be invalidated
# if only the version changed.
RUN node create-tmp-pkg.js
odopli94

odopli943#

我花了一些时间思考这个问题,从根本上说,我是在作弊,因为package.json文件实际上被更改了,这意味着任何在技术上绕过该高速缓存无效的操作都会使构建不可复制。
然而,就我的目的而言,我更关心构建时间而不是严格的缓存正确性。

构建工件.js

/*
Used to keep docker cache fresh despite package.json version bumps.

In this script
- copy package.json to package-artifact.json
- zero package.json version

In Docker
- copy package.json
- run npm install normal
- copy package-artifact.json to package.json (undo-build-artifacts.js accomplishes this with a conditional check that package-artifact exists)
*/

const fs = require('fs');
const package = fs.readFileSync('package.json', 'utf8');
fs.writeFileSync('package-artifact.json', package);
const modifiedPackage = { ...JSON.parse(package), version: '0.0.0' };
fs.writeFileSync('package.json', JSON.stringify(modifiedPackage));

const packageLock = fs.readFileSync('package-lock.json', 'utf8');
fs.writeFileSync('package-lock-artifact.json', packageLock);
const modifiedPackageLock = { ...JSON.parse(packageLock), version: '0.0.0' };
fs.writeFileSync('package-lock.json', JSON.stringify(modifiedPackageLock));

撤消生成工件.js

const fs = require('fs');

const hasBuildArtifacts = fs.existsSync('package-artifact.json');
if (hasBuildArtifacts) {
  const package = fs.readFileSync('package-artifact.json', 'utf8');
  const packageLock = fs.readFileSync('package-lock-artifact.json', 'utf8');

  fs.writeFileSync('package.json', package);
  fs.writeFileSync('package-lock.json', packageLock);

  fs.unlinkSync('package-artifact.json');
  fs.unlinkSync('package-lock-artifact.json');
}

这两个文件用于重新定位package.jsonpackage-lock.json,将它们替换为版本为零的工件。这些工件将在docker构建中使用,并在npm install完成时替换为原始版本。
我在Travis CI before_script中运行build-artifacts.js,并在Docker文件本身中运行undo-build-artifacts.js(在I npm install之后)。undo-build-artifacts.js包含了对构建工件的检查,这意味着如果build-artifacts.js没有运行,Docker容器仍然可以构建。

ac1kyiln

ac1kyiln4#

我的做法有点不同,我只是忽略了package.json中的版本,并将其设置为1.0.0。相反,我添加了一个文件version.json,然后使用如下所示的脚本进行部署。

如果需要发布到npm,则此方法不起作用,因为版本永远不会更改

version.json

{"version":"1.2.3"}

deploy.sh

#!/bin/sh
VERSION=`node -p "require('./version.json').version"`

#docker build
docker pull node:10
docker build . -t mycompany/myapp:v$VERSION

#commit version tag
git add version.json
git commit  -m "version $VERSION"
git tag v$VERSION
git push origin
git push origin v$VERSION

#push Docker image to repo
docker push mycompany/myapp:v$VERSION

我通常只是手动更新版本文件,但如果您想要类似npm version的工作方式,则可以使用这样的脚本,该脚本使用semvar包。
patch.js

var semver = require('semver')
var fs = require('fs')
var version = require('./version.json').version
var patch = semver.inc(version, 'patch')

fs.writeFile('./version.json', JSON.stringify({'version': patch}), (err) => {
  if (err) {
    console.error(err)
  } else {
    console.log(version + ' -> ' + patch)
  }
})
pzfprimi

pzfprimi5#

基于n3 tn 0 de answer,我将停靠文件更改为

######## Preperation
FROM node:12-alpine AS deps

COPY package.json package-lock.json ./
RUN npm version --allow-same-version 1.0.0

######## Building
FROM node:12-alpine

WORKDIR /app

COPY --from=deps package.json package-lock.json ./
RUN npm ci

COPY . .

EXPOSE 80
CMD ["npm", "start"]

这种方法将避免使用2个不同的Docker映像-更少的下载和存储-并修复/避免package.json中的任何问题

iyfjxgzm

iyfjxgzm6#

另一个选项pnpm现在具有pnpm fetch,它只使用锁定文件,因此您可以自由地对package.json进行其他更改
这需要从npm/Yarn切换到使用pnpm
示例来自:https://pnpm.io/cli/fetch

FROM node:14

RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm

# pnpm fetch does require only lockfile
COPY pnpm-lock.yaml ./

RUN pnpm fetch --prod

ADD . ./
RUN pnpm install -r --offline --prod

EXPOSE 8080
CMD [ "node", "server.js" ]
rpppsulh

rpppsulh7#

不用jq也可以使用基本的sed来修补版本:

FROM alpine AS temp
COPY package.json /tmp
RUN sed -e 's/"version": "[0-9]\+\.[0-9]\+\.[0-9]\+",/"version": "0.0.0",/'  
  < /tmp/package.json > /tmp/package-v0.json

FROM node:14.5.0-alpine
....
COPY --from=temp /tmp/package-v0.json package.json
...

sed正则表达式假定版本值遵循semver方案(例如1. 23. 456)
另一个假设是"version": "xx.xx.xx,”字符串在文件的其他地方找不到。模式末尾的“,”可以帮助降低“误报”的概率。当然,出于安全考虑,请在使用package.json文件之前检查它。

pgky5nke

pgky5nke8#

步骤:
1.从包json中删除版本
1.安装用于生产的软件包
1.拷贝到生产映像
好处:

  • 可以自由地修补json包而不使docker失效
  • 如果依赖关系没有改变,将不会为生产执行不必要的npm安装(软件包不会频繁更改)

实际上:

# prepare package
FROM node:14-alpine AS package
COPY ./package.json ./package-lock.json ./
RUN node -e "['./package.json','./package-lock.json'].forEach(n => {  \
            let p = require(n);                               \
            p.version = '0.0.0';                              \
            fs.writeFileSync(n, JSON.stringify(p));           \
          });"

# install deps
FROM node:14-alpine AS build
COPY --from=package package*.json ./
RUN npm ci --only=production

# production
FROM node:14-alpine
...
COPY . .
COPY --from=build ./node_modules ./node_modules
...

相关问题