Skip to main content

Docker and Dumb-Init

· 5 min read
Abdulmalik

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:

  1. Signal handling — it receives Unix signals such as SIGTERM and decides what to pass on.
  2. 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.

ToolAuthorDefault in Docker?Typical base image
dumb-initYelpNoGood for Debian/Ubuntu/general images
tinikrallinYes, included in docker run --initGood 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:

  • tini or dumb-init becomes PID 1.
  • "--" separates the init arguments from the command it will run.
  • The application runs as a child process.
  • Signals such as SIGTERM and SIGINT are 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:

  1. Explain why running an app as PID 1 can cause signal and process-reaping problems.
  2. Add either dumb-init or tini to a Dockerfile for Alpine, Debian/Ubuntu, or a binary-download scenario.
  3. Write a correct ENTRYPOINT/CMD pair that keeps the init system as PID 1.
  4. Use the same pattern in a Kubernetes manifest when you cannot rebuild the image.

References


Comments