Managing Docker Compose with OpenRC

#gentoo #openrc #docker #dockercompose

On one of my machines, I host a couple services using docker-compose. I wanted to start/restart/stop these using the default init/service manager used on the machine, openrc. This would allow them to start/stop automatically with Docker (which coincides with the machine powering on or off, respectively).

I’ve set this up through a single docker-compose meta-service. To add new docker-compose projects to be managed, all I need to do for openrc configuration is creating a symlink, and configure the path to the docker-compose.yaml file.

The meta-service lives at /etc/init.d/docker-compose, just like all other services managed by openrc. This file is quite straightforward. To start off, a number of variables are set and exported.

name="$RC_SVCNAME"
description="OpenRC script for managing the $name docker-compose project"

# Set default values
DOCKER_COMPOSE="${DOCKER_COMPOSE:-docker-compose} $DOCKER_COMPOSE_ARGS"

COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-$name}"

# Export all variables used by docker-compose CLI
export COMPOSE_PROJECT_NAME
export COMPOSE_FILE
export COMPOSE_PROFILES
export COMPOSE_API_VERSION
export DOCKER_HOST
export DOCKER_TLS_VERIFY
export DOCKER_CERT_PATH
export COMPOSE_HTTP_TIMEOUT
export COMPOSE_TLS_VERSION
export COMPOSE_CONVERT_WINDOWS_PATHS
export COMPOSE_PATH_SEPARATOR
export COMPOSE_FORCE_WINDOWS_HOST
export COMPOSE_IGNORE_ORPHANS
export COMPOSE_PARALLEL_LIMIT
export COMPOSE_INTERACTIVE_NO_CLI
export COMPOSE_DOCKER_CLI_BUILD

One of the services I use is also configured with its own external network. I want it to be created if it doesn’t exist, to ensure that the service can start up properly. I do not want it to be removed, so I left that out.

# Set up (external) networks
for name in "${DOCKER_NETWORKS[@]}"
do
        # Create the network if needed
        if ! docker network ls | awk '{ print $2 }' | grep -q "$name"
        then
                einfo "Creating docker network '$name'"
                docker network create "$name" > /dev/null
        fi

        # Expose some variables for the networks
        network_id="DOCKER_NETWORK_${name}_ID"

        declare -gx DOCKER_NETWORK_${name}_ID="$(docker network ls | awk '$2 == "'"$name"'" { print $1 }')"
        declare -gx DOCKER_NETWORK_${name}_GATEWAY="$(docker network inspect "${!network_id}" | jq -r '.[0].IPAM.Config[0].Gateway')"

        unset network_id
done

And lastly, there’s the four simple functions to declare dependencies, configure how to start or stop, and how to get the status of the service.

depend() {
        need docker
}

start() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" up -d
}

status() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" ps
}

stop() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" down
}

Now, to actually create a service file to manage a docker-compose project, a symlink must be made. I’ll take my botamusique service as an example.

ln -s /etc/init.d/docker-compose /etc/init.d/botamusique

This service can’t start just yet, as there’s no $COMPOSE_PROJECT_DIRECTORY configured for it yet. For this, a similarly named file should be made in /etc/conf.d. In here, any variable used by the service can be configured.

$EDITOR /etc/conf.d/botamusique

In my case, it only pertains the $COMPOSE_PROJECT_DIRECTORY variable.

COMPOSE_PROJECT_DIRECTORY="/var/docker-compose/botamusique"

And that’s it. For additional docker-compose projects I need to make only a symlink and a configuration file. If I discover a bug or nuisance, only a single file needs to be altered to get the benefit on all the docker-compose services.

For reference, here’s the full /etc/init.d/docker-compose file.

#!/sbin/openrc-run
# Copyright 2021 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

name="$RC_SVCNAME"
description="OpenRC script for managing the $name docker-compose project"

# Set default values
DOCKER_COMPOSE="${DOCKER_COMPOSE:-docker-compose} $DOCKER_COMPOSE_ARGS"

COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-$name}"

# Export all variables used by docker-compose CLI
export COMPOSE_PROJECT_NAME
export COMPOSE_FILE
export COMPOSE_PROFILES
export COMPOSE_API_VERSION
export DOCKER_HOST
export DOCKER_TLS_VERIFY
export DOCKER_CERT_PATH
export COMPOSE_HTTP_TIMEOUT
export COMPOSE_TLS_VERSION
export COMPOSE_CONVERT_WINDOWS_PATHS
export COMPOSE_PATH_SEPARATOR
export COMPOSE_FORCE_WINDOWS_HOST
export COMPOSE_IGNORE_ORPHANS
export COMPOSE_PARALLEL_LIMIT
export COMPOSE_INTERACTIVE_NO_CLI
export COMPOSE_DOCKER_CLI_BUILD

# Set up (external) networks
for name in "${DOCKER_NETWORKS[@]}"
do
        # Create the network if needed
        if ! docker network ls | awk '{ print $2 }' | grep -q "$name"
        then
                einfo "Creating docker network '$name'"
                docker network create "$name" > /dev/null
        fi

        # Expose some variables for the networks
        network_id="DOCKER_NETWORK_${name}_ID"

        declare -gx DOCKER_NETWORK_${name}_ID="$(docker network ls | awk '$2 == "'"$name"'" { print $1 }')"
        declare -gx DOCKER_NETWORK_${name}_GATEWAY="$(docker network inspect "${!network_id}" | jq -r '.[0].IPAM.Config[0].Gateway')"

        unset network_id
done

depend() {
        need docker
}

start() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" up -d
}

status() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" ps
}

stop() {
        $DOCKER_COMPOSE --project-directory "$COMPOSE_PROJECT_DIRECTORY" down
}