Entrypoint offers extensible entrance and init functionality for containers such as Docker and Kubernetes. It allows template-induced configuration, extensible initialization routines and works as a simple process supervisor and init system designed to run as PID 1 inside minimal container environments. Entrypoint is written in python3 and tested extensively.
Key features:
- A simple process supervisor and init system taking care of zombies and signal propagation.
- Configurability via environment variables and a YAML configuration file.
- Jinja2 templates to carry out the actual configurations.
- Ability to extend the initialization with Python-based hooks before and after the configuration step.
Init system strongly inspired by dumb-init
Lightweight containers have popularized the idea of running a single process or service without normal init systems like systemd or sysvinit. However, omitting an init system often leads to incorrect handling of processes and signals, and can result in problems such as containers which can't be gracefully stopped, or leaking containers which should have been destroyed.
entrypoint
enables you to simply prefix your command with entrypoint
. It acts
as PID 1 and immediately spawns your command as a child process, taking care to
properly handle and forward signals as they are received. You can always omit
this functionality with entrypoint --no-init --
, which skips all init system
responsibilities and uses exec to run your actual command after other
initializations.
Normally, when you launch a Docker container, the process you're executing becomes PID 1, giving it the quirks and responsibilities that come with being the init system for the container.
There are two common issues this presents:
-
In most cases, signals won't be handled properly.
The Linux kernel applies special signal handling to processes which run as PID 1.
When processes are sent a signal on a normal Linux system, the kernel will first check for any custom handlers the process has registered for that signal, and otherwise fall back to default behavior (for example, killing the process on
SIGTERM
).However, if the process receiving the signal is PID 1, it gets special treatment by the kernel; if it hasn't registered a handler for the signal, the kernel won't fall back to default behavior, and nothing happens. In other words, if your process doesn't explicitly handle these signals, sending it
SIGTERM
will have no effect at all.A common example is CI jobs that do
docker run my-container script
: sendingSIGTERM
to thedocker run
process will typically kill thedocker run
command, but leave the container running in the background. -
Orphaned zombie processes aren't properly reaped.
A process becomes a zombie when it exits, and remains a zombie until its parent calls some variation of the
wait()
system call on it. It remains in the process table as a "defunct" process. Typically, a parent process will callwait()
immediately and avoid long-living zombies.If a parent exits before its child, the child is "orphaned", and is re-parented under PID 1. The init system is thus responsible for
wait()
-ing on orphaned zombie processes.Of course, most processes won't
wait()
on random processes that happen to become attached to them, so containers often end with dozens of zombies rooted at PID 1.
Unless otherwise specified, entrypoint
runs as PID 1, acting like a simple init system. It launches a
single process and then proxies all received signals to a session rooted at
that child process.
Since your actual process is no longer PID 1, when it receives signals from
entrypoint
, the default signal handlers will be applied, and your process will
behave as you would expect. If your process dies, entrypoint
will also die,
taking care to clean up any other processes that might still remain.
In its default mode, entrypoint
establishes a
session rooted at the
child, and sends signals to the entire process group. This is useful if you
have a poorly-behaving child (such as a shell script) which won't normally
signal its children before dying.
This can actually be useful outside of Docker containers in regular process
supervisors like daemontools or supervisord for
supervising shell scripts. Normally, a signal like SIGTERM
received by a
shell isn't forwarded to subprocesses; instead, only the shell process dies.
With entrypoint
, you can just write shell scripts with entrypoint
in the shebang:
#!/usr/bin/entrypoint /bin/sh
my-web-server & # launch a process in the background
my-other-server # launch another process in the foreground
Ordinarily, a SIGTERM
sent to the shell would kill the shell but leave those
processes running (both the background and foreground!). With entrypoint
, your
subprocesses will receive the same signals your shell does.
If you'd like for signals to only be sent to the direct child, you can run with
the --no-setsid
argument when running entrypoint
. In this mode,
entrypoint
is completely transparent; you can even string multiple
together (like entrypoint --no-setsid -- entrypoint --no-setsid echo 'oh, hi'
).
Entrypoint
allows rewriting incoming signals before proxying them. This is
useful in cases where you have a Docker supervisor (like Mesos or Kubernetes)
which always sends a standard signal (e.g. SIGTERM). Some apps require a
different stop signal in order to do graceful cleanup.
For example, to rewrite the signal SIGTERM to SIGQUIT,
just add --rewrite term:quit
on the command line.
To drop a signal entirely, you can rewrite it to the special name none
.
Note: Rewrites are case-insensitive and they may include the sig
prefix.
When running in setsid mode, it is not sufficient to forward
SIGTSTP
/SIGTTIN
/SIGTTOU
in most cases, since if the process has not added
a custom signal handler for these signals, then the kernel will not apply
default signal handling behavior (which would be suspending the process) since
it is a member of an orphaned process group. For this reason, we set default
rewrites to SIGSTOP
from those three signals. You can opt out of this
behavior by rewriting the signals back to their original values, if desired.
One caveat with this feature: for job control signals (SIGTSTP
, SIGTTIN
,
SIGTTOU
), entrypoint
will always suspend itself after receiving the signal,
even if you rewrite it to something else.
Entrypoint
offers
Often containers want to do some pre-start work which can't be done during
build time. For example, you might want to template out some config files based
on environment variables or a more complex configuration. You may also want to
run some initial scripts to, for instance, setup a database.
By defult entrypoint
searches recursively for Jinja templates from
the /templates
directory. Each found file /templates/<path>
is rendered
with the environment and configuration variables, and the resulting document
placed in /<path>
. If the destination file already exists, rendering of
the corresponding template is skipped. So, make sure to delete those files
you will template on init. In this way, it is easy to override configuration
files from outside of the containers.
All non-existing sub-directories are copied form /templates
including
ownership and mode. The template files also define the ownership and mode
of the resulting files.
Additional Jinja templates that do not represent a real file itself but will
be included from other templates can be plcaed under /jinja
directory
(change the default location with --jinja
).
Any rendering error causes entrypoint
to print an error and stop. All Jinja
variables must be defined, but of course the default
filter is useful for
allowing default values.
In some cases environment variables are enough to perform sibmple container
setups. However, if more complex configuration is needed, the configuartion
can be placed in a YAML configuration, which is then mapped to path
/variables.yml
within the container (change the default path
with --variables
).
YAML configuration makes it possible to offer a simple configuration of containers possibly configuring multiple services (such as: barman and cron; odbc and your service; kerberos and postgresql). This encapsulates all configurations, most probably of differen forms, to offer a signle containerized service.
Hooks are located in Python modules under /entrypoint_hooks
directory
(change with --hooks
). Such module should define at least one of functions
prehook(variables)
, hook(variables)
or posthook(variables)
. Prehooks
are executed before template rendering and can therefore even edit the
configuration variables. Hooks and posthooks are run after template redering.
The only variable for these functions is a dict-like variable space.
Install with pip and git:
pip install git+https://github.com/hlub/entrypoint
ToDo: install from release.
Once installed inside your container, simply prefix your commands with
entrypoint
(and make sure that you're using the recommended JSON
syntax).
Within a Dockerfile, it's a good practice to use entrypoint
as your container's
entrypoint. An "entrypoint" is a partial command that gets prepended to your
CMD
instruction, making it a great fit for entrypoint
:
# Runs "entrypoint -- /my/script --with --args"
# Note the double dash (--) which indicates that `entrypoint` stops handling
# its parameters and leaves the rest unprocessed.
ENTRYPOINT ["entrypoin", "--"]
# or if you use --rewrite or other cli flags
# ENTRYPOINT ["entrypoint", "--rewrite", "term:none", "--"]
CMD ["/my/script", "--with", "--args"]
If you declare an entrypoint in a base image, any images that descend from it
don't need to repeat it. They can just set a CMD
as usual.
For interactive one-off usage, you can just prepend it manually:
$ docker run my_container entrpoint -- python -c 'while True: pass'
Running this same command without entrypoint
would result in being unable to
stop the container without SIGKILL
, but with entrypoint
, you can send it
more humane signals like SIGTERM
.
It's important that you use the JSON syntax for CMD
and
ENTRYPOINT
. Otherwise, Docker invokes a shell to run your command, resulting
in the shell as PID 1 instead of entrypoint
.
To develop entrypoint
further you can clone this repository, create a
virtualenv, install the requiremens and development requirements:
git clone https://github.com/hlub/entrypoint
cd entrypoint
virtualenv --python python3 venv
source venv/bin/activate
pip install -r requirements.txt
pip install -r requirements-dev.txt
Now you are ready to run the entrypoint with python -m entrypoint.main
,
and you can run the tests with command pytest
.
$ make