在不使docker缓存无效的情况下碰撞package.json版本

时间:2018-06-30 00:29:38

标签: node.js docker

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

void GlobalFunc_TransUnit1();
void GlobalFunc_TransUnit2();

void ClassStaticFunc_TransUnit1();
void ClassStaticFunc_TransUnit2();

int main(int argc, char** argv)
{
    // This won't link (as expected).
    // The linker reports that GlobalFunc() is defined twice.
    GlobalFunc_TransUnit1();
    GlobalFunc_TransUnit2();

   // This links despite Foo::ClassStaticFunc() being defined twice.
   // In the final executable, both ClassStaticFunc_TransUnit1() and 
   // ClassStaticFunc_TransUnit2() call the same Foo::ClassStaticFunc() -
   // which happens to be the definition in TranslationUnit1.cpp
   ClassStaticFunc_TransUnit1();  // Calls Foo::ClassStaticFunc() in TranslationUnint1.cpp
   ClassStaticFunc_TransUnit2();  // Also calls Foo::ClassStaticFunc() in TranslationUnit1.cpp
}

将我的# 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 分成两部分是有利的,因为它允许Docker缓存(长)COPY步骤。

但是,最近,我开始使用semver来提升npm install版本。这样做的副作用是使package.json步骤的Docker缓存无效,大大延长了我的构建时间。

有没有我可以使用的替代缓存策略,以便npm install仅在我的依存关系更改时运行?

5 个答案:

答案 0 :(得分:5)

根据其他答案,这是我的看法,但更简短,使用jq

Dockerfile:

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 i

COPY . .

RUN npm run build

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

我将dependenciesdevDependencies字段提取到一个单独的文件中,然后在下一个构建步骤中,将上一步中的内容复制为package.jsonCOPY --from=deps /tmp/deps.json ./package.json)。

RUN npm i之后,COPY . .将用原来的{覆盖} package.json(您可以通过在RUN cat package.json命令之后添加COPY . .来对其进行测试。

请注意,像postinstall这样的npm-scripts commands将不会运行,因为它们在npm install期间以及文件npm i is running from root中都没有出现在文件中--unsafe-perm

运行COPY . .之后的命令或/和(如果需要)通过jq包含它们(更改命令将使缓存层无效)或添加--unsafe-perm

Dockerfile:

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 i --unsafe-perm 
# allow postinstall to run from root (security risk)

RUN npm i

RUN npm run postinstall

...

答案 1 :(得分:3)

我花了一些时间思考这个问题。从根本上讲,我之所以作弊是因为package.json文件实际上已更改,这意味着从技术上规避了缓存无效的任何事情都使得该构建无法再现。

但是,出于我的目的,我更关心构建时间,而不是严格的缓存正确性。这是我想出的:

build-artifacts.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));

undo-build-artifacts.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 build-artifacts.js中运行before_script,在Dockerfile本身中运行undo-build-artifacts.js(在我npm install之后)。 undo-build-artifacts.js包含对构建工件的检查,这意味着如果build-artifacts.js没有运行,则Docker容器仍然可以构建。这样可以使容器在我的书中足够轻便。 :)

答案 2 :(得分:2)

您可以在Dockerfile中添加一个额外的“准备”步骤,以创建一个临时package.json,其中"version"字段是固定的。然后,在安装依赖项时使用此文件,然后将其替换为“真实” package.json
由于所有这些都是在Docker构建过程中发生的,因此您的实际源存储库没有受到影响(因此您可以在构建过程中以及运行docker脚本时使用环境变量npm_package_version,例如进行标记),解决方案是便携式:

Dockerfile:

# 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文件中:

create-tmp-pkg.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

答案 3 :(得分:1)

我在这方面有所不同。我只是忽略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)
  }
})

答案 4 :(得分:0)

打补丁可以不用jq,使用基本sed:

"version": "xx.xx.xx

sed 正则表达式假定版本值遵循 semver 方案(例如 1.23.456)

另一个假设是 package.json," 字符串在文件的其他地方找不到。模式末尾的 "," 可以帮助降低“误报”的概率。检查之前当然是通过安全保护您的 {{1}} 文件。