Merge branch 'master' into master

This commit is contained in:
Siddharth
2020-08-21 00:39:41 +05:30
committed by GitHub
20 changed files with 878 additions and 90 deletions

View File

@ -960,6 +960,15 @@
"contributions": [
"content"
]
},
{
"login": "andreoav",
"name": "Andreo Vieira",
"avatar_url": "https://avatars2.githubusercontent.com/u/508827?v=4",
"profile": "https://twitter.com/andreoav07",
"contributions": [
"content"
]
}
],
"projectName": "nodebestpractices",

196
README.md
View File

@ -9,7 +9,7 @@
<br/>
<div align="center">
<img src="https://img.shields.io/badge/⚙%20Item%20count%20-%2086%20Best%20Practices-blue.svg" alt="86 items"> <img src="https://img.shields.io/badge/%F0%9F%93%85%20Last%20update%20-%20March%2012%202020-green.svg" alt="Last update: March, 2020"> <img src="https://img.shields.io/badge/ %E2%9C%94%20Updated%20For%20Version%20-%20Node%2012.12.0-brightgreen.svg" alt="Updated for Node 13.1.0">
<img src="https://img.shields.io/badge/⚙%20Item%20count%20-%20101%20Best%20Practices-blue.svg" alt="101 items"> <img src="https://img.shields.io/badge/%F0%9F%93%85%20Last%20update%20-%20March%2012%202020-green.svg" alt="Last update: March, 2020"> <img src="https://img.shields.io/badge/ %E2%9C%94%20Updated%20For%20Version%20-%20Node%2012.12.0-brightgreen.svg" alt="Updated for Node 13.1.0">
</div>
<br/>
@ -27,11 +27,13 @@ Read in a different language: [![CN](/assets/flags/CN.png)**CN**](/README.chines
# Latest Best Practices and News
- **:tada: Node.js best practices reached 40k stars**: Thank you to each and every contributor who helped turning this project into what it is today! We've got lots of plans for the time ahead, as we expand our ever-growing list of Node.js best practices even further.
- **:tada: Node.js best practices reached 50k stars**: Thank you to each and every contributor who helped turning this project into what it is today! We've got lots of plans for the time ahead, as we expand our ever-growing list of Node.js best practices even further.
- **:rocket: Two New Best Practices**: We've been working hard on two new best practices, a section about [using npm ci](https://github.com/goldbergyoni/nodebestpractices#-519-install-your-packages-with-npm-ci) to preview the dependency state in production environments and another on [testing your middlewares in isolation](https://github.com/goldbergyoni/nodebestpractices#-413-test-your-middlewares-in-isolation)
- **🎧 Podcast**: Yoni Goldberg from our team participated in the last JS Party Podcast (Very cool one!) episode to speak about Node.js best practices, [🎧 listen here](https://changelog.com/jsparty/139)
- **:whale: Node.js + Docker best practices**: We've opened a [call for ideas](https://github.com/goldbergyoni/nodebestpractices/issues/620) to collect best practices related to running dockerized Node.js applications. If you've got any further best practices, don't hesitate to join the conversation!
- **:whale: Node.js + Docker best practices**: We've just release the Docker with Node.js section which includes 15 bullets about better coding techqniues with Docker
- **🎤 A talk at OdessaJS**: We will speak about Node.js testing this week at the great [OdessaJS conference](https://odessajs.org/)
<br/><br/>
@ -54,7 +56,7 @@ Read in a different language: [![CN](/assets/flags/CN.png)**CN**](/README.chines
5. [Going To Production Practices (19) ](#5-going-to-production-practices)
6. [Security Practices (25)](#6-security-best-practices)
7. [Performance Practices (2) (Work In Progress ✍️)](#7-draft-performance-best-practices)
8. [Docker Practices (Work In Progress ✍️)](#7-draft-docker-best-practices)
8. [Docker Practices (15)](#8-docker-best-practices)
<br/><br/>
@ -736,7 +738,7 @@ All statements above will return false if used with `===`
**TL;DR:** You have to be sure that production code uses the exact version of the packages you have tested it with. Run `npm ci` to do a clean install of your dependencies matching package.json and package-lock.json.
**Otherwise:** QA will thoroughly test the code and approve a version that will behave differently in production. Even worse, different servers in the same production cluster might run different code
**Otherwise:****** QA will thoroughly test the code and approve a version that will behave differently in production. Even worse, different servers in the same production cluster might run different code
🔗 [**Read More: Use npm ci**](/sections/production/installpackageswithnpmci.md)
@ -1075,171 +1077,181 @@ Bear in mind that with the introduction of the new V8 engine alongside the new E
<p align="right"><a href="#table-of-contents">⬆ Return to top</a></p>
# `7. Draft: Docker Best Practices`
# `8. Docker Best Practices`
## Our contributors are working on this section. [Would you like to join?](https://github.com/goldbergyoni/nodebestpractices/issues/682)
🏅 Many thanks to [Bret Fisher](https://github.com/BretFisher) from whom we learned many of the following practices
<br/><br/>
## ![✔] 8.1. Clean npm cache
## ![✔] 8.1 Use multi-stage builds for leaner and more secure Docker images
**TL;DR:**
**TL;DR:** Use multi-stage build to copy only necessary production artifacts. A lot of build-time dependencies and files are not needed for running your application. With multi-stage builds these resources can be used during build while the runtime environment contains only what's necessary. Multi-stage builds are an easy way to get rid of overweight and security threats
**Otherwise:**
**Otherwise:** Larger images will take longer to build and ship, build-only tools might contain vulnerabilities and secrets only meant for the build phase might be leaked.
🔗 [**Read More: Clean npm cache**](/sections/docker/file.md)
### Example Dockerfile for multi-stage builds
```dockerfile
FROM node:14.4.0 AS build
COPY . .
RUN npm install && npm run build
FROM node:slim-14.4.0
USER node
EXPOSE 8080
COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm install --production
CMD [ "node", "dist/app.js" ]
```
🔗 [**Read More: Use multi-stage builds**](/sections/docker/multi_stage_builds.md)
<br /><br /><br />
## ![✔] 8.2. Bootstrap the code using 'node' command, avoid 'npm run' scripts
## ![✔] 8.2. Bootstrap using 'node' command, avoid npm start
**TL;DR:**
**TL;DR:** use `CMD ['node','server.js']` to start your app, avoid using npm scripts which don't pass OS signals to the code. This prevents problems with child-process, signal handling, graceful shutdown and having processes.
**Otherwise:**
**Otherwise:** When no signals are passed, your code will never be notified about shutdowns. Without this, it will lose its chance to close properly possibly losing current requests and/or data.
🔗 [**Read More: Clean npm cache**](/sections/docker/file.md)
[**Read More: Bootstrap container using node command, avoid npm start**](/sections/docker/bootstrap-using-node.md)
<br /><br /><br />
## ![✔] 8.3. Remove development dependencies
## ![✔] 8.3. Let the Docker runtime handle replication and uptime
**TL;DR:** Although Dev-Dependencies are sometimes needed during the build and test life-cycle, eventually the image that is shipped to production should be minimal and clean from development dependencies. Doing so guarantees that only necessary code is shipped and the amount of potential attacks (i.e. attack surface) is minimized. When using multi-stage build (see dedicated bullet) this can be achieved by installing all dependencies first and finally running 'npm ci --production'
**TL;DR:** When using a Docker run time orchestrator (e.g., Kubernetes), invoke the Node.js process directly without intermediate process managers or custom code that replicate the process (e.g. PM2, Cluster module). The runtime platform has the highest amount of data and visibility for making placement decision - It knows best how many processes are needed, how to spread them and what to do in case of crashes
**Otherwise:** Many of the infamous npm security breaches were found within development packages
**Otherwise:** Container keeps crashing due to lack of resources will get restarted indefinitely by the process manager. Should Kubernetes be aware of that, it could relocate it to a different roomy instance
🔗 [**Read More: Remove development dependencies**](/sections/docker/install-for-production.md)
🔗 [**Read More: Let the Docker orchestrator restart and replicate processes**](/sections/docker/restart-and-replicate-processes.md)
<br/><br /><br />
## ![✔] 8.4. Use .dockerignore to prevent leaking secrets
**TL;DR**: Include a .dockerignore file that filters out common secret files and development artifacts. By doing so, you might prevent secrets from leaking into the image. As a bonus the build time will significantly decrease. Also, ensure not to copy all files recursively rather explicitly choose what should be copied to Docker
**Otherwise**: Common personal secret files like .env, .aws and .npmrc will be shared with anybody with access to the image (e.g. Docker repository)
🔗 [**Read More: Use .dockerignore**](/sections/docker/docker-ignore.md)
<br /><br /><br />
## ![✔] 8.4. Lint your Dockerfile
## ![✔] 8.5. Clean-up dependencies before production
**TL;DR:**
**TL;DR:** Although DevDependencies are sometimes needed during the build and test life-cycle, eventually the image that is shipped to production should be minimal and clean from development dependencies. Doing so guarantees that only necessary code is shipped and the amount of potential attacks (i.e. attack surface) is minimized. When using multi stage build (see dedicated bullet) this can be achieved by installing all dependencies first and finally running 'npm ci --production'
**Otherwise:**
**Otherwise:** Many of the infamous npm security breaches were found within development packages (e.g. [eslint-scope](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes))
🔗 [**Read More: Lint your Dockerfile**](/sections/docker/file.md)
🔗 Read More: [Remove development dependencies](/sections/docker/install-for-production.md)
<br /><br /><br />
## ![✔] 8.5. Utilize caching for better build time
## ![✔] 8.6. Shutdown smartly and gracefully
**TL;DR:**
**TL;DR:** Handle the process SIGTERM event and clean-up all existing connection and resources. This should be done while responding to ongoing requests. In Dockerized runtimes shutting down containers is not a rare event, rather a frequent occurrence that happen as part of routine work. Achieving this demands some thoughtful code to orchestrate several moving parts: The load balancer, keep-alive connections, the HTTP server and other resources
**Otherwise:**
**Otherwise:** Dying immediately means not responding to thousands of disappointed users
🔗 [**Read More: Utilize caching for better build time**](/sections/docker/file.md)
🔗 [**Read More: Graceful shutdown**](/sections/docker/graceful-shutdown.md)
<br /><br /><br />
## ![✔] 8.6. Set Docker memory limits which are in-par with v8 memory limit
## ![✔] 8.7. Set memory limits using both Docker and v8
**TL;DR:**
**TL;DR:** Always configure a memory limit using both Docker and the JavaScript runtime flags: Set the v8's old space memory to be a bit less than the container limit
**Otherwise:**
**Otherwise:** The docker definition is needed to perform thoughtful scaling decision and prevent starving other citizens. Without also defining the v8's limits, it will under utilize the container resources - Without explicit instructions it crashes when utilizing ~50-60% of its host resources
🔗 [**Read More: Set Docker memory limits which are in-par with v8 memory limit**](/sections/docker/file.md)
🔗 [**Read More: Set memory limits using Docker only**](/sections/docker/memory-limit.md)
<br /><br /><br />
## ![✔] 8.7. Scan your image for vulnerabilities
## ![✔] 8.8. Caching
**TL;DR:** Besides checking code dependencies vulnerabilities, also scan the final image that is shipped to production. Docker image scanners check the code dependencies but also the OS binaries. This E2E security scan covers more ground and verifies that no bad guy injected bad things during the build. Consequently, it is recommended running this as the last step before deployment. There are a handful of free and commercial scanners that also provide CI/CD plugins
**TL;DR:** Rebuilding a whole docker image from cache can be nearly instantaneous if done correctly. The less updated instructions should be at the top of your Dockerfile and the ones constantly changing (like app code) should be at the bottom.
**Otherwise:** Your code might be entirely free from vulnerabilities. However, it might still get hacked due to vulnerable version of OS-level binaries (e.g. OpenSSL, TarBall) that are commonly being used by applications
**Otherwise:** Docker build will be very long and consume lot of resources even when making tiny changes
🔗 [**Read More: Scan your image for vulnerabilities**](/sections/docker/scan-images.md)
🔗 [**Read More: Leverage caching to reduce build times**](/sections/docker/use-cache-for-shorter-build-time.md)
<br /><br /><br />
## ![✔] 8.8. Use multistage builds
## ![✔] 8.9. Use explicit image reference, avoid `latest` tag
**TL;DR:**
**TL;DR:** Specify an explicit image digest or versioned label, never refer to 'latest'. Developers are often led to believe that specifying the `latest` tag will provide them with the most recent image in the repository however this is not the case. Using a digest guarantees that every instance of the service is running exactly the same code.
**Otherwise:**
In addition, referring to an image tag means that the base image is subject to change, as image tags cannot be relied upon for a deterministic install. Instead, if a deterministic install is expected, a SHA256 digest can be used to reference an exact image.
🔗 [**Read More: Use multistage builds**](/sections/docker/file.md)
**Otherwise:** A new version of a base image could be deployed into production with breaking changes, causing unintended application behaviour.
🔗 [**Read More: Understand image tags and use the "latest" tag with caution**](/sections/docker/image-tags.md)
<br /><br /><br />
## ![✔] 8.9. Don't use "latest" tags, use a digest
## ![✔] 8.10. Prefer smaller Docker base images
**TL;DR:**
**TL;DR:** Large images lead to higher exposure to vulnerabilities and increased resource consumption. Using leaner Docker images, such as Slim and Alpine Linux variants, mitigates this issue.
**Otherwise:**
**Otherwise:** Building, pushing, and pulling images will take longer, unknown attack vectors can be used by malicious actors and more resources are consumed.
🔗 [**Read More: Don't use "latest", use a digest**](/sections/docker/file.md)
🔗 [**Read More: Prefer smaller images**](/sections/docker/smaller_base_images.md)
<br /><br /><br />
## ![✔] 8.10. Prefer smaller images
## ![✔] 8.11. Clean-out build-time secrets, avoid secrets in args
**TL;DR:**
**TL;DR:** Avoid secrets leaking from the Docker build environment. A Docker image is typically shared in multiple environment like CI and a registry that are not as sanitized as production. A typical example is an npm token which is usually passed to a dockerfile as argument. This token stays within the image long after it is needed and allows the attacker indefinite access to a private npm registry. This can be avoided by coping a secret file like `.npmrc` and then removing it using multi-stage build (beware, build history should be deleted as well) or by using Docker build-kit secret feature which leaves zero traces
**Otherwise:**
**Otherwise:** Everyone with access to the CI and docker registry will also get access to some precious organization secrets as a bonus
🔗 [**Read More: Prefer smaller images**](/sections/docker/file.md)
🔗 [**Read More: Clean-out build-time secrets**](/sections/docker/avoid-build-time-secrets.md)
<br /><br /><br />
## ![✔] 8.11. Graceful shutdown
**TL;DR:**
**Otherwise:**
🔗 [**Read More: Graceful shutdown**](/sections/docker/file.md)
<br /><br /><br />
## ![✔] 8.12. Avoid sending secrets as build time arguments
**TL;DR:**
**Otherwise:**
🔗 [**Read More: Avoid sending secrets as build time arguments**](/sections/docker/file.md)
<br /><br /><br />
## ![✔] Use .dockerignore to prevent leaking secrets
## ![✔] 8.12. Scan images for multi layers of vulnerabilities
**TL;DR:** Include a `.dockerignore` file that filters out common secret files and development artifacts. By doing so, you might prevent secrets from leaking into the image. As a bonus, the build time will significantly decrease. Also, ensure not to copy all files recursively rather explicitly choose what should be copied to Docker
**Otherwise:** Common personal secret files like `.env`, `.aws` and `.npmrc` will be shared with anybody with access to the image (e.g. Docker repository)
🔗 [**Read More: On the importance of docker ignore**](/sections/docker/docker-ignore.md)
🔗 [**Read More: Generic Docker practices**](/sections/docker/scan-images.md)
<br /><br /><br />
## ![✔] 8.14. Avoid inconsistent images
## ![✔] 8.13 Clean NODE_MODULE cache
**TL;DR:**
**TL;DR:** After installing dependencies in a container remove the local cache. It doesn't make any sense to duplicate the dependencies for faster future installs since there won't be any further installs - A Docker image is immutable. Using a single line of code tens of MB (typically 10-50% of the image size) are shaved off
**Otherwise:**
**Otherwise:** The image that will get shipped to production will weigh 30% more due to files that will never get used
🔗 [**Read More: Avoid inconsistent images**](/sections/docker/file.md)
🔗 [**Read More: Clean NODE_MODULE cache**](/sections/docker/clean-cache.md)
<br /><br /><br />
## ![✔] 8.15. Avoid process managers
## ![✔] 8.14. Generic Docker practices
**TL;DR:**
**TL;DR:** This is a collection of Docker advice that is not related directly to Node.js - the Node implementation is not much different than any other language. Click read more to skim through.
**Otherwise:**
🔗 [**Read More: Generic Docker practices**](/sections/docker/generic-tips.md)
🔗 [**Read More: Avoid process managers**](/sections/docker/file.md)
<br/><br /><br />
<br /><br /><br />
## ![✔] 8.16. Generic Docker practices
## ![✔] 8.15. Lint your Dockerfile
**TL;DR:**
**TL;DR:** Linting your Dockerfile is an important step to identify issues in your Dockerfile which differ from best practices. By checking for potential flaws using a specialised Docker linter, performance and security improvements can be easily identified, saving countless hours of wasted time or security issues in production code.
**Otherwise:**
**Otherwise:** Mistakenely the Dockerfile creator left Root as the production user, and also used an image from unknown source repository. This could be avoided with with just a simple linter.
🔗 [**Read More: Generic Docker practices**](/sections/docker/file.md)
🔗 [**Read More: Lint your Dockerfile**](/sections/docker/lint-dockerfile.md)
<br /><br /><br />
<br/><br /><br />
<p align="right"><a href="#table-of-contents">⬆ Return to top</a></p>
@ -1303,6 +1315,15 @@ Full Stack Developer & Site Reliability Engineer based in New Zealand, intereste
<br/>
<img align="left" width="100" height="100" src="assets/images/members/kevyn.png">
[Kevyn Bruyere](https://github.com/kevynb)
<a href="https://www.linkedin.com/in/kevynbruyere/"><img src="assets/images/linkedin.png" width="16" height="16"></img></a>
Independent full-stack developer with a taste for Ops and automation.
<br/>
### Steering Committee Emeriti
<img align="left" width="100" height="100" src="assets/images/members/sagir.png">
@ -1322,9 +1343,9 @@ Thank you to all our collaborators! 🙏
Our collaborators are members who are contributing to the repository on a regular basis, through suggesting new best practices, triaging issues, reviewing pull requests and more. If you are interested in helping us guide thousands of people to craft better Node.js applications, please read our [contributor guidelines](/.operations/CONTRIBUTING.md) 🎉
| <a href="https://github.com/idori" target="_blank"><img src="assets/images/members/ido.png" width="75" height="75"></a> | <a href="https://github.com/TheHollidayInn" target="_blank"><img src="assets/images/members/keith.png" width="75" height="75"></a> | <a href="https://github.com/kevynb" target="_blank"><img src="assets/images/members/kevyn.png" width="59" height="59"></a> |
| :---------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------: |
| [Ido Richter (Founder)](https://github.com/idori) | [Keith Holliday](https://github.com/TheHollidayInn) | [Kevyn Bruyere](https://github.com/kevynb) |
| <a href="https://github.com/idori" target="_blank"><img src="assets/images/members/ido.png" width="75" height="75"></a> | <a href="https://github.com/TheHollidayInn" target="_blank"><img src="assets/images/members/keith.png" width="75" height="75"></a> |
| :---------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------: |
| [Ido Richter (Founder)](https://github.com/idori) | [Keith Holliday](https://github.com/TheHollidayInn) |
### Collaborator Emeriti
@ -1482,6 +1503,7 @@ Thanks goes to these wonderful people who have contributed to this repository!
</tr>
<tr>
<td align="center"><a href="https://github.com/OrBin"><img src="https://avatars1.githubusercontent.com/u/6897234?v=4" width="100px;" alt=""/><br /><sub><b>Or Bin</b></sub></a><br /><a href="#content-OrBin" title="Content">🖋</a></td>
<td align="center"><a href="https://twitter.com/andreoav07"><img src="https://avatars2.githubusercontent.com/u/508827?v=4" width="100px;" alt=""/><br /><sub><b>Andreo Vieira</b></sub></a><br /><a href="#content-andreoav" title="Content">🖋</a></td>
</tr>
</table>

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -0,0 +1,93 @@
# Clean build-time secrets, avoid secrets as args
<br/><br/>
### One Paragraph Explainer
A Docker image isn't just a bunch of files but rather multiple layers revealing what happened during build-time. In a very common scenario, developers need the npm token during build time (mostly for private registries) - this is falsely achieved by passing the token as a build time args. It might seem innocent and safe, however this token can now be fetched from the developer's machine Docker history, from the Docker registry and the CI. An attacker who gets access to that token is now capable of writing into the organization private npm registry. There are two more secured alternatives: The flawless one is using Docker --secret feature (experimental as of July 2020) which allows mounting a file during build time only. The second approach is using multi-stage build with args, building and then copying only the necessary files to production. The last technique will not ship the secrets with the images but will appear in the local Docker history - This is typically considered as secured enough for most organizations.
<br/><br/>
### Code Example Using Docker mounted secrets (experimental but stable)
<details>
<summary><strong>Dockerfile</strong></summary>
```
# syntax = docker/dockerfile:1.0-experimental
FROM node:12-slim
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm,target=/root/.npmrc npm ci
# The rest comes here
```
</details>
<br/><br/>
### Code Example Building securely using multi-stage build
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim AS build
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY . /dist
RUN echo "//registry.npmjs.org/:\_authToken=\$NPM_TOKEN" > .npmrc && \
npm ci --production && \
rm -f .npmrc
FROM build as prod
COPY --from=build /dist /dist
CMD ["node","index.js"]
# The ARG and .npmrc won't appear in the final image but can be found in the Docker daemon un-tagged images list - make sure to delete those
```
</details>
<br/><br/>
### Code Example Anti Pattern Using build time args
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
ARG NPM_TOKEN
WORKDIR /usr/src/app
COPY . /dist
RUN echo "//registry.npmjs.org/:\_authToken=\$NPM_TOKEN" > .npmrc && \
npm ci --production && \
rm -f .npmrc
# Deleting the .npmrc within the same copy command will not save it inside the layer, however it can be found in image history
CMD ["node","index.js"]
```
</details>
<br/><br/>
### Blog Quote: "These secrets arent saved in the final Docker"
From the blog, [Alexandra Ulsh](https://www.alexandraulsh.com/2019/02/24/docker-build-secrets-and-npmrc/?fbclid=IwAR0EAr1nr4_QiGzlNQcQKkd9rem19an9atJRO_8-n7oOZXwprToFQ53Y0KQ)
> In November 2018 Docker 18.09 introduced a new --secret flag for docker build. This allows us to pass secrets from a file to our Docker builds. These secrets arent saved in the final Docker image, any intermediate images, or the image commit history. With build secrets, you can now securely build Docker images with private npm packages without build arguments and multi-stage builds.
```
```

View File

@ -0,0 +1,85 @@
# Bootstrap container using node command instead of npm
## One paragraph explainer
We are used to see code examples where folks start their app using `CMD 'npm start'`. This is a bad practice. The `npm` binary will not forward signals to your app which prevents graceful shutdown (see [/sections/docker/graceful-shutdown.md]). If you are using Child-processes they wont be cleaned up correctly in case of unexpected shutdown, leaving zombie processes on your host. `npm start` also results in having an extra process for no benefit. To start you app use `CMD ['node','server.js']`. If your app spawns child-processes also use `TINI` as an entrypoint.
### Code example - Bootsraping using Node
```dockerfile
FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force
CMD ["node", "server.js"]
```
### Code example - Using Tiny as entrypoint
```dockerfile
FROM node:12-slim AS build
# Add Tini if using child-processes
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force
ENTRYPOINT ["/tini", "--"]
CMD ["node", "server.js"]
```
### Antipatterns
Using npm start
```dockerfile
FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force
# dont do that!
CMD "npm start"
```
Using node in a single string will start a bash/ash shell process to execute your command. That is almost the same as using `npm`
```dockerfile
FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force
# dont do that, it will start bash
CMD "node server.js"
```
Starting with npm, heres the process tree:
```
$ ps falx
UID PID PPID COMMAND
0 1 0 npm
0 16 1 sh -c node server.js
0 17 16 \_ node server.js
```
There is no advantage to those two extra process.
Sources:
https://maximorlov.com/process-signals-inside-docker-containers/
https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#handling-kernel-signals

View File

@ -0,0 +1,27 @@
# Clean NODE_MODULE cache
<br/><br/>
### One Paragraph Explainer
Node package managers, npm & Yarn, cache the installed packages locally so that future projects which need the same libraries won't need to fetch from a remote repository. Although this duplicates the packages and consumes more storage - it pays off in a local development environment that typically keeps installing the same packages. In a Docker container this storage increase is worthless since it installs the dependency only once. By removing this cache, using a single line of code, tens of MB are shaved from the image. While doing so, ensure that it doesn't exit with non-zero code and fail the CI build because of caching issues - This can be avoided by including the --force flag.
*Please not that this is not relevant if you are using a multi-stage build as long as you don't install new packages in the last stage*
<br/><br/>
### Code Example Clean cache
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim AS build
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --production && npm clean cache --force
# The rest comes here
```
</details>

View File

@ -4,7 +4,7 @@
### One Paragraph Explainer
The Docker build command copies the local files into the build context environment over a virtual network. Be careful - development and CI folders contain secrets like .npmrc, .aws, .env files, and other sensitive files. Consequently, Docker images might hold secrets and expose them in unsafe territories (e.g. Docker repository, partners servers). In a better world, the Dockerfile should be explicit about what is being copied. On top of this, include a .dockerignore file that acts as the last safety net that filters out unnecessary folders and potential secrets. Doing so also boosts the build speed - By leaving out common development folders that have no use in production (e.g. .git, test results, IDE configuration), the build can better utilize the cache and achieve better performance
The Docker build command copies the local files into the build context environment over a virtual network. Be careful - development and CI folders contain secrets like .npmrc, .aws, .env files and other sensitive files. Consequently, Docker images might hold secrets and expose them in unsafe territories (e.g. Docker repository, partners servers). In a better world the Dockerfile should be explicit about what is being copied. On top of this include a .dockerignore file that acts as the last safety net that filters out unnecessary folders and potential secrets. Doing so also boosts the build speed - By leaving out common development folders that have no use in production (e.g. .git, test results, IDE configuration), the builder can better utilize the cache and achieve better performance
<br/><br/>

View File

@ -0,0 +1,29 @@
[✔]: ../../assets/images/checkbox-small-blue.png
# Common Node.js Docker best practices
This common Docker guidelines section contains best practices that are standardized among all programming languages and have no special Node.js interpretation
## ![✔] Prefer COPY over ADD command
**TL;DR:** COPY is safer as it copies local files only while ADD supports fancier fetches like downloading binaries from remote sites
## ![✔] Avoid updating the base OS
**TL;DR:** Updating the local binaries during build (e.g. apt-get update) creates inconsistent images every time it runs and also demands elevated privileges. Instead use base images that are updated frequently
## ![✔] Classify images using labels
**TL;DR:** Providing metadata for each image might help Ops professionals treat it adequately. For example, include the maintainer name, build date and other information that might prove useful when someone needs to reason about an image
## ![✔] Use unprivileged containers
**TL;DR:** Privileged container have the same permissions and capabilities as the root user over the host machine. This is rarely needed and as a rule of thumb one should use the 'node' user that is created within official Node images
## ![✔] Inspect and verify the final result
**TL;DR:** Sometimes it's easy to overlook side effects in the build process like leaked secrets or unnecessary files. Inspecting the produced image using tools like [Dive](https://github.com/wagoodman/dive) can easily help to identify such issues
## ![✔] Perform integrity check
**TL;DR:** While pulling base or final images, the network might be mislead and redirected to download malicious images. Nothing in the standard Docker protocol prevents this unless signing and verifying the content. [Docker Notary](https://docs.docker.com/notary/getting_started/) is one of the tools to achieve this

View File

@ -0,0 +1,84 @@
# Shutdown gracefully
<br/><br/>
### One Paragraph Explainer
In a Dockerized runtime like Kubernetes, containers are born and die frequently. This happens not only when errors are thrown but also for good reasons like relocating containers, replacing them with a newer version and more. It's achieved by sending a notice (SIGTERM signal) to the process with a 30 second grace period. This puts a challenge on the developer to ensure the app is handling the ongoing requests and clean-up resources in a timely fashion. Otherwise thousands of sad users will not get a response. Implementation-wise, the shutdown code should wait until all ongoing requests are flushed out and then clean-up resources. Easier said than done, practically it demands orchestrating several parts: Tell the LoadBalancer that the app is not ready to serve more requests (via health-check), wait for existing requests to be done, avoid handling new requests, clean-up resources and finally log some useful information before dying. If Keep-Alive connections are being used, the clients must also be notified that a new connection should be established - A library like [Stoppable](https://github.com/hunterloftis/stoppable) can greatly help achieving this.
<br/><br/>
### Code Example Placing Node.js as the root process allows passing signals to the code (see [/sections/docker/bootstrap-using-node.md])
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
# Build logic comes here
CMD ["node", "index.js"]
#This line above will make Node.js the root process (PID1)
```
</details>
<br/><br/>
### Code Example Using Tiny process manager to forward signals to Node
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
# Build logic comes here
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["node", "index.js"]
#Now Node will run a sub-process of TINI which acts as PID1
```
</details>
<br/><br/>
### Code Example Anti Pattern Using npm scripts to initialize the process
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
# Build logic comes here
CMD ["npm", "start"]
#Now Node will run a sub-process of npm and won't receive signals
```
</details>
<br/><br/>
### Example - The shutdown phases
From the blog, [Rising Stack](https://blog.risingstack.com/graceful-shutdown-node-js-kubernetes/)
![alt text](/assets/images/Kubernetes-graceful-shutdown-flowchart.png "The shutdown phases")

View File

@ -0,0 +1,27 @@
# Understand image tags vs digests and use the `:latest` tag with caution
### One Paragraph Explainer
If this is a production situation and security and stability are important then just "convenience" is likely not the best deciding factor. In addition the `:latest` tag is Docker's default tag. This means that a developer who forgets to add an explicit tag will accidentally push a new version of an image as `latest`, which might end in very unintended results if the `latest` tag is being relied upon as the latest production image.
### Code example:
```bash
$ docker build -t company/image_name:0.1 .
# :latest image is not updated
$ docker build -t company/image_name
# :latest image is updated
$ docker build -t company/image_name:0.2 .
# :latest image is not updated
$ docker build -t company/image_name:latest .
# :latest image is updated
```
### What Other Bloggers Say
From the blog by [Vladislav Supalov](https://vsupalov.com/docker-latest-tag/):
> Some people expect that :latest always points to the most-recently-pushed version of an image. Thats not true.
From the [Docker success center](https://success.docker.com/article/images-tagging-vs-digests)
>
<br/>

View File

@ -4,7 +4,7 @@
### One Paragraph Explainer
Dev dependencies greatly increase the container attack surface (i.e. potential security weakness) and the container size. As an example, some of the most impactful npm security breaches were originated from devDependecies like [eslint-scope](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes) or affected dev packages like [event-stream that was used by nodemon](https://snyk.io/blog/a-post-mortem-of-the-malicious-event-stream-backdoor/). For those reasons, the image that is finally shipped to production should be safe and minimal. Running npm install with a --production is a great start however it get even safer to run 'npm ci' that ensure a fresh install and the existence of lock file. Removing the local cache can shave additional tens of MB. Often there is a need to test or debug within a container using devDependencies - In that case, multi stage builds can help in having different sets of dependencies and finally only those for production (See dedicated bullet on multi stage builds)
Dev dependencies greatly increase the container attack surface (i.e. potential security weakness) and the container size. As an example, some of the most impactful npm security breaches were originated from devDependecies like [eslint-scope](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes) or affected dev packages like [event-stream that was used by nodemon](https://snyk.io/blog/a-post-mortem-of-the-malicious-event-stream-backdoor/). For those reasons the image that is finally shipped to production should be safe and minimal. Running npm install with a `--production` is a great start, however it gets even safer to run 'npm ci' that ensures a fresh install and the existence of a lock file. Removing the local cache can shave additional tens of MB. Often there is a need to test or debug within a container using devDependencies - In that case, multi stage builds can help in having different sets of dependencies and finally only those for production (See dedicated bullet on multi stage builds)
<br/><br/>
@ -25,6 +25,37 @@ RUN npm ci --production && npm clean cache --force
</details>
<br/><br/>
### Code Example Installing for production with multi-stage build
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:14.8.0-alpine AS build
COPY --chown=node:node package.json package-lock.json ./
# ✅ Safe install
RUN npm ci
COPY --chown=node:node src ./src
RUN npm run build
# Run-time stage
FROM node:14.8.0-alpine
COPY --chown=node:node --from=build package.json package-lock.json ./
COPY --chown=node:node --from=build node_modules ./node_modules
COPY --chown=node:node --from=build dist ./dist
# ✅ Clean dev packages
RUN npm prune --production
CMD [ "node", "dist/app.js" ]
```
</details>
<br/><br/>
### Code Example Anti-Pattern Installing all dependencies

View File

@ -0,0 +1,25 @@
# Lint your Dockerfile
### One Paragraph Explainer
As our core application code is linted to conform to best practices and eliminate issues and bugs before it could become a problem, so too should our Dockerfiles. Linting the Dockerfile means increasing the chances of catching production issues on time with very light effort. For example, it can ensure that there arent any structural problems with the logic and instructions specified in your Dockerfiles like trying to copy from non-existing stage, copying from unknown online repository, running the app with power user (SUDO) and many more. The Open Source Dockerfile linter [Hadolint](https://github.com/hadolint/hadolint) can be used manually or as part of a CI process to lint your Dockerfile/s. Hadolint is a specialized Dockerfile linter that aims to embrace the [Docker best practices.](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
<br/>
### Code example: Inspecting a Dockerfile using hadolint
```bash
hadolint production.Dockerfile
hadolint --ignore DL3003 --ignore DL3006 <Dockerfile> # exclude specific rules
hadolint --trusted-registry my-company.com:500 <Dockerfile> # Warn when using untrusted FROM images
```
### What Other Bloggers Say
From the blog by [Josh Reichardt](https://thepracticalsysadmin.com/lint-your-dockerfiles-with-hadolint/):
> If you havent already gotten in to the habit of linting your Dockerfiles you should. Code linting is a common practice in software development which helps find, identify and eliminate issues and bugs before they are ever able to become a problem. One of the main benefits of linting your code is that it helps identify and eliminate nasty little bugs before they ever have a chance to become a problem.
From the blog by [Jamie Phillips](https://www.phillipsj.net/posts/hadolint-linting-your-dockerfile/)
> Linters are commonly used in development to help teams detect programmatic and stylistic errors. Hadolint is a linter created for Dockerfiles using Haskell. This tool validates against the best practices outlined by Docker and takes a neat approach to parse the Dockerfile that you should checkout. It supports all major platforms, and this tutorial will be leveraging the container to perform the linting on an example Dockerfile.
<br/>

View File

@ -0,0 +1,70 @@
# Set memory limits using Docker only
<br/><br/>
### One Paragraph Explainer
A memory limit tells the process/container the maximum allowed memory usage - a request or usage beyond this number will kill the process (OOMKill). Applying this is a great practice to ensure one citizen doesn't drink all the juice alone and leaves other components to starve. Memory limits also allow the runtime to place a container in the right instance - placing a container that consumes 500MB in an instance with 300MB memory available will lead to failures. Two different options allow configuring this limit: V8 flags (--max-old-space-size) and the Docker runtime, both are absolutely needed. Ensure to always configure the Docker runtime limits as it has a much wider perspective for making the right health decisions: Given this limit, the runtime knows how to scale and create more resources. It can also make a thoughtful decision on when to crash - if a container has a short burst in memory request and the hosting instance is capable of supporting this, Docker will let the container stay alive. Last, with Docker the Ops experts can set various production memory configurations that can be taken into account like memory swap. This by itself won't be enough - Without setting v8's --max-old-space-size, the JavaScript runtime won't push the garbage collection when getting close to the limits and will also crash when utilizing only 50-60% of the host environment. Consequently, set v8's limit to be 75-10% of Docker's memory limit.
<br/><br/>
### Code Example Memory limit with Docker
<details>
<summary><strong>Bash</strong></summary>
```
docker run --memory 512m my-node-app
```
</details>
<br/><br/>
### Code Example Memory limit with Kubernetes and v8
<details>
<summary><strong>Kubernetes deployment yaml</strong></summary>
```
apiVersion: v1
kind: Pod
metadata:
name: my-node-app
spec:
containers:
- name: my-node-app
image: my-node-app
resources:
requests:
memory: "400Mi"
limits:
memory: "500Mi"
command: ["node index.js --max-old-space-size=350"]
```
</details>
<br/><br/>
### Kubernetes documentation: "If you do not specify a memory limit"
From [K8S documentation](https://kubernetes.io/docs/tasks/configure-pod-container/assign-memory-resource/)
> The Container has no upper bound on the amount of memory it uses. The Container could use all of the memory available on the Node where it is running which in turn could invoke the OOM Killer. Further, in case of an OOM Kill, a container with no resource limits will have a greater chance of being killed.
<br/><br/>
### Docker documentation: "it throws an OOME and starts killing processes "
From [Docker official docs](https://docs.docker.com/config/containers/resource_constraints/)
> It is important not to allow a running container to consume too much of the host machines memory. On Linux hosts, if the kernel detects that there is not enough memory to perform important system functions, it throws an OOME, or Out Of Memory Exception, and starts killing processes to free up memory.
<br/><br/>
### Node.js documentation: "V8 will spend more time on garbage collection"
From [Node.js official docs](https://nodejs.org/api/cli.html#cli_max_old_space_size_size_in_megabytes)
> Sets the max memory size of V8's old memory section. As memory consumption approaches the limit, V8 will spend more time on garbage collection in an effort to free unused memory. On a machine with 2GB of memory, consider setting this to 1536 (1.5GB) to leave some memory for other uses and avoid swapping.

View File

@ -0,0 +1,114 @@
# Use multi-stage builds
### One Paragraph Explainer
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 also have to expose environment variables during build that should not be present at runtime (see [/sections/docker/avoid-build-time-secrets.md]), 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
Let's imagine the following directory structure
```
- Dockerfile
- src/
- index.ts
- package.json
- yarn.lock
- .dockerignore
- docs/
- README.md
```
Your [.dockerignore](/sections/docker/dockerignore.md) will already filter out files that won't be needed for building and running your application.
```
# 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 with multiple stages
```dockerfile
FROM node:14.4.0 AS build
COPY --chown=node:node . .
RUN yarn install && yarn build
FROM node:14.4.0
USER node
EXPOSE 8080
# Copy results from previous stage
COPY --chown=node:node --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/yarn.lock ./
RUN yarn install --production
CMD [ "node", "dist/app.js" ]
```
#### Dockerfile with multiple stages and different base images
```dockerfile
FROM node:14.4.0 AS build
COPY --chown=node:node . .
RUN yarn install && yarn build
# This will use a minimal base image for the runtime
FROM node:14.4.0-alpine
USER node
EXPOSE 8080
# Copy results from previous stage
COPY --chown=node:node --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/yarn.lock ./
RUN yarn install --production
CMD [ "node", "dist/app.js" ]
```
#### Full Dockerfile with multiple stages and different base images
Our Dockerfile will contain two phases: One for building the application using the fully-featured Node.js Docker image,
and a second phase for running the application, based on the minimal Alpine image. We'll only copy over the built files to our second stage,
and then install production dependencies.
```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 install --frozen-lockfile
# 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 dependency information and install production-only dependencies
COPY --chown=node:node package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production
# Copy results from previous stage
COPY --chown=node:node --from=build /home/node/app/dist ./dist
CMD [ "node", "dist/app.js" ]
```

View File

@ -0,0 +1,44 @@
# Let the Docker orchestrator restart and replicate processes
<br/><br/>
### One Paragraph Explainer
Docker runtime orchestrators like Kubernetes are really good at making containers health and placement decisions: They will take care to maximize the number of containers, balance them across zones, and take into account many cluster factors while making these decisions. Goes without words, they identify failing processes (i.e., containers) and restart them in the right place. Despite that some may be tempted to use custom code or tools to replicate the Node process for CPU utilization or restart the process upon failure (e.g., Cluster module, PM2). These local tools don't have the perspective and the data that is available on the cluster level. For example, when the instances resources can host 3 containers and given 2 regions or zones, Kubernetes will take care to spread the containers across zones. This way, in case of a zonal or regional failure, the app will stay alive. On the contrary side when using local tools for restarting the process the Docker orchestrator is not aware of the errors and can not make thoughtful decisions like relocating the container to a new instance or zone.
<br/><br/>
### Code Example Invoking Node.js directly without intermediate tools
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
# The build logic comes here
CMD ["node", "index.js"]
```
</details>
<br/><br/>
### Code Example Anti Pattern Using a process manager
<details>
<summary><strong>Dockerfile</strong></summary>
```
FROM node:12-slim
# The build logic comes here
CMD ["pm2-runtime", "indes.js"]
```
</details>

View File

@ -4,7 +4,7 @@
### One Paragraph Explainer
Scanning the code for vulnerabilities is a valuable act, but it doesn't cover all the potential threats. Why? Because vulnerabilities also exist on the OS level and the app might execute those binaries like Shell, Tarball, OpenSSL. Also, vulnerable dependencies might be injected after the code scan (i.e. supply chain attacks) - hence scanning the final image just before production is in order. This idea resembles E2E tests - after testing the various pieces in-isolation, it's valuable to finally check the assmebled deliverable. There are 3 main scanner families: Local/CI binaries with a cached vulnerabilities DB, scanners as a service in the cloud and a niche of tools which scan during the docker build itself. The first group is the most popular and usually the fastest - Tools like [Trivvy](https://github.com/aquasecurity/trivy), [Anchore](https://github.com/anchore/anchore) and [Snyk](https://support.snyk.io/hc/en-us/articles/360003946897-Container-security-overview) are worth exploring. Most CI vendors provide a local plugin that facilitates the interaction with these scanners. It should be noted that these scanners cover a lot of ground and therefore will show findings in almost every scan - consider setting a high threshold bar to avoid getting overwhelmed
Scanning the code for vulnerabilities is a valuable act but it doesn't cover all the potential threats. Why? Because vulnerabilities also exist on the OS level and the app might execute those binaries like Shell, Tarball, OpenSSL. Also, vulnerable dependencies might be injected after the code scan (i.e. supply chain attacks) - hence scanning the final image just before production is in order. This idea resembles E2E tests - after testing the various pieces in-isolation, it's valuable to finally check the assembled deliverable. There are 3 main scanner families: Local/CI binaries with a cached vulnerabilities DB, scanners as a service in the cloud and a niche of tools which scan during the docker build itself. The first group is the most popular and usually the fastest - Tools like [Trivvy](https://github.com/aquasecurity/trivy), [Anchore](https://github.com/anchore/anchore) and [Snyk](https://support.snyk.io/hc/en-us/articles/360003946897-Container-security-overview) are worth exploring. Most CI vendors provide a local plugin that facilitates the interaction with these scanners. It should be noted that these scanners cover a lot of ground and therefore will show findings in almost every scan - consider setting a high threshold bar to avoid getting overwhelmed
<br/><br/>

View File

@ -0,0 +1,13 @@
# Prefer smaller Docker base images
Large Docker images can lead to higher exposure to vulnerabilities and increased resource consumption. Often you don't need certain packages installed at runtime that are needed for building.
Pulling and storing larger images will become more expensive at scale, when dealing with larger images. By design minimal images may not come with common libraries needed for building native modules or packages useful for debugging (e.g. curl) pre-installed.
Using the Alpine Linux variants of images can lead to a reduced footprint in terms of resources used and the amount of attack vectors present in fully-featured systems. The Node.js v14.4.0 Docker image is ~345MB in size versus ~39MB for the Alpine version, which is almost 10x smaller.
A Slim variant based on Debian, which is only 38MB in size and contains the minimal packages needed to run Node.js, is also a great choice.
### Blog Quote: "If you want to shrink your Docker images, have your services start faster and be more secure then try Alpine out."
From [Nick Janetakis' blog](https://nickjanetakis.com/blog/the-3-biggest-wins-when-using-alpine-as-a-base-docker-image)
> Its no secret by now that Docker is heavily using Alpine as a base image for official Docker images. This movement started near the beginning of 2016. [...]
When pulling down new Docker images onto a fresh server, you can expect the initial pull to be quite a bit faster on Alpine. The slower your network is, the bigger the difference it will be. [...] Another perk of being much smaller in size is that the surface area to be attacked is much less. When theres not a lot of packages and libraries on your system, theres very little that can go wrong.

View File

@ -0,0 +1,115 @@
# Leverage caching to reduce build times
## One paragraph explainer
Docker images are a combination of layers, each instruction in your Dockerfile creates a layer. The docker daemon can reuse those layers between builds if the instructions are identical or in the case of a `COPY` or `ADD` files used are identical. ⚠️ If the cache can't be used for a particular layer all the subsequent layers will be invalidated too. That's why order is important. It is crucial to layout your Dockerfile correctly to reduce the number of moving parts in your build; the less updated instructions should be at the top and the ones constantly changing (like app code) should be at the bottom. It's also important to think that instructions that trigger long operation should be close to the top to ensure they happen only when really necessary (unless it changes every time you build your docker image). Rebuilding a whole docker image from cache can be nearly instantaneous if done correctly.
![Docker layers](/assets/images/docker_layers_schema.png)
* Image taken from [Digging into Docker layers](https://medium.com/@jessgreb01/digging-into-docker-layers-c22f948ed612) by jessgreb01*
### Rules
#### Avoid LABEL that change all the time
If you have a label containing the build number at the top of your Dockerfile, the cache will be invalidated at every build
```Dockerfile
#Beginning of the file
FROM node:10.22.0-alpine3.11 as builder
# Don't do that here!
LABEL build_number="483"
#... Rest of the Dockerfile
```
#### Have a good .dockerignore file
[**See: On the importance of docker ignore**](/sections/docker/docker-ignore.md)
The docker ignore avoids copying files that could bust our cache logic, like tests results reports, logs or temporary files.
#### Install "system" packages first
It is recommended to create a base docker image that has all the system packages you use. If you **really** need to install packages using `apt`,`yum`,`apk` or the likes, this should be one of the first instructions. You don't want to reinstall make,gcc or g++ every time you build your node app.
**Do not install package only for convenience, this is a production app.**
#### First, only ADD your package.json and your lockfile
```Dockerfile
COPY "package.json" "package-lock.json" "./"
RUN npm ci
```
The lockfile and the package.json change less often. Copying them first will keep the `npm install` step in the cache, this saves precious time.
### Then copy your files and run build step (if needed)
```Dockerfile
COPY . .
RUN npm run build
```
## Examples
### Basic Example with node_modules needing OS dependencies
```Dockerfile
#Create node image version alias
FROM node:10.22.0-alpine3.11 as builder
RUN apk add --no-cache \
build-base \
gcc \
g++ \
make
USER node
WORKDIR /app
COPY "package.json" "package-lock.json" "./"
RUN npm ci --production
COPY . "./"
FROM node as app
USER node
WORKDIR /app
COPY --from=builder /app/ "./"
RUN npm prune --production
CMD ["node", "dist/server.js"]
```
### Example with a build step (when using typescript for example)
```Dockerfile
#Create node image version alias
FROM node:10.22.0-alpine3.11 as builder
RUN apk add --no-cache \
build-base \
gcc \
g++ \
make
USER node
WORKDIR /app
COPY "package.json" "package-lock.json" "./"
RUN npm ci
COPY . .
RUN npm run build
FROM node as app
USER node
WORKDIR /app
# Only copying the files that we need
COPY --from=builder /app/node_modules node_modules
COPY --from=builder /app/package.json .
COPY --from=builder /app/dist dist
RUN npm prune --production
CMD ["node", "dist/server.js"]
```
## Useful links
Docker docks: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache