Docker and Dumb-Init
When it comes to graceful shutdown, signal forwarding, and reducing attack surface in containerized environments, an init process belongs in the conversation. The two smallest options are dumb-init and tini.
Why PID 1 matters in a container
When a container starts, the first process becomes PID 1. PID 1 has two responsibilities that normal child processes do not:
- Signal handling — it receives Unix signals such as
SIGTERMand decides what to pass on. - Zombie reaping — it must wait on orphaned child processes that exit before their parent.
Most applications are not written to be PID 1. If your Node.js or shell script ends up as PID 1, a docker stop or Kubernetes pod termination can hang because the app never receives the signal, or stale processes can accumulate.
A concrete example: with CMD ["npm", "start"] in a Dockerfile, the npm process becomes PID 1. Node.js underneath may not forward SIGTERM cleanly, so graceful shutdown fails. You can see this in ps output inside the container:
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
The same problem appears with shell-based entrypoints such as ENTRYPOINT ["source", "-c", "env && ./app.sh"]. The shell becomes PID 1 and may not forward signals to the actual application.
Dumb-init vs Tini
Both tools solve the same problem with a tiny footprint.
| Tool | Author | Default in Docker? | Typical base image |
|---|---|---|---|
| dumb-init | Yelp | No | Good for Debian/Ubuntu/general images |
| tini | krallin | Yes, included in docker run --init | Good for Alpine because it is in apk |
I use tini for Alpine-based images because it is one apk add away. dumb-init works the same way and is a good choice for other bases. Pick one and use it consistently.
The pattern: init first, app second
The recommended Dockerfile pattern is:
ENTRYPOINT ["tini", "--"]
CMD ["npm", "start"]
Or with dumb-init:
ENTRYPOINT ["dumb-init", "--"]
CMD ["npm", "start"]
What this does:
tiniordumb-initbecomes PID 1."--"separates the init arguments from the command it will run.- The application runs as a child process.
- Signals such as
SIGTERMandSIGINTare forwarded correctly.
A single-command ENTRYPOINT also works, but you must pass the command as separate array elements. This is wrong because the shell will not parse a space inside the string:
# Bad: "npm start" is treated as one argument
ENTRYPOINT ["dumb-init", "npm start"]
Use either ENTRYPOINT ["dumb-init", "--", "npm", "start"] or split the static and overridable parts across ENTRYPOINT and CMD.
Installing per base image
Choose the line that matches your base image.
Alpine
FROM alpine:latest
RUN apk add --no-cache tini
...
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["your-app"]
For dumb-init on Alpine:
FROM alpine:latest
RUN apk add --no-cache dumb-init
...
ENTRYPOINT ["dumb-init", "--"]
CMD ["your-app"]
Debian / Ubuntu
FROM ubuntu:latest
RUN apt-get update && apt-get install -y --no-install-recommends tini
...
ENTRYPOINT ["tini", "--"]
CMD ["your-app"]
For dumb-init:
FROM ubuntu:latest
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
...
ENTRYPOINT ["dumb-init", "--"]
CMD ["your-app"]
Download the binary directly
If the package is not available, download a release binary. Use a pinned version and verify the checksum in production.
FROM node:latest
ENV DUMB_INIT_VERSION=1.2.5
ADD https://github.com/Yelp/dumb-init/releases/download/v${DUMB_INIT_VERSION}/dumb-init_${DUMB_INIT_VERSION}_x86_64 /usr/local/bin/dumb-init
RUN chmod +x /usr/local/bin/dumb-init
...
ENTRYPOINT ["/usr/local/bin/dumb-init", "--"]
CMD ["npm", "start"]
Python
The official python image is Debian-based, so use apt-get for tini:
FROM python:latest
RUN apt-get update && apt-get install -y --no-install-recommends tini
...
ENTRYPOINT ["tini", "--"]
CMD ["python", "app.py"]
For dumb-init via pip:
FROM python:latest
RUN pip install --no-cache-dir dumb-init
...
ENTRYPOINT ["dumb-init", "--"]
CMD ["python", "app.py"]
Kubernetes usage
When you control the container image, install the init system there. When you do not, you can add it at the pod level:
containers:
- image: your-image:latest
name: app
command: ['dumb-init', 'sh', '-c']
args: ['npm start']
A cleaner long-term fix is rebuilding the image with ENTRYPOINT ["dumb-init", "--"] so the Kubernetes manifest stays simple.
Security note: init alone is not enough
Running an init process reduces the attack surface related to PID 1, such as privilege-escalation controls that do not apply to PID 1. It is still only one layer. For production images you should also:
- Use multi-stage builds.
- Run as a non-root user.
- Pin base image versions and package versions.
- Keep the image small.
Completion criterion
After reading this, you should be able to:
- Explain why running an app as PID 1 can cause signal and process-reaping problems.
- Add either dumb-init or tini to a Dockerfile for Alpine, Debian/Ubuntu, or a binary-download scenario.
- Write a correct
ENTRYPOINT/CMDpair that keeps the init system as PID 1. - Use the same pattern in a Kubernetes manifest when you cannot rebuild the image.
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/