Running services inside a container using runit + Alpine linux

April 19, 2018

Here's a simple way to run your apps within a docker container using runit.

First you'll need to create a Dockerfile that'll bake in the necessary alpine packages that'll we'll need.

Dockerfile

FROM alpine:3.7
ENV TERM=xterm-256color
COPY ./boot.sh /sbin/boot.sh

RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.7/community" >> /etc/apk/repositories  && \
    apk --update upgrade && \
    apk add  \
    bash \
    runit && \
    rm -rf /var/cache/apk/* && \
    chmod +x /sbin/boot.sh && \
    mkdir /etc/run_once

CMD [ "/sbin/boot.sh" ]

Then you'll need to create the boot.sh file. This is run when the container is booted.

The boot file sets up the correct environment variables, runs the runit service and properly shutdowns the runit services when the container is stopped.

boot.sh

#!/bin/sh

shutdown() {
  echo "shutting down container"

  # first shutdown any service started by runit
  for _srv in $(ls -1 /etc/service); do
    sv force-stop $_srv
  done

  # shutdown runsvdir command
  kill -HUP $RUNSVDIR
  wait $RUNSVDIR

  # give processes time to stop
  sleep 0.5

  # kill any other processes still running in the container
  for _pid  in $(ps -eo pid | grep -v PID  | tr -d ' ' | grep -v '^1$' | head -n -6); do
    timeout -t 5 /bin/sh -c "kill $_pid && wait $_pid || kill -9 $_pid"
  done
  exit
}

# store enviroment variables
export > /etc/envvars

PATH=/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/usr/X11R6/bin

# run all scripts in the run_once folder
/bin/run-parts /etc/run_once

exec env - PATH=$PATH runsvdir -P /etc/service &

RUNSVDIR=$!
echo "Started runsvdir, PID is $RUNSVDIR"
echo "wait for processes to start...."

sleep 5
for _srv in $(ls -1 /etc/service); do
    sv status $_srv
done

# catch shutdown signals
trap shutdown SIGTERM SIGHUP SIGQUIT SIGINT
wait $RUNSVDIR

shutdown

The boot file will look for services in the /etc/service directory. Add your runit services in this folder and they'll be automatically run by runit.

For example, let's create a service that'll just print "hello world" to the console until terminated.

First, we'll create our service file /etc/service/helloworld/run. This file will contain the necessary code to start whatever process we want:

/etc/service/helloworld/run

#!/bin/bash

echo "Started service..."

for i in {1..1000}
do
    echo "Hello world"
    sleep 1
done

exit 1

If you want to run an external process, like uwsgi, you can use a service file like this:

/etc/service/web/run

#!/bin/sh -e

# pipe stderr to stdout and run app via uwsgi
exec 2>&1
exec uwsgi \
    --http :80 \
    --module web:app \
    --enable-threads \
    --processes 2 \
    --threads 2

Best practise to add your services inside the docker image is to inherit from the above Dockerfile and use COPY statements to copy your service files into the docker image

You can find a working base image with runit + alpine setup at https://github.com/sanjeevan/baseimage