Last active
March 9, 2026 08:23
-
-
Save seia-soto/f38965b3977e32110c5a7b147a16d30e to your computer and use it in GitHub Desktop.
Podman compose init rc-script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| set -u | |
| PATH=/usr/sbin:/usr/bin:/sbin:/bin | |
| export LC_ALL=C | |
| umask 077 | |
| SCRIPT_NAME=${0##*/} | |
| INTERVAL=${INTERVAL:-60} | |
| PARALLEL_JOBS=${PARALLEL_JOBS:-auto} | |
| STATEFILE=${STATEFILE:-${TMPDIR:-/tmp}/healthcheck.state} | |
| is_positive_integer() { | |
| case ${1:-} in | |
| ''|*[!0-9]*|0) | |
| return 1 | |
| ;; | |
| *) | |
| return 0 | |
| ;; | |
| esac | |
| } | |
| is_executable() { | |
| command -v "$1" >/dev/null 2>&1 | |
| } | |
| is_xargs_parallel() { | |
| printf '%s\n' probe | xargs -n 1 -P 1 printf '%s\n' >/dev/null 2>&1 | |
| } | |
| is_list_contains() { | |
| local needle=$1 list=$2 item= | |
| for item in $list | |
| do | |
| if [ "$item" = "$needle" ]; then | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| is_container_running() { | |
| [ "$(podman inspect --format '{{.State.Running}}' "$1" 2>/dev/null)" = "true" ] | |
| } | |
| get_parallel_jobs() { | |
| local jobs=$PARALLEL_JOBS | |
| case $jobs in | |
| ''|auto) | |
| jobs=$(nproc 2>/dev/null || printf '%s\n' 1) | |
| ;; | |
| esac | |
| if ! is_positive_integer "$jobs"; then | |
| jobs=1 | |
| fi | |
| printf '%s\n' "$jobs" | |
| } | |
| get_running_container_ids() { | |
| podman ps -q | |
| } | |
| get_line_count() { | |
| wc -l | tr -d '[:space:]' | |
| } | |
| get_state_dir() { | |
| dirname "$STATEFILE" | |
| } | |
| get_last_checked_at() { | |
| local cid=$1 id= ts= | |
| if [ ! -r "$STATEFILE" ]; then | |
| printf '%s\n' 0 | |
| return 0 | |
| fi | |
| while IFS=' ' read -r id ts | |
| do | |
| [ -n "$id" ] || continue | |
| [ "$id" = "$cid" ] || continue | |
| if is_positive_integer "${ts:-0}"; then | |
| printf '%s\n' "$ts" | |
| else | |
| printf '%s\n' 0 | |
| fi | |
| return 0 | |
| done < "$STATEFILE" | |
| printf '%s\n' 0 | |
| } | |
| get_container_interval() { | |
| local cid=$1 interval_ns= interval= | |
| interval_ns=$(podman inspect --format '{{if .Config.Healthcheck}}{{printf "%d" .Config.Healthcheck.Interval}}{{end}}' "$cid" 2>/dev/null) || return 1 | |
| if [ -z "$interval_ns" ]; then | |
| return 2 | |
| fi | |
| case $interval_ns in | |
| *[!0-9]*) | |
| return 1 | |
| ;; | |
| esac | |
| if [ "$interval_ns" -le 0 ]; then | |
| interval=$INTERVAL | |
| else | |
| interval=$(( (interval_ns + 999999999) / 1000000000 )) | |
| fi | |
| if ! is_positive_integer "$interval"; then | |
| interval=$INTERVAL | |
| fi | |
| printf '%s\n' "$interval" | |
| } | |
| get_due_container_ids() { | |
| local now=$1 container_ids=$2 cid= interval= last_checked= remaining= rc= | |
| for cid in $container_ids | |
| do | |
| interval=$(get_container_interval "$cid") | |
| rc=$? | |
| case $rc in | |
| 0) | |
| ;; | |
| 2) | |
| use_log INFO "container $cid skipped: no healthcheck defined" | |
| continue | |
| ;; | |
| *) | |
| use_log WARN "unable to inspect container $cid healthcheck interval" | |
| continue | |
| ;; | |
| esac | |
| last_checked=$(get_last_checked_at "$cid") | |
| if ! is_positive_integer "$last_checked"; then | |
| last_checked=0 | |
| fi | |
| if [ "$last_checked" -gt "$now" ]; then | |
| last_checked=0 | |
| fi | |
| if [ $((now - last_checked)) -ge "$interval" ]; then | |
| printf '%s\n' "$cid" | |
| else | |
| remaining=$((interval - (now - last_checked))) | |
| use_log INFO "container $cid skipped: interval not due (${remaining}s remaining)" | |
| fi | |
| done | |
| } | |
| get_health_state() { | |
| local cid=$1 state= | |
| state=$(podman inspect --format '{{if .State.Health}}{{.State.Health.Status}} {{.State.Health.FailingStreak}}{{else if .State.Healthcheck}}{{.State.Healthcheck.Status}} {{.State.Healthcheck.FailingStreak}}{{end}}' "$cid" 2>/dev/null) || return 1 | |
| printf '%s\n' "$state" | |
| } | |
| set_statefile_ready() { | |
| local dir=$(get_state_dir) | |
| if [ ! -d "$dir" ] && ! install -d -m 0700 "$dir"; then | |
| use_log ERROR "failed to create statefile directory: $dir" | |
| return 1 | |
| fi | |
| if [ ! -e "$STATEFILE" ] && ! install -m 0600 /dev/null "$STATEFILE"; then | |
| use_log ERROR "failed to create statefile: $STATEFILE" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| set_statefile_entries() { | |
| local checked_at=$1 running_ids=$2 checked_ids=$3 dir= tmp_state= id= ts= | |
| dir=$(get_state_dir) | |
| if [ ! -d "$dir" ] && ! install -d -m 0700 "$dir"; then | |
| use_log ERROR "failed to create statefile directory: $dir" | |
| return 1 | |
| fi | |
| tmp_state="${STATEFILE}.$$" | |
| if ! : > "$tmp_state"; then | |
| use_log ERROR "failed to write temporary statefile: $tmp_state" | |
| return 1 | |
| fi | |
| if [ -r "$STATEFILE" ]; then | |
| while IFS=' ' read -r id ts | |
| do | |
| [ -n "$id" ] || continue | |
| is_list_contains "$id" "$running_ids" || continue | |
| is_list_contains "$id" "$checked_ids" && continue | |
| if ! is_positive_integer "${ts:-0}"; then | |
| ts=0 | |
| fi | |
| if ! printf '%s %s\n' "$id" "$ts" >> "$tmp_state"; then | |
| rm -f "$tmp_state" | |
| use_log ERROR "failed to update temporary statefile: $tmp_state" | |
| return 1 | |
| fi | |
| done < "$STATEFILE" | |
| fi | |
| for id in $checked_ids | |
| do | |
| is_list_contains "$id" "$running_ids" || continue | |
| if ! printf '%s %s\n' "$id" "$checked_at" >> "$tmp_state"; then | |
| rm -f "$tmp_state" | |
| use_log ERROR "failed to update temporary statefile: $tmp_state" | |
| return 1 | |
| fi | |
| done | |
| if ! mv "$tmp_state" "$STATEFILE"; then | |
| rm -f "$tmp_state" | |
| use_log ERROR "failed to replace statefile: $STATEFILE" | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| use_log() { | |
| local level=$1 | |
| shift | |
| local ts=$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null || date) | |
| printf '%s [%s] %s: %s\n' "$ts" "$SCRIPT_NAME" "$level" "$*" >&2 | |
| } | |
| use_usage() { | |
| cat <<EOF | |
| Usage: | |
| ${SCRIPT_NAME} help | |
| ${SCRIPT_NAME} check | |
| ${SCRIPT_NAME} daemon | |
| Notes: | |
| - Execute this script directly, not as "sh ${SCRIPT_NAME}". | |
| - For Alpine, install: | |
| apk add findutils coreutils | |
| - Configure it with environment variables. | |
| Example: | |
| INTERVAL=10 PARALLEL_JOBS=4 STATEFILE=/run/healthcheck.state ./${SCRIPT_NAME} daemon | |
| - The script polls every INTERVAL seconds, tracks last checks in STATEFILE, | |
| and only runs a container when its configured health interval is due. | |
| - Containers that stop before execution are skipped. | |
| Config variables: | |
| INTERVAL=60 | |
| PARALLEL_JOBS=auto | |
| STATEFILE=/tmp/healthcheck.state | |
| EOF | |
| } | |
| use_config_check() { | |
| local jobs= | |
| if ! is_executable podman; then | |
| use_log ERROR "podman executable not found in PATH" | |
| return 1 | |
| fi | |
| if ! is_executable xargs; then | |
| use_log ERROR "xargs executable not found in PATH" | |
| return 1 | |
| fi | |
| if ! is_executable nproc; then | |
| use_log ERROR "nproc executable not found in PATH" | |
| return 1 | |
| fi | |
| if ! is_xargs_parallel; then | |
| use_log ERROR "xargs does not support -P; install Alpine findutils" | |
| return 1 | |
| fi | |
| if ! is_positive_integer "$INTERVAL"; then | |
| use_log ERROR "INTERVAL must be a positive integer" | |
| return 1 | |
| fi | |
| jobs=$(get_parallel_jobs) | |
| if ! is_positive_integer "$jobs"; then | |
| use_log ERROR "PARALLEL_JOBS resolved to an invalid value" | |
| return 1 | |
| fi | |
| if ! podman ps -q >/dev/null 2>&1; then | |
| use_log ERROR "podman is not accessible for this user/context" | |
| return 1 | |
| fi | |
| if ! set_statefile_ready; then | |
| return 1 | |
| fi | |
| use_log INFO "configuration looks good (jobs=$jobs interval=${INTERVAL}s statefile=$STATEFILE)" | |
| return 0 | |
| } | |
| use_worker() { | |
| local container_id=$1 healthcheck_output= healthcheck_rc= health_state= health_status= failing_streak= | |
| if ! is_container_running "$container_id"; then | |
| use_log INFO "container $container_id skipped: no longer running" | |
| return 0 | |
| fi | |
| healthcheck_output=$(podman healthcheck run "$container_id" 2>&1) | |
| healthcheck_rc=$? | |
| case $healthcheck_rc in | |
| 0|1) | |
| health_state=$(get_health_state "$container_id") | |
| if [ $? -ne 0 ]; then | |
| if [ -n "$healthcheck_output" ]; then | |
| use_log ERROR "container $container_id health state unavailable: $healthcheck_output" | |
| else | |
| use_log ERROR "container $container_id health state unavailable" | |
| fi | |
| return 1 | |
| fi | |
| health_status=${health_state%% *} | |
| failing_streak=${health_state#* } | |
| if [ "$failing_streak" = "$health_state" ]; then | |
| failing_streak=0 | |
| fi | |
| case $health_status in | |
| healthy) | |
| use_log INFO "container $container_id healthy" | |
| return 0 | |
| ;; | |
| starting) | |
| use_log INFO "container $container_id starting (failing_streak=$failing_streak)" | |
| return 0 | |
| ;; | |
| unhealthy) | |
| if [ -n "$healthcheck_output" ]; then | |
| use_log WARN "container $container_id unhealthy (failing_streak=$failing_streak): $healthcheck_output" | |
| else | |
| use_log WARN "container $container_id unhealthy (failing_streak=$failing_streak)" | |
| fi | |
| return 1 | |
| ;; | |
| '') | |
| if [ -n "$healthcheck_output" ]; then | |
| use_log INFO "container $container_id healthcheck executed: $healthcheck_output" | |
| else | |
| use_log INFO "container $container_id healthcheck executed" | |
| fi | |
| return 0 | |
| ;; | |
| *) | |
| use_log WARN "container $container_id health state is $health_status (failing_streak=$failing_streak)" | |
| return 1 | |
| ;; | |
| esac | |
| ;; | |
| 125) | |
| case $healthcheck_output in | |
| *"no defined healthcheck"*|*"no healthcheck"*) | |
| use_log INFO "container $container_id skipped: no healthcheck defined" | |
| return 0 | |
| ;; | |
| *"not running"*|*"unable to find"*|*"no such container"*) | |
| use_log INFO "container $container_id skipped: no longer running" | |
| return 0 | |
| ;; | |
| *) | |
| if [ -n "$healthcheck_output" ]; then | |
| use_log ERROR "container $container_id error: $healthcheck_output" | |
| else | |
| use_log ERROR "container $container_id error (podman returned 125)" | |
| fi | |
| return 1 | |
| ;; | |
| esac | |
| ;; | |
| *) | |
| if [ -n "$healthcheck_output" ]; then | |
| use_log ERROR "container $container_id unexpected exit $healthcheck_rc: $healthcheck_output" | |
| else | |
| use_log ERROR "container $container_id unexpected exit $healthcheck_rc" | |
| fi | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| use_check_once() { | |
| local jobs=$(get_parallel_jobs) now= container_ids= rc= total_count= due_ids= due_count= xargs_rc= checked_at= running_ids= | |
| now=$(date +%s 2>/dev/null || printf '%s\n' 0) | |
| if ! is_positive_integer "$now"; then | |
| now=0 | |
| fi | |
| container_ids=$(get_running_container_ids) | |
| rc=$? | |
| if [ "$rc" -ne 0 ]; then | |
| use_log ERROR "failed to list running containers" | |
| return 1 | |
| fi | |
| if [ -z "$container_ids" ]; then | |
| set_statefile_entries "$now" "" "" || return 1 | |
| use_log INFO "no running containers found" | |
| return 0 | |
| fi | |
| total_count=$(printf '%s\n' "$container_ids" | get_line_count) | |
| due_ids=$(get_due_container_ids "$now" "$container_ids") | |
| if [ -z "$due_ids" ]; then | |
| set_statefile_entries "$now" "$container_ids" "" || return 1 | |
| use_log INFO "no container healthchecks due (${total_count} running)" | |
| return 0 | |
| fi | |
| due_count=$(printf '%s\n' "$due_ids" | get_line_count) | |
| use_log INFO "running healthchecks for ${due_count} due container(s) out of ${total_count} running with ${jobs} parallel worker(s)" | |
| printf '%s\n' "$due_ids" | xargs -n 1 -P "$jobs" "$0" use_worker | |
| xargs_rc=$? | |
| checked_at=$(date +%s 2>/dev/null || printf '%s\n' "$now") | |
| if ! is_positive_integer "$checked_at"; then | |
| checked_at=$now | |
| fi | |
| running_ids=$(get_running_container_ids) | |
| rc=$? | |
| if [ "$rc" -ne 0 ]; then | |
| use_log ERROR "failed to refresh running containers" | |
| return 1 | |
| fi | |
| set_statefile_entries "$checked_at" "$running_ids" "$due_ids" || return 1 | |
| if [ "$xargs_rc" -ne 0 ]; then | |
| case $xargs_rc in | |
| 123) | |
| use_log ERROR "one or more container healthchecks failed" | |
| ;; | |
| *) | |
| use_log ERROR "healthcheck sweep failed (xargs=$xargs_rc)" | |
| ;; | |
| esac | |
| return 1 | |
| fi | |
| use_log INFO "healthcheck sweep complete for ${due_count} container(s)" | |
| return 0 | |
| } | |
| use_daemon_loop() { | |
| use_log INFO "starting podman healthcheck loop (interval=${INTERVAL}s statefile=$STATEFILE)" | |
| while : | |
| do | |
| if ! use_check_once; then | |
| use_log WARN "healthcheck sweep reported failures" | |
| fi | |
| sleep "$INTERVAL" || return 1 | |
| done | |
| } | |
| entrypoint() { | |
| local command_name=${1:-check} | |
| case $command_name in | |
| check) | |
| use_config_check || exit 1 | |
| use_check_once | |
| ;; | |
| daemon) | |
| use_config_check || exit 1 | |
| use_daemon_loop | |
| ;; | |
| use_worker) | |
| shift | |
| if [ $# -ne 1 ]; then | |
| use_log ERROR "use_worker requires exactly one container ID" | |
| exit 2 | |
| fi | |
| use_worker "$1" | |
| ;; | |
| help|-h|--help) | |
| use_usage | |
| ;; | |
| *) | |
| use_usage >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| entrypoint "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/sbin/openrc-run | |
| command_user="<CHANGE_ME_TO_USER>" | |
| name="zz<CHANGE_ME_TO_PROJECT_NAME>" | |
| output_log="/var/log/${name}.log" | |
| error_log="/var/log/${name}.err" | |
| # PID file required by start-stop-daemon for tracking, must be in a root-owned directory | |
| PIDFILE="/run/${name}.pid" | |
| WORKING_DIR="<CHANGE_ME_TO_WORKING_DIR>" | |
| COMMAND="/usr/bin/podman-compose" | |
| COMMAND_ARGS="up --remove-orphans" | |
| STOP_COMMAND_ARGS="down" | |
| depend() { | |
| need localmount net cgroups | |
| } | |
| start_pre() { | |
| if [ ! -d "${WORKING_DIR}" ]; then | |
| eerror "WORKING_DIR=${WORKING_DIR} not found for user ${command_user}. Is directory available?" | |
| return 1 | |
| fi | |
| cd "${WORKING_DIR}" | |
| if [ -f "${PIDFILE}" ]; then | |
| elog "Found an ungratefully exited PID file! Trying to clean-up running process." | |
| su -c "${COMMAND} ${STOP_COMMAND_ARGS}" - "${command_user}" | |
| fi | |
| # Ensure log files exist and have correct permissions | |
| checkpath -f --mode 0640 --owner "${command_user}:${command_user}" "${output_log}" "${error_log}" | |
| } | |
| stop_pre() { | |
| cd "${WORKING_DIR}" | |
| } | |
| start() { | |
| ebegin "Starting ${name} in ${WORKING_DIR}" | |
| start-stop-daemon --start --quiet --background --make-pidfile --pidfile "${PIDFILE}" \ | |
| --chuid "${command_user}" --chdir "${WORKING_DIR}" \ | |
| --stdout "${output_log}" --stderr "${error_log}" \ | |
| --exec "${COMMAND}" -- ${COMMAND_ARGS} | |
| eend $? | |
| } | |
| stop() { | |
| ebegin "Stopping ${name}" | |
| start-stop-daemon --stop --quiet --pidfile "${PIDFILE}" \ | |
| --chuid "${command_user}" --chdir "${WORKING_DIR}" \ | |
| --signal INT | |
| if [ -f "${PIDFILE}" ]; then | |
| # Clean up the PID file after stop | |
| rm "${PIDFILE}" | |
| fi | |
| eend $? | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment