Upgrade all your systemd containers at once with this small script

If you are operating a server, chances are that you use containers to provide an additional layer of isolation for the services running on this server. One method for setting up and running containers is using the systemd built-in tools, namely systemd-nspawn (click here for my previous guide on how to set up containers).

Keeping your system up to date is always crucial when operating a server. When using containers, you need to make sure that every container is kept up to date, since each of them occupies their own portion of the filesystem.

One option to achieve this is to set up unattended upgrades in each container. However, in case you don’t want to that, I wrote a simple bash script to manually upgrade all of your containers, including the host system, with one command. It expects APT as package manager, so it works out-of-the-box with many distributions, e.g. Debian and its derivatives (Ubuntu/Raspbian/…). However, this can also be changed easily in the script.

Below is the full script:

#!/bin/bash

# This script is intended to comfortably update packages
# on a machine and all systemd containers which run on
# this machine.
# It was tested with systemd version 232-19.

# The command to be run for updates
CMD='apt-get update && '
CMD+='apt-get dist-upgrade --auto-remove --purge && '
CMD+='apt-get clean'

# Preparations
ME=$(basename $0)
RED='\e[0;31m'
GREEN='\e[0;32m'
YELLOW='\e[0;33m'
BLUE='\e[0;34m'
ENDCOLOR='\e[0m'

# If help was requested, echo help and exit
if [ $1 ] && ([ $1 = 'help' ] || [ $1 = '--help' ]); then
    echo -e "Usage: ${YELLOW}${ME} [<command> [<args>]]${ENDCOLOR}"
    echo ""
    echo -e "  default (no command)   - Update host and all running guests"
    echo -e "  ${YELLOW}host${ENDCOLOR}                   - Update only the host"
    echo -e "  ${YELLOW}machine <name>${ENDCOLOR}         - Update only the specified guest"
    echo -e "  ${YELLOW}help${ENDCOLOR}                   - Display this help and exit"
    exit 0
fi

# Make sure only root runs this script
if [ $(id -u) -ne 0 ]; then
    echo -e "${RED}This script must be run as root.${ENDCOLOR}" 1>&2
    exit 1
fi

# Check if the given command is valid
if [ $1 ] && [ $1 != 'host' ] && [ $1 != 'machine' ]; then
    echo -e "${RED}Invalid command: $1${ENDCOLOR}"
    exit 1
fi

# Update this machine (the host) if requested
if [ -z $1 ] || [ $1 = 'host' ]; then
    echo -e "${GREEN}[${ME}] Updating host system ...${ENDCOLOR}"

    eval "${CMD}"

    RET=$?
    if [ ${RET} -gt 0 ]; then
        echo -e "${RED}[${ME}] An error occurred during host system update!${ENDCOLOR}"
        echo -e "${RED}[${ME}] Return code: ${RET}${ENDCOLOR}"
    fi
fi

# If only a host update was requested, exit now
if [ $1 ] && [ $1 = 'host' ]; then
    exit 0
fi

# Use 'machinectl list' to obtain a list of containers which are
# currently running on this machine. Note that this command only
# lists active machines (which is what we want). However, the
# output contains more information than just machine name; thus
# it needs to be formatted a bit.
LIST=$(machinectl list | {
    # Skip header line
    read LINE
    # Iterate over machines
    while read LINE; do
        # Check for empty line (indicates the end of the machine
        # list), used to cut off following lines
        if [ -z "${LINE}" ]; then
            break
        fi

        # Get first word from line (which is the machine name)
        echo "${LINE}" | awk '{print $1;}'
    done
})

# If a specific machine was given, check if it exists and remove
# all other machines from the list
if [ $1 ] && [ $1 = 'machine' ]; then
    if [ -z $2 ]; then
        echo -e "${RED}You must specify a machine name when using the 'machine' command.${ENDCOLOR}"
        exit 1
    fi

    EXISTS=0
    for MACHINE in ${LIST}; do
        if [ $2 = ${MACHINE} ]; then
            EXISTS=1
            LIST="${MACHINE}"
        fi
    done

    if [ ${EXISTS} -eq 0 ]; then
        echo -e "${RED}Machine '$2' does not exist or is offline.${ENDCOLOR}"
        exit 1
    fi
fi

# Iterate over the machines
for MACHINE in ${LIST}; do
    echo -e "${GREEN}[${ME}] Updating machine '${MACHINE}' ...${ENDCOLOR}"

    # Upgrade the machine
    machinectl shell ${MACHINE} /bin/bash -c "${CMD}
    R=\$?
    if [ \$R -gt 0 ]; then
        echo -e \"${RED}[${ME}] An error occurred during update of machine '${MACHINE}'!${ENDCOLOR}\"
        echo -e \"${RED}[${ME}] Return code: \$R${ENDCOLOR}\"
    fi" # Using one-letter variable here because the ${...} syntax doesn't work in container somehow
done

You can use the --help argument to see available command line options. The script supports updating everything or just a specific machine (note: container and machine are used synonymously here, as in systemd). It obviously requires root privileges.

The command used to trigger the package upgrades is

apt-get update && apt-get dist-upgrade --auto-remove --purge && apt-get clean

It is defined on lines 9 through 11. If you use a different package manager, or want to use a less thorough upgrade command, you can change it there.

The script parses the output of machinectl list to obtain a list of all running containers on the system. Powered-off containers are not touched.

Feel free to use the script as you like, but note that it comes with absolutely no warranty and that you use it at your own risk.

For any feedback or improvement suggestions, please leave a comment below!

Leave a Reply

Your email address will not be published. Required fields are marked *