Nodebestpractices: Feedback before writing: Utilize multi-stage builds for smaller and more secure Docker images

Created on 5 Jun 2020  路  11Comments  路  Source: goldbergyoni/nodebestpractices

Context: This is used to share my initial draft of a new best practice to get feedback before writing it down.

Title: Utilize multi-stage builds for smaller and more secure Docker images

TL&DR:

  • Multi-stage builds allow to separate build- and runtime-specific environment details, such as available binaries, exposed environment variables, and even the underlying operating system
  • Splitting up your Dockerfiles into multiple stages will help to reduce final image and container size as you'll only ship what you really need to run your application
  • Sometimes, you'll need to include tools that are only needed during the build phase, for example development dependencies such as the TypeScript CLI. You can install it during the build stage and only use the final output in the run stage. This also means your image will shrink as some dependencies won't get copied over.
  • You might have to expose environment variables during build that should not be present at runtime, such as API Keys and secrets used for communicating with specific services
  • In the final stage, you can copy in pre-built resources such as your build folder, or production-only dependencies (which you can also fetch in a subsequent step)

Example file structure and Dockerfile

Example directory and file structure:

- Dockerfile
- src/
    - index.ts
- package.json
- yarn.lock
- .dockerignore
- docs/
  - README.md

.dockerignore

# Don't copy in existing node_modules, we'll fetch our own
node_modules

# Docs are large, we don't need them in our Docker image
docs

Dockerfile

# Start with fully-featured Node.js base image
FROM node:14.4.0 AS build

USER node
WORKDIR /home/node/app

# Copy dependency information and install all dependencies
COPY --chown=node:node package.json yarn.lock ./

RUN yarn

# Copy source code (and all other relevant files)
COPY --chown=node:node src ./src

# Build code
RUN yarn build

# Run-time stage
FROM node:14.4.0-alpine

# Set non-root user and expose port 8080
USER node
EXPOSE 8080

WORKDIR /home/node/app

# Copy results from previous stage
COPY --chown=node:node --from=build /home/node/app/dist ./dist

# Install production-only dependencies
RUN yarn install --production

CMD [ "node", "dist/app.js" ]
docker-best-practices

All 11 comments

Your text drives the point home. I'm trying to think what can make it even better? 馃

  1. Clarify the downside of the alternative - One of Docker's main goals is to create a shared env among dev & ops. We used to have dev env artifacts and another separated env which is for staging/production. This triggered the known effect of 'It worked on my machine!'. Docker to unify those environments, however there are different needs as you exemplified. I saw developers creating two docker files, one for env and second for prod... This kills the point of Docker. Multi-stage build allows us to use a single Dockerfile that states both the Dev & the Ops needs.

  2. The chosen examples - Your examples are great, I would stick to highly useful things (e.g. copy the dist folder and production packages only) and drop exotic use cases (different OS)

  3. Security example - Maybe mention that many of last years npm breaches were related to dev dependecies
  4. Code example - Showing an example dockerfile with stages is in order. Maybe we should create an entire best practice dockerfile and include in our repo?

Thanks for the feedback! Would love to incorporate 3, do you possibly have a link to further resources for this? Would be great to have a point of reference 馃憤

Very clear indeed!

Do you feel there is any benefits to doing a fresh yarn install --production instead of copying the packages from the previous stage and doing a yarn install --production (or a npm prune --production if using npm) ? (It's not a remark, it's curiosity 馃槃 )

Maybe we could use the --frozen-lockfile option on the first yarn install ? Since we recommend to run npm ci elsewhere in the guide.

Maybe let's create a PR for the code examples to ./examples/docker so we can discuss specific lines of code over the PR UX? This way, we will also kick-off the 'examples' folder

Why yarn and not npm? I would guess, that the 80% are using npm

@kevynb Yeah, another fresh install seems to slow things down. However, if we do 'npm ci' it means we ask for a fresh and safe install and this happens only once before production. Typically in a CI/CD env so the additional 30 seconds might worth it?

@goldbergyoni I鈥檓 not sure my point was well written, here it is in another form.

I鈥檓 used to run an npm install in the first stage. Then in the last stage I copy the node_modules folder located in the first stage.
After copying the node_modules, I run a prune to remove all the unneeded packages for production.

My intuition was that doing this steps was faster than running npm install again in the last build stage as there is no networking or building occurring, only I/Os.

I was wondering if Bruno (or anyone else) had a different experience.

I think both ways are certainly possible, the important step there will be the prune to get rid of non-production dependencies (haven't used it so I'm not sure if it can do this).
I primarily used the second install although you're right, it takes some more time to complete the build

It does if we specify the --production flag.

From the npmjs docs

If the --production flag is specified or the NODE_ENV environment variable is set to production, this command will remove the packages specified in your devDependencies. Setting --no-production will negate NODE_ENV being set to production.

In this case, prune should be better

@kevynb @BrunoScheufler 'npm ci' is safer and faster than 'install', and maybe with local caching (outside the container) the performance differences comparing with copy and prune will be minor?

This is now added in #732

732 is merged, closing for housekeeping 馃槃

Was this page helpful?
0 / 5 - 0 ratings

Related issues

goldbergyoni picture goldbergyoni  路  4Comments

mcollina picture mcollina  路  3Comments

halfzebra picture halfzebra  路  4Comments

heyfirst picture heyfirst  路  6Comments

goldbergyoni picture goldbergyoni  路  4Comments