forked from goldbergyoni/nodebestpractices
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Sync with original text (goldbergyoni#13)
- Loading branch information
Showing
45 changed files
with
1,945 additions
and
180 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 aren’t 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 aren’t 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. | ||
``` | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 won’t 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 | ||
|
||
# don’t 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 | ||
|
||
# don’t do that, it will start bash | ||
CMD "node server.js" | ||
``` | ||
|
||
Starting with npm, here’s 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 cache clean --force | ||
# The rest comes here | ||
``` | ||
|
||
</details> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Use .dockerignore to prevent leaking secrets | ||
|
||
<br/><br/> | ||
|
||
### 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 builder can better utilize the cache and achieve better performance | ||
|
||
<br/><br/> | ||
|
||
### Code Example – A good default .dockerignore for Node.js | ||
|
||
<details> | ||
<summary><strong>.dockerignore</strong></summary> | ||
|
||
``` | ||
**/node_modules/ | ||
**/.git | ||
**/README.md | ||
**/LICENSE | ||
**/.vscode | ||
**/npm-debug.log | ||
**/coverage | ||
**/.env | ||
**/.editorconfig | ||
**/.aws | ||
**/dist | ||
``` | ||
|
||
</details> | ||
|
||
<br/><br/> | ||
|
||
### Code Example Anti-Pattern – Recursive copy of all files | ||
|
||
<details> | ||
<summary><strong>Dockerfile</strong></summary> | ||
|
||
``` | ||
FROM node:12-slim AS build | ||
WORKDIR /usr/src/app | ||
# The next line copies everything | ||
COPY . . | ||
# The rest comes here | ||
``` | ||
|
||
</details> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 [bootstrap using node](/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") |
Oops, something went wrong.