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:
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" ]
Your text drives the point home. I'm trying to think what can make it even better? 馃
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.
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)
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