Getting everything right with your Node.js applications in Docker is hard. Features and bugs swallow your precious time, and deadlines are looming over the horizon. Following best practices right now improves your security, decreases deployment times, and saves you headaches later on.
Do you and your team follow all of these? If not, try implementing them in your project.
At least in production (and preferably in your E2E tests, should you have any), your
NODE_ENV environment variable should be set to
This practice once started with the Express web server, and now many libraries listen for this env variable and enable optimizations or disable some logging/debugging code. This speeds up your application and may increase your security by disabling certain program paths you only need during development.
The default Linux user inside Docker containers is
root user has elevated privileges.
When we use it to run our process inside Docker containers, we increase the likelihood that an attacker can obtain access to the Docker host, should they gain control via our running application.
All node Docker images have the node user which has no
sudo privileges. Use it like this in your
# make sure to setup permissions for files correctly # see example Dockerfile below USER node
The process that you start first within your Docker container has process ID 1 (PID 1).
Processes that run as PID 1 have several additional responsibilities to other processes, such as reaping zombie processes.
Node.js is not designed to run with PID 1.
If Node.js is running as PID 1, it will not appropriately respond to signals like
--init flag of Docker or add Tini within your
Dockerfile. This is even advised in Kubernetes scenarios.
# example for alpine ... RUN apk add --no-cache tini # Tini is now available at /sbin/tini ENTRYPOINT ["/sbin/tini", "--"] CMD ["node", "index.js"]
Want to learn more about why Tini is awesome? Read this explanation by its creator. You'll learn a whole lot about Docker and processes.
Using npm in your
CMD definition adds additional startup time to your containers and at least one running process.
npm can also swallow exit signals such as
CMD ["node", "index.js"]
Even-numberer Node.js versions are Long Term Support (LTS) releases. They receive security and stability updates for two and a half years after a newer current version is released. Yes, many teams already use LTS versions of Node.js. But using an LTS version does not help if you don't regularly update your base image. Updating your base image does update not only your version of Node.js but also the version of the underlying Linux system and packages. Even with tiny images like Alpine, it is crucial to stay up-to-date with security updates of your distribution.
Solution: Add a helper tool like diun to your CI pipeline to get (Slack) notifications when an update to your base image is available.
Herok co-founder Adam Wiggins created the 12-factor methodology. It helps to improve developing, building, deploying, and running decoupled stateless microservices, but in my opinion, it also helps with single services. Using it improves the time and cost to onboard new developers to a project and increases portability between execution environments. 12-factor apps are also suitable for deployment on modern cloud platforms and play nicely with containers. They minimize the divergence between development and production and enable continuous deployment. As an additional benefit, scaling up and scaling out is more straightforward with apps that follow these methodologies.
You can find more info about the 12-factor method on the 12-factor homepage.
If you need to build your application before running it, you probably have a build step in your Dockerfile. Your initial Dockerfile might look like this:
FROM node:10 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . RUN npm run build EXPOSE 8080 CMD [ "node", "dist/index.js" ]
Dockerfile is already somewhat optimized for build speed/caching, because only the package*.json files are copied over before installing dependencies. With this, npm install is only called when something changes in package.json.
RUN npm run build, bundles your whole application code.
Now, after building your application, you don't need your dev dependencies and unbundled application code anymore, but they are still included in the original image.
Solution: Use multi-stage Dockerfiles.
# build stage FROM node:12.16.1-alpine3.11 AS build WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # cmd stage FROM node:12.16.1-alpine3.11 WORKDIR /usr/src/app COPY package*.json ./ RUN NODE_ENV=production npm install COPY --from=appbuild /usr/src/app/dist ./dist EXPOSE 8080 CMD [ "node", "dist/index.js" ]
There are many things to consider when using Node.js with Docker that are not obvious during the initial research. I hope this article helps you to build more stable and secure Docker images in the future.
I created an example Dockerfile based on the recommendations in this article. You can find it on this GitHub page.