Containers and Signal Handling: Why You Need to Care About PID 1
When running applications in Docker containers, many developers overlook a critical detail: what process runs as PID 1. This seemingly minor choice can lead to unresponsive containers, resource leaks, and unexpected behavior during shutdown.
Why PID 1 is Special
In Linux, the kernel treats PID 1 differently from all other processes. It's the "init" process that bootstraps the system and has two critical responsibilities:
Signal handling: The kernel doesn't deliver certain signals (like SIGTERM) to PID 1 unless it explicitly registers handlers for them.
Process reaping: PID 1 must clean up zombie processes by calling waitpid()
on dead children.
When you run a container with:
CMD ["./my-app"]
Your application becomes PID 1, inheriting these kernel expectations whether it's designed for them or not.
What Goes Wrong
Without proper PID 1 handling, you'll encounter:
-
Unresponsive shutdowns:
docker stop
sends SIGTERM to PID 1, but if your app doesn't handle it, the kernel ignores it. Docker waits 10 seconds, then forcibly kills with SIGKILL - Zombie accumulation: Child processes that die aren't reaped, leaving zombie entries in the process table
- Orphaned processes: Background processes lose their parent but aren't properly managed
The Solution
Use a proper init system that handles PID 1 responsibilities:
# Install tini RUN apk add --no-cache tini # Use tini as entrypoint ENTRYPOINT ["/sbin/tini", "--"] CMD ["./my-app"]
Or enable Docker's built-in init:
docker run --init my-image
Another excellent option is fpco/pid1
, a minimal init system implemented in Haskell:
ENV PID1_VERSION=0.1.3.1 RUN curl -sSL "https://github.com/fpco/pid1/releases/download/v${PID1_VERSION}/pid1" -o /sbin/pid1 && \ chown root:root /sbin/pid1 && \ chmod +x /sbin/pid1 ENTRYPOINT ["/sbin/pid1", "-u", "appuser", "-g", "appgroup"] CMD ["./my-app"]
What makes fpco/pid1
particularly useful are its -u
and -g
flags, which allow you to execute the child process as a specified user and group. This eliminates the need for separate user switching logic while maintaining proper PID 1 behavior.
It is also very useful for development as it makes it possible to switch to a dynamic user that that matches your host UID so no permissons get messed up on munted host volumes.
There's also the new and improved pid1-rs
(rewritten in Rust):
ENV PID1_VERSION=0.1.2 RUN curl -sSL "https://github.com/fpco/pid1-rs/releases/download/v${PID1_VERSION}/pid1-x86_64-unknown-linux-musl" -o /sbin/pid1 && \ chown root:root /sbin/pid1 && \ chmod +x /sbin/pid1 ENTRYPOINT ["/sbin/pid1"] CMD ["./my-app"]
Even better, if you're writing Rust applications, you can embed PID 1 functionality directly by using the pid1
crate. This means your application can handle PID 1 responsibilities natively without needing an external init process:
use pid1::Pid1Settings; fn main() { let mut settings = Pid1Settings::new(); settings.enable_signal_handling().enable_reaping(); settings.launch(|| { // Your application code here run_my_app(); }).expect("Failed to launch with pid1"); }
Conclusion
A proper init process:
- Forwards signals to your application
- Reaps zombie processes automatically
- Ensures graceful shutdown by sending SIGTERM to children before exiting
- Provides the "grace period" needed for clean application shutdown
Unless your application is specifically designed to handle PID 1 responsibilities, wrap it with a minimal init system. It's a small change that prevents hard-to-debug container behavior and ensures predictable shutdown semantics.
Your future self (and your ops team) will thank you.
Did you like this post?
If your organization needs help with implementing modern DevOps practices, scaling you infrastructure and engineer productivity... I can help! I offer a variety of services.Get in touch!