Docker and Dumb-Init
when it comes to containerized environment graceful shutdown, process management and reducing attack surface, I believe we can't leave dumb-init and tini out of it.
What is Dumb-init
Dumb-init, is a tool developed by Yelp which is a simple and non-heavy process management tool with less footprint, so that way we don't end up exposing our applications to more issues through the tooling security vulnerabilities.
dumb-init can also be described as a tool that serves as a parent process to another process in your docker or containers.
What is Tini
Tini, created by krallin, is another minimal init system specifically designed for containers. It's been increasingly adopted in container ecosystems, including being the default init system in Docker and other container runtimes.
Why use dumb-init or tini with your docker or containers
- Signal Forwarding
when you have the following CMD ["npm start"] in your Dockerfile, then if the npm start runs in your container, it will take the process PID 1 which is an init process, the first process that runs at the boot time of any Unix system.
PID USER TIME COMMAND
1 node 0:00 npm start
6 node 0:01 node /app/node_modules/.bin/ts-node-dev --poll src/index.ts
17 node 4:09 sh
24 node 0:58 ps
Knowing that Node.js is not designed to run as PID 1, so running it as PID 1 would make it give some unexpected behaviors such as the app not responding to the SIGTERM and other signals.
or if you have the following ENTRYPOINT ["source -c env && ./app.sh"] in your docker file, then source -c env && ./app.sh
will become PID 1 and then the started application itself becomes a subprocess and
PID USER TIME COMMAND
1 node 0:00 source -c env && ./app.sh
6 node 0:01 app.sh -- Listening on PORT 9000
since the app itself is the one configured to handle SIGINT and SIGTERM, the shell script won't be able to pass those signals to the app.
- Reduced attack surface
Since the container process is wrapped around dumb-init and other processes are now child of the dumb-init process which is now PID 1 process, this shields your app process against responsibilities that comes with PID 1
for example, if you want to make sure your app process doesn't assume new privileges in the container, you can simply set prctl(PR_SET_NO_NEW_PRIVS, 1) or use securityContext: allowPrivilegeEscalation=false, but this Linux kernel feature doesn't work with PID 1 process, instead, it works with the child processes.
So letting your main app process assume PID 1 in the container they are running can be risky.
Using Dumb-Init with your Dockerfile
You will have to install dumb-init in the Dockerfile, also it's advisable to call this before other command layers.
now you can do this in two ways, using ENTRYPOINT, CMD or yaml file if you are working with Kubernetes.
with ENTRYPOINT, you can call the dumb-init to be the PID 1
FROM node:latest
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
....
ENTRYPOINT ["dumb-init","npm start"]
with ENTRYPOINT and CMD, you can call the dumb-init to be the PID 1
FROM node:latest
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
....
ENTRYPOINT ["dumb-init", "--"]
CMD ["npm start"]
but in a situation where you are experiencing issues getting dumb-init in your docker images, there are other ways to get on depending on the system.
generalized containers with dumb-init binary
FROM node:latest
RUN wget -O /usr/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64
chmod +x /usr/local/bin/dumb-init
....
ENTRYPOINT ["dumb-init", "--"]
CMD ["npm start"]
Ubuntu
FROM ubuntu:latest
RUN apt install -y dumb-init
....
ENTRYPOINT ["dumb-init", "--"]
Alphine
FROM alphine:latest
RUN apk add dumb-init
....
ENTRYPOINT ["dumb-init", "--"]
Python
FROM python:latest
RUN pip3 install dumb-init
....
ENTRYPOINT ["dumb-init", "--"]
Using dumb-init with Kubernetes yaml file
containers:
- image: ourdockerfile:latest-dumb-init-installed
name: container-name
command:
['dumb-init', 'sh', '-c']
args:
['npm start']
Using Tini with your Dockerfile
You will have to install Tini in the Dockerfile, also it's advisable to call this before other command layers.
now you can do this in two ways, using ENTRYPOINT, CMD or yaml file if you are working with Kubernetes.
with ENTRYPOINT, you can call the dumb-init to be the PID 1
FROM alpine:3.20.2 AS base
ENV TINI_VERSION v0.19.0
ENV NODE_VERSION=20.15.1
ENV YARN_VERSION=1.22.22
RUN apk add --no-cache \
yarn=${YARN_VERSION} \
tini=${TINI_VERSION}
....
ENTRYPOINT ["tini", "yarn", "start"]
With ENTRYPOINT and CMD, you can call Tini to be the PID 1:
FROM alpine:3.20.2 AS base
ENV TINI_VERSION v0.19.0
ENV NODE_VERSION=20.15.1
ENV YARN_VERSION=1.22.22
RUN apk add --no-cache \
yarn=${YARN_VERSION} \
tini=${TINI_VERSION}
....
ENTRYPOINT ["tini", "--"]
CMD ["yarn", "start"]
If you're experiencing issues getting Tini in your Docker images, there are other ways to install it depending on the system.
generalized containers with tini Binary
FROM node:latest
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN chmod +x /usr/bin/tini
....
ENTRYPOINT ["tini", "--"]
CMD ["npm", "start"]
Ubuntu
FROM ubuntu:latest
RUN apt-get update && apt-get install -y tini
....
ENTRYPOINT ["tini", "--"]
Alpine
FROM alpine:latest
RUN apk add --no-cache tini
....
ENTRYPOINT ["/sbin/tini", "--"]
Python
FROM python:latest
RUN apk add --no-cache tini
....
ENTRYPOINT ["/sbin/tini", "--"]
Using Tini with Kubernetes yaml file
containers:
- image: ourdockerfile:latest-tini-installed
name: container-name
command:
['tini', 'sh', '-c']
args:
['yarn start']
You can either use dumb-init or tini, implementing an init process is crucial for robust, secure containerized applications.
i have been personally been using tini because i work mostly with alpine base images, and i find it simple to use with, dumb-init works same way, just my new preference.
Also if you pay closer attention to the dockerfile layer above, you will see they are not meeting the production/security readiness for creating Dockerfiles.
It's better to use multistage building and change user ownership from root to node.
Well, that's it, folks! I hope you find this piece insightful and helpful.
References
- https://github.com/Yelp/dumb-init
- https://github.com/krallin/tini
- https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/