Why You Need a Proper Init Process on Docker
Posted on by Gentaro "hibariya" Terada
You have to use proper init process when you create a docker image. Running a process on docker without it might lead unexpected results. Today, I'll explain about that.
The “init” Process and Orphans
On a Linux system, the process which has PID 1 is the root of the process tree. It is called “init”. The default signal handlers of the init process is different from other ordinary processes. And the init process has a special responsibility.
Generally, an ordinary process is reaped by its parent process at its termination. But occasionally, a parent process terminates earlier than its children. The children left by the parent are called “orphan”. And the init process, the ancestor of all processes is supposed to adopt those orphan processes and to reap zombie processes.
For example, you can confirm this mechanism by running below code.
#!/usr/bin/env ruby
Process.fork {
File.open('output', 'w') do |f|
f.puts "Parent: #{Process.ppid}" # will write parent's PID
sleep 2 # wait for termination of the parent
f.puts "Parent: #{Process.ppid}" # will write init's PID
end
}
sleep 1
exit # exit w/o waiting (reaping) the child
After you run that code, the output of the child process can be confirmed by viewing the file “output”. The file will show you something like below.
Parent: 20935
Parent: 1
On the second output, the parent PID seems be changed. That means the child process had been adopted by the init process (PID: 1) since the original parent process had gone earlier.
Problems Using Docker w/o a Proper Init Process
The process table in a container is separated from the host side. And as I said above, the init process is supposed to treat its descendants. Therefore, if you don't use a “proper” init process, troubles such as unexpected process termination may happen.
For example, if the pid-1 process which has children is terminating and it doesn't wait its child processes, the children will be killed ungracefully. Children like those can't handle termination properly. It's equivalent to be killed by 9 (SIGKILL).
# The child will able to trap termination (SIGTERM)
docker run -it --rm ruby:alpine ruby -e "
pid = Process.fork {
trap(:TERM) { puts %(Terminating...); exit }
sleep
}
Process.kill(:TERM, pid) # exit the child normally w/ SIGTERM
Process.waitpid(pid)
"
# The child won't able to trap termination (equivalent to SIGKILL)
docker run -it --rm ruby:alpine ruby -e "
pid = Process.fork {
trap(:TERM) { puts %(Terminating...); exit }
sleep
}
exit # just exit immediately
"
Even if those orphans are already dead, they will remain on the process table as zombie processes and won't be reaped. Thus they will keep consuming the kernel resources.
Using a Proper Init on Docker
If you use docker 1.13 or later, you can just pass --init
option to docker run
command. You can also use tini. And there are some more “proper” init programs such as s6-overlay.