0

I am trying to improve my docker images by minimizing as much as possible their size. In order to do so I did the follow steps with a nestjs example as a use case:

nest new testing-docker-slim

and then inside of it I created the following docker file, with multistage and also some differences on npm installation of packages in between production and development:

FROM node:17-alpine as base 
 
FROM base as development 
WORKDIR /app 
COPY . . 
RUN npm i 
CMD ["sh", "-c", "npm run start:dev"] 
 
FROM base as staging 
WORKDIR /app 
COPY . . 
RUN npm i 
RUN npm run build 
CMD ["sh", "-c", "npm run start:prod"] 
 
FROM base as production 
WORKDIR /app 
COPY --from=staging /app/dist ./dist 
COPY --from=staging /app/package.json /app/package-lock.json ./ 
RUN npm i --production 
CMD ["sh", "-c", "npm run start:prod"]

Added a docker ignore file with the following entries:

node_modules
.git
.idea

Created a docker images with the following command:

docker build . --target production -t testing-docker-slim 

And also with this command another images:

docker build . --target development -t testing-docker-slim-dev

The images have this size:

testing-docker-slim-dev    latest       4408308966c3   22 minutes ago      471MB
testing-docker-slim    latest       c688ff1bedc2   22 minutes ago      185MB

So we already have a reasonable improvement on size of image from 471Mb to 185Mb... Still I did a bit more of research and found out about 2 tools, dive and docker slim.

dive in both images says that it is a 99% image efficiency score... so did not found out much with it that I could do with the image to improve.

then I saw docker slim which claims to have an extraordinary level of compression of images, and I run the following command:

docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock dslim/docker-slim build testing-docker-slim --expose 3000 --http-probe=false --continue-after=1

creating a beautiful image of this size:

testing-docker-slim.slim    latest       99a51a88a0f2   9 minutes ago       89.9MB

this size of image is a reasonable one but when I run the image to see if it is working as expected, it crashes with this error:

> testing-docker-slim@0.0.1 start:prod
> node dist/main

node:internal/modules/cjs/loader:936
  throw err;
  ^

Error: Cannot find module 'iterare'
Require stack:
- /app/node_modules/@nestjs/common/pipes/validation.pipe.js
- /app/node_modules/@nestjs/common/pipes/parse-array.pipe.js
- /app/node_modules/@nestjs/common/pipes/index.js
- /app/node_modules/@nestjs/common/index.js
- /app/node_modules/@nestjs/core/discovery/discovery-module.js
- /app/node_modules/@nestjs/core/discovery/index.js
- /app/node_modules/@nestjs/core/index.js
- /app/dist/main.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/app/node_modules/@nestjs/common/pipes/validation.pipe.js:5:19)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1149:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:999:19) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/app/node_modules/@nestjs/common/pipes/validation.pipe.js',
    '/app/node_modules/@nestjs/common/pipes/parse-array.pipe.js',
    '/app/node_modules/@nestjs/common/pipes/index.js',
    '/app/node_modules/@nestjs/common/index.js',
    '/app/node_modules/@nestjs/core/discovery/discovery-module.js',
    '/app/node_modules/@nestjs/core/discovery/index.js',
    '/app/node_modules/@nestjs/core/index.js',
    '/app/dist/main.js'
  ]
}

Node.js v17.4.0

the same happens when I use docker slim on the multistage for development, but the docker multistage without using docker slim works perfectly fine.

Did you use docker multistage with docker slim for production stage with a nodejs if possible nestjs application? Do you have any further advice to improve image size of a docker nodejs / nestjs application?

juan garcia
  • 1,326
  • 2
  • 23
  • 56
  • 1
    185 is pretty average for a Nest application depending on what packages you're using. [I believe my base template comes out to around 100MB and that's with no other packages, so database, orm, validation, etc will definitely add to the size](https://github.com/jmcdo29/nest-docker-template). Any reason you're thinking that's too large? – Jay McDoniel Feb 04 '22 at 20:55
  • I agree 186 is not huge, the main reason I try to do it as small as possible is, I am studying a bit security in images and it is recommended to disiminush as much as possible image sizes in order to have a less attack surface on your images. And besides that when I saw the numbers on docker-slim I was impressed and thought I am missing on making it better... https://github.com/docker-slim/docker-slim#minification-examples (the nodejs example is out of date like version 6.x.x) – juan garcia Feb 04 '22 at 21:00
  • 1
    @JayMcDoniel I saw the repo, and thanks the order in which you placed yours stages made more sense + including node prune of tj was a cool one, and copying the node modules pruned base and only the dist folder from the built dev... tl:dr that was a clever dockerfile thanks for the guidance. – juan garcia Feb 04 '22 at 22:58

1 Answers1

0

As mentioned by the previous comments I took into consideration the Dockerfile of this template https://github.com/jmcdo29/nest-docker-template Some of the key points on it were the order of the stages + the node prune created by TJ.

Last but not least for making docker slim to work I needed to add a flag to the line of docker slim that mentions in their documentation to make it work:

docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock dslim/docker-slim build testing --expose 3000 --http-probe=false --continue-after=1 --include-path=/app

the last flat --include-path=/app did effectively add more size to the slim version of the docker image but made it work as expected, making a good gain on size overall.

UPDATE

Step by step and docker file

  • using the nest - cli call the following nest new testing-docker-slim this will create a brand new nest project for you.
  • copy in the folder testing-docker-slim the following docker ignore
node_modules
.git
.idea

changing .idea for anything your editor may be

  • copy in the folder testing-docker-slim the following docker file
FROM node:17-alpine AS base
EXPOSE 3000

FROM base AS pruned
WORKDIR /app
COPY package.json package-lock.json ./
RUN apk update && apk add curl
RUN npm i --production
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
RUN node-prune
EXPOSE 3000

FROM base as development
WORKDIR /app
COPY ./src ./src
COPY package.json package-lock.json tsconfig.build.json tsconfig.json .eslintrc.js .prettierrc ./
RUN npm i
RUN npm run build
EXPOSE 3000
CMD ["sh", "-c", "npm run start:dev"]

FROM base AS production
WORKDIR /app
COPY --from=development /app/dist ./dist
COPY --from=pruned /app/package.json /app/package-lock.json ./
COPY --from=pruned /app/node_modules ./node_modules
EXPOSE 3000
CMD ["sh", "-c", "npm run start:prod"]
  • inside of the folder testing-docker-slim run the following command:
docker build . -t testing-docker-slim-production --target=production

this is the base image needed to slim.

  • we can run the docker slim with included path:
docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock dslim/docker-slim build testing-docker-slim-production --expose 3000 --continue-after=1 --include-path=/app --tag=slimmed
  • now the results of the images are:
slimmed                              latest                   fcefaece0bdd   4 seconds ago        97.6MB
testing-docker-slim-production       latest                   63ab17071260   18 minutes ago       178MB
testing-docker-slim-development   latest                   2beb278dfdec   About an hour ago    396MB
node                                              17-alpine              c6a2764d974b   4 days ago           170MB

Also take into account that the base image of node 17 is a bit bigger than the one of 16.

juan garcia
  • 1,326
  • 2
  • 23
  • 56
  • 1
    Out of curiosity, what was the final image size, compared to the 185MB you had without docker-slim? – Jay McDoniel Feb 04 '22 at 23:28
  • sure here some numbers, with the modified Dockerfile and node prune went from 185Mb to 176Mb, it disminushed some Mbytes... after docker slim it got to 90Mb. Not so confident with the docker slim yet and running some testing on it, for a bit more of a micro service it is (aprox) 236Mb went down to 222Mb after Docker with your recommendations, after docker slim 141Mb. – juan garcia Feb 04 '22 at 23:49
  • But also consider that I have slightly different versions of nestjs and node Iam in 17... and in the microservice as you mention before it contains some grpc / pubsub, google cloud events.. so it is a bit more bulky too – juan garcia Feb 04 '22 at 23:52