Search code examples
dockergitlab-cisystemd

Entrypoint of systemd container for Gitlab CI


I'm building a docker image for running Gitlab CI jobs. One of the components needs systemd up and running inside the container, this is not trivial but there are several guides on the web so I managed to do it. Part of the process requires to define this entrypoint in the Dockerfile:

ENTRYPOINT ["/usr/sbin/init"]

so that systemd runs as PID 1 in the container, as needed. This seems to conflict with Gitlab CI requirements: as far as I understand gitlab-runner overrides the Dockerfile's CMD to spawn a shell which then executes the CI script. But the /usr/sbin/init entrypoint cannot understand the Gitlab's CMD so the shell is not spawned and the execution halts.

I cannot figure out how to solve this:

  • executing an entrypoint script which starts /usr/sbin/init and then a shell won't work because systemd won't be PID1;
  • using a shell as ENTRYPOINT and then systemd as CMD won't work since Gitlab CI overrides CMD.

I cannot think of any other possible solution, so any help is much appreciated.


Solution

  • Finally I have been able to put together a working solution. The ENTRYPOINT executes a script:

    ENTRYPOINT ["/entrypoint.sh"]
    

    which retrieves the stdin/stdout fds of PID 1 (i.e. those that will be used by Gitlab CI for the job I/O), pins them by attaching them to a long-running process and then spawns systemd as PID 1:

    #!/bin/bash
    
    # Start a long-running process to keep the container pipes open
    sleep infinity < /proc/1/fd/0 > /proc/1/fd/1 2>&1 &
    # Wait a bit before retrieving the PID
    sleep 1
    # Save the long-running PID on file
    echo $! > /container-pipes-pid
    # Start systemd as PID 1
    exec /usr/lib/systemd/systemd
    

    The pinning of stdin/stdout is needed since some systemd versions close stdin/stdout at startup, but they are needed since the CI infrastructure uses these to send CI commands to the shell and receive console output. So attaching them to sleep infinity makes them persist even after exec /usr/lib/systemd/systemd.

    The shell is then spawned by a systemd unit (previously enabled in the Dockerfile):

    [Unit]
    Description=Start bash shell attached to container STDIN/STDOUT
    
    [Service]
    Type=simple
    PassEnvironment=PATH LD_LIBRARY_PATH
    ExecStart=/bin/bash -c "echo Attaching to pipes of PID `cat container-pipes-pid` && exec /bin/bash < /proc/`cat container-pipes-pid`/fd/0 > /proc/`cat container-pipes-pid`/fd/1 2>/proc/`cat container-pipes-pid`/fd/2"
    ExecStopPost=/usr/bin/systemctl exit $EXIT_STATUS
    
    [Install]
    WantedBy=multi-user.target rescue.target
    

    The pinned fds are attached to the shell (no conflict is generated by this since neither systemd nor sleep do I/O on those fds), so the shell correctly receives CI commands and directs console output to CI logs. Then at the end of the job, when stdin is closed by the CI infrastructure, the shell terminates and the unit shuts down the container, returning the job execution result so that the CI correctly retrieves the job outcome.

    This is a rather involved implementation which can for sure be refined and improved, but is working beautifully for my purpose.