Running cron inside a docker container


    It just so happened that running cron in a Docker container is a very specific, if not complicated, thing. The network is full of decisions and ideas on this topic. Here is one of the most popular (and simple) ways to run:
    cron -f
    

    But such a solution (and most others, too) has a number of disadvantages that are difficult to get around right away:
    • inconvenience of viewing logs ( docker logs command does not work)
    • cron uses its own Environment (environment variables passed at container start are not visible for cron jobs)
    • it is impossible to normally (gracefully) stop the container with the docker stop command (in the end, SIGKILL arrives at the container)
    • container stops with non-zero error code


    Logs


    The problem of viewing logs using standard Docker tools is relatively easy to resolve. To do this, it is enough to make a decision about in which file their cron job logs will be written. Suppose this is /var/log/cron.log: Starting the container after this with the command:
    * * * * * www-data task.sh >> /var/log/cron.log 2>&1


    cron && tail -f /var/log/cron.log
    

    we can always see the results of completing tasks with the help of docker logs.

    A similar effect can be achieved by redirecting /var/log/cron.log to the standard output of the container:
    ln -sf /dev/stdout /var/log/cron.log
    

    UPD : This method will not work for this reason.

    If cron tasks write logs to different files, then, most likely, the option using tail, which can "monitor" several logs at the same time, will be preferable:
    cron && tail -f /var/log/task1.log /var/log/task2.log
    

    UPD : It is more convenient to create the file (s) for the log in the form of a named pipe (FIFO). This will avoid the accumulation of unnecessary information inside the container, and assign the log rotate tasks to the Docker . Example:
    mkfifo --mode 0666 /var/log/cron.log
    


    Environment variables


    Studying information on the topic of assigning environment variables for cron tasks, I found out that the latter can use the so-called authentication plug-ins (PAM) . What at first glance, is not related to a subject theme of fact. But PAM has the ability to define and redefine any environment variables for services that use it (or rather, authentication modules), including for cron. All configuration is done in the /etc/security/pam_env.conf file (in the case of Debian / Ubuntu). That is, any variable described in this file automatically enters the Environment of all cron jobs.

    But there is one problem, or rather two. The syntax of the file (its description) at first glance can enter into a stupordiscourage. The second problem is how, when starting the container, transfer environment variables inside pam_env.conf.

    Experienced Docker users about the second problem will probably say right away that you can use the life hack called docker-entrypoint.sh and they will be right. The essence of this life hack is to write a special script that runs when the container starts, and is the entry point for the parameters listed in the CMD or passed on the command line. The script can be written inside the Dockerfile, for example, like this:
    ENTRYPOINT ["/docker-entrypoint.sh"]
    

    And his code must be written in a special way:
    docker-entrypoint.sh
    #!/usr/bin/env bash
    set -e
    # код переноса переменных окружения в /etc/security/pam_env.conf
    exec "$@"
    


    Let us return to the transfer of environment variables a little later, but for now let us dwell on the syntax of the pam_env.conf file. When describing any variable in this file, the value can be specified using two directives: DEFAULT and OVERRIDE. The first allows you to specify the default value of the variable (if it is not defined at all in the current environment), and the second allows you to override the value of the variable (if the value of this variable is in the current environment). In addition to these two cases, the file as an example describes more complex cases, but by and large we are only interested in DEFAULT. In total, to determine the value for some environment variable, which will then be used in cron, you can use this example:
    VAR DEFAULT="value"
    

    Note that value in this case should not contain variable names (for example, $ VAR), because the file context is executed inside the target Environment, where the specified variables are absent (or have a different value).

    But you can do even easier (and for some reason this method is not described in the examples pam_env.conf). If you are satisfied that the variable in the target Environment will have the specified value, regardless of whether it is already defined in this environment or not, then instead of the above line, you can write simply:
    VAR="value"
    

    Here it should be warned that you cannot replace $ PWD, $ USER and $ PATH for cron jobs if you wish, because cron assigns the values ​​of these variables based on its own beliefs. You can, of course, use various hacks , among which there are workers, but this is at your discretion.

    And finally, if you need to transfer all current variables to the environment of cron jobs, then in this case you can use the following script:
    docker-entrypoint.sh
    #!/usr/bin/env bash
    set -e
    # переносим значения переменных из текущего окружения
    env | while read -r LINE; do  # читаем результат команды 'env' построчно
        # делим строку на две части, используя в качестве разделителя "=" (см. IFS)
        IFS="=" read VAR VAL <<< ${LINE}
        # удаляем все предыдущие упоминания о переменной, игнорируя код возврата
        sed --in-place "/^${VAR}/d" /etc/security/pam_env.conf || true
        # добавляем определение новой переменной в конец файла
        echo "${VAR} DEFAULT=\"${VAL}\"" >> /etc/security/pam_env.conf
    done
    exec "$@"
    


    By placing the print_env script in the /etc/cron.d folder inside the image and running the container (see Dockerfile), we can verify that this solution works:
    print_env
    * * * * * www-data env >> /var/log/cron.log 2>&1
    


    Dockerfile
    FROM debian:jessie
    RUN apt-get clean && apt-get update && apt-get install -y cron
    RUN rm -rf /var/lib/apt/lists/*
    RUN mkfifo --mode 0666 /var/log/cron.log
    COPY docker-entrypoint.sh /
    COPY print_env /etc/cron.d
    ENTRYPOINT ["/docker-entrypoint.sh"]
    CMD ["/bin/bash", "-c", "cron && tail -f /var/log/cron.log"]
    


    container launch
    docker build --tag cron_test .
    docker run --detach --name cron --env "CUSTOM_ENV=custom_value" cron_test
    docker logs -f cron  # нужно подождать минуту
    



    Graceful shutdown


    Speaking about the reason for the impossibility of normal completion of the described container with cron, we should mention the way the Docker daemon communicates with the service running inside it. Any such service (process) starts with PID = 1, and only with this PID Docker can work. That is, every time Docker sends a control signal to the container, it addresses it to the process with PID = 1. In the case of docker stop, this is SIGTERM and, if the process continues, after 10 seconds SIGKILL. Since "/ bin / bash -c" is used to start (in the case of "CMD cron && tail -f /var/log/cron.log" Docker still uses "/ bin / bash -c", just implicitly), then PID = 1 gets the process / bin / bash, and cron and tail already get other PIDs, which are impossible to predict for obvious reasons.

    So it turns out that when we execute the docker stop cron command, SIGTERM receives the process / bin / bash -c, and in this mode it ignores any received signal (except for SIGKILL, of course).

    The first thought in this case is usually - you need to somehow “tail” the tail process. Well, this is easy enough to do:
    docker exec cron killall -HUP tail
    

    Cool, the container immediately stops working. True about graceful, there are some doubts. And the error code is still nonzero. In general, I could not advance in solving the problem by following this path.

    By the way, starting the container using the cron -f command also does not give the desired result, cron in this case simply refuses to respond to any signals.

    True graceful shutdown with zero exit code


    There is only one thing left - to write a separate script to start the cron daemon, while being able to correctly respond to control signals. It is relatively easy, even if you didn’t have to write on bash before, you can find information that it has the ability to program signal processing (using the trap command ). Here's how, for example, a script like this might look like:
    start-cron
    #!/usr/bin/env bash
    # запускаем cron
    service cron start
    # ловим SIGINT или SIGTERM и выходим
    trap "service cron stop; exit" SIGINT SIGTERM
    


    if we could somehow make this script work endlessly (until a signal is received). And here another life hack comes to the rescue, peeped here , namely, adding such a line to the end of our script:
    tail -f /var/log/cron.log & wait $!
    

    Or, if cron jobs write logs to different files:
    tail -f /var/log/task1.log /var/log/task2.log & wait $!
    


    Conclusion


    The result was an effective solution for running cron inside a Docker container, bypassing the limitations of the first and observing the rules of the second, with the possibility of a normal stop and restart of the container.

    At the end I give a link where everything described in the article is designed as a separate Docker image: renskiy / cron .

    Also popular now: