How to get version value of package.json inside of Dockerfile? - node.js

This is how my Dockerfile looks like: First I copy the files, then I run npm install and npm build. As you can see, I do set the productive ENV variable.
I would like to get the version of the current package.json file to the running docker image. So I thought of using en ENV variable, e.g. VERSION.
My package.json file could look like this:
"version": "0.0.1"
"scripts": {
"version": "echo $npm_package_version"
}
So npm run version returns the version value. But I don't know how to use this result as ENV in my dockerfile
COPY . /app
RUN npm install --silent
RUN npm run build
RUN VERSION=$(npm run version)
ENV NODE_ENV production
ENV VERSION ???
CMD ["node", "server.js"]

If you just need the version from inside of your node app.. require('./package.json').version will do the trick.
Otherwise, since you're already building your own container, why not make it easier on yourself and install jq? Then you can run VERSION=$(jq .version package.json -r).
Either way though, you cannot simply export a variable from a RUN command for use in another stage. There is a common workaround though:
FROM node:8-alpine
RUN apk update && apk add jq
COPY package.json .
COPY server.js .
RUN jq .version package.json -r > /root/version.txt
CMD VERSION=$(cat /root/version.txt) node server.js
Results from docker build & run:
{ NODE_VERSION: '8.11.1',
YARN_VERSION: '1.5.1',
HOSTNAME: 'xxxxx',
SHLVL: '1',
HOME: '/root',
VERSION: '1.0.0',
PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
PWD: '/' }

Have you tried something like:
COPY . /app
RUN npm install --silent
RUN npm run build
RUN export VERSION=$(npm run version)
ENV NODE_ENV production
CMD ["node", "server.js"]

you can simply use npm pkg get version to retrieve the package version in the working directory.
in Dockerfile that's be something like that:
$(npm pkg get version)
or
RUN npm pkg get version
if you want other information from the package.json then change the version to any different key you want.

Try using bash script (i.e. grep) in RUN command to read the value and set as environment variable. Docker will do a snapshot with the variable set and your good to go.

Related

Issue with running node ts project with docker

I just started learning Docker and i'm trying to run my node ts app with few simple commands. Already googled and tried a lot of things but still no success. App works perfectly when i run it directly from terminal, so mistake might only be in the Dockerfile, nothing else crossed my mind.
It always builds successfully, but breaks when i try to run.
FROM node:14.17.1-alpine
# Install base packages
RUN apk update
RUN apk upgrade
# Change TimeZone
RUN apk add --update tzdata
ENV TZ Europe/Berlin
# Clean APK cache
RUN rm -rf /var/cache/apk/*
WORKDIR /app
COPY package*.json ./
RUN npm i
COPY . .
ENV NODE_ENV=production
CMD ["node","src/app.ts"]
Any help would be really appreciated,
Thanks
Typscript app cannot be run with Node. You need to compile .ts files to .js files and then you can run .js file with Node.
For more detailed you can checkout a blogpost on how to containerize. For example: https://itnext.io/dockerize-a-typescript-app-in-15-mins-a0e8c1e904b3
But there is ts-node, REPL kind of thing for TS. You can add it in the package.json as devDependencies and then can do:
CMD ["npx", "ts-node", "src/app.ts"]
You can check more here: How to run TypeScript files from command line?
EDIT:
if you have tsconfig file then you can install tsc and run it in the Dockerfile to compile. Then you can run the compiled JS file in Node.
You can take hint from this: How to compile typescript in Dockerfile
RUN npm install tsc -g
RUN tsc
CMD ["node", "<urapppath>/app.js"]

How to cache node_modules on Docker build with version?

To cache node_modules I add the package.json first then I run npm i inside docker image.
which is works great. but I also need to have version inside the package.json, and each deploy/build I increment the version number.
Because package.json has been changed, docker is not cache mode_modules because of it.
How can I cache node_modules in this senirio?
FROM node
# If needed, install system dependencies here
# Add package.json before rest of repo for caching
ADD package.json /app/
WORKDIR /app
RUN npm install
ADD . /app
# If needed, add additional RUN commands here
You can achieve this cache using BUILD_VERSION along with package.json version.
ARG BUILD_VERSION=0.0.0
Set some default value to BUILD_VERSION, keep the same value from BUILD_VERSION as package.json version to ignore the npm installation process.
Suppose you have the version in package.json is 0.0.0 and build version should be 0.0.0 to ignore installation.
FROM node:alpine
WORKDIR /app
ARG BUILD_VERSION=0.0.0
copy package.json /app/package.json
RUN echo BUILD_VERSION is $BUILD_VERSION and Package.json version is $(node -e "console.log(require('/app/package.json').version);")
RUN if [ "${BUILD_VERSION}" != $(node -e "console.log(require('/app/package.json').version);") ];then \
echo "ARG version and Package.json is different, installing node modules";\
npm install;\
else \
echo "npm installation process ignored";\
fi
To ignore npm installation during the build, run build command with
docker build --no-cache --build-arg BUILD_VERSION=0.0.0 -t test-cache-image .
Now, if you want to install node_modules just update the run command and it will work as you are expecting but more control as compared to caching track.
docker build --no-cache --build-arg BUILD_VERSION=0.0.1 -t test-cache-image .
This will install node_modules if the package.json version did not match with build-version.

Docker image unable to run postinstall script with error my_pck#2.0.0~postinstall: cannot run in wd my_pck#2.0.0 node symLink.js (wd=/build)

I am building an docker image. My docker file is pretty simple as shown below:-
FROM node:10.15.3
RUN mkdir /build
WORKDIR /build
COPY package.json .
COPY . .
RUN ["npm", "install"]
EXPOSE 3000
CMD [ "npm", "run", "start" ]
The only thing i have extra is i have a postinstall script in package.json which does some extra stuff. The problem is when i try to run the install fails with below error:-
postinstall script with error my_pck#2.0.0~postinstall: cannot run in wd my_pck#2.0.0 node symLink.js (wd=/build)
I have looked into various posts and question reporting same issue where they suggest to use option of:-
using --unsafe-per in npm install does not help
I also tried adding in package.json but that also does not help.
I also tried adding in .npmrc file but unfortunately it also not helping.
I am running the following command to build docker image: docker build -t my_img:1.0 .
I am using windows system.
Any Suggestion on how i can solve this.?

npm unable to find correct version of package within Docker

I am attempting to perform npm install within a docker image. As part of the package.json, I need version 1.8.8 of react-pattern-library. Within the docker image, only version 0.0.1 appears to be available.
If I locally run
npm view react-pattern-library versions
I can see version 1.8.8
However the same command within my docker file only show version 0.0.1
Can anyone tell me what configuration setting I need to be able to find the correct version when attempting my docker build?
docker build -t jhutc/molly-ui
Contents of Dockerfile
FROM node:10
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm#5+)
#COPY package*.json ./
COPY package.json ./
RUN npm set strict-ssl false
ENV HTTP_PROXY="http://proxy.company.com:8080"
ENV HTTPS_PROXY="https://proxy.company.com:8080"
RUN echo $HTTP_PROXY
RUN echo $HTTPS_PROXY
RUN npm view react-pattern-library versions
#RUN npm install
Try deleting the package-lock.json and running npm install again.

Bumping package.json version without invalidating docker cache

I'm using a pretty standard Dockerfile to containerize a Node.js application:
# 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
Breaking up my COPY into two parts was advantageous because it allowed Docker to cache the (long) npm install step.
Recently, however, I started bumping my package.json version using semver. This had the side effect of invalidating the Docker cache for the npm install step, lengthening my build times significantly.
Is there an alternative caching strategy I can use so that npm install only runs when my dependencies change?
Here's my take on this, based off other answers, but shorter and with usage of 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 ci
# https://docs.npmjs.com/cli/ci.html#description
COPY . .
RUN npm run build
LABEL maintainer="Alexey Vishnyakov <n3tn0de#gmail.com>"
I extract dependencies and devDependencies fields to a separate file, then on next build step I copy it from the previous step as package.json (COPY --from=deps /tmp/deps.json ./package.json).
After RUN npm ci, COPY . . will overwrite gutted package.json with the original one (you can test it by adding RUN cat package.json after COPY . . command.
Note that npm-scripts commands like postinstall won't run since they're not present in the file during npm ci and also if npm ci is running from root and without --unsafe-perm
Either run commands after COPY . . or/and (if needed) include them via jq (changing command will invalidate cache layer) or add --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 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
...
You can add an additional "preparation" step in your Dockerfile that creates a temporary package.json where the "version" field is fixed. This file is then used while installing dependencies and afterwards replaced by the "real" package.json.
As all of this happens during the Docker build process, your actual source repository is not touched (so you can use the environment variable npm_package_version both during your build and when running the docker script, e.g. to tag) and the solution is portable:
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
If you think inlining the Node script is iffy (I like it, because this way the entire Docker build process can be found in the Dockerfile), you can of course extract it to a separate JS file:
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' }));
and change your preparation step to:
# 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
I spent some time thinking about this. Fundamentally, I'm cheating because the package.json file is, in fact, changed, which means anything that circumvents the cache invalidation technically makes the build not reproducible.
For my purposes, however, I care more about build time than strict cache correctness. Here's what I came up with:
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');
}
These two files serve to relocate package.json and package-lock.json, replacing them with artifacts that have zeroed-out versions. These artifacts will be used in the docker build, and will be replaced with the original versions upon npm install completion.
I run build-artifacts.js in a Travis CI before_script, and undo-build-artifacts.js in the Dockerfile itself (after I npm install). undo-build-artifacts.js incorporates a check for the build artifacts, meaning the Docker container can still build if build-artifacts.js hasn't run. That keeps the container portable enough in my books. :)
I went about this a bit different. I just ignore the version in package.json and leave it set to 1.0.0. Instead I add a file version.json then I use a script like the one below for deploying.
This approach won't work if you need to publish to npm, since the version will never change
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
I normally just update the version file manually but if you want something that works like npm version you can use a script like this that uses the semvar package.
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)
}
})
Based on n3tn0de answer
I changed the Dockerfile to be
######## 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"]
This approach will avoid using 2 different Docker images -less download and less storage- and fix/avoid any issues in package.json
Another option pnpm now has pnpm fetch which only uses the lockfile so you are free to make other changes to package.json
This requires switching from npm/yarn to using pnpm
Example from: 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" ]
Patching the version can be done without jq, using basic 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
...
The sed regex assumes that the version value follows the semver scheme (e.g. 1.23.456)
The other assumption is that the "version": "xx.xx.xx," string is not found elsewhere in the file. The "," at the end of the pattern can help to lower the probability of "false positives". Check it before with your package.json file by security of course.
Steps:
remove version from package json
install packages for production
Copy to production image
Benefits:
Can freely patch package json without invalidating docker
If dependencies were not changed will not do unecessary npm install for production (packages don't change that frequently)
in practice:
# 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
...

Resources