Skip to content

Instantly share code, notes, and snippets.

@monun
Last active July 12, 2026 21:29
Show Gist options
  • Select an option

  • Save monun/b8b81fe3e8c05cc87633867d3ff875f3 to your computer and use it in GitHub Desktop.

Select an option

Save monun/b8b81fe3e8c05cc87633867d3ff875f3 to your computer and use it in GitHub Desktop.
docker-gc.service
#!/usr/bin/env bash
set -Eeuo pipefail
UNIT_DIR="/etc/systemd/system"
SERVICE_UNIT="docker-gc.service"
TIMER_UNIT="docker-gc.timer"
TMP_DIR=""
cleanup() {
if [[ -n "${TMP_DIR:-}" && -d "$TMP_DIR" ]]; then
rm -r -- "$TMP_DIR"
fi
}
trap cleanup EXIT
usage() {
printf '%s\n' \
'Usage:' \
' sudo ./svc.sh install' \
' sudo ./svc.sh uninstall' \
' sudo ./svc.sh status' \
'' \
'Remote usage:' \
' curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s install' \
' curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s uninstall' \
' curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s status'
}
die() {
echo "error: $*" >&2
exit 1
}
require_root() {
if (( EUID != 0 )); then
die "this command must be run with sudo"
fi
}
require_command() {
local command="$1"
if ! command -v "$command" >/dev/null 2>&1; then
die "required command not found: $command"
fi
}
require_systemd() {
require_command systemctl
if [[ ! -d /run/systemd/system ]]; then
die "systemd is not running"
fi
}
open_terminal() {
# Read prompts from the terminal even when invoked through curl | sudo bash.
if ! { exec 3<>/dev/tty; } 2>/dev/null; then
die "unable to open an interactive terminal"
fi
}
prompt() {
local message="$1"
local default="$2"
printf '%s [%s]: ' "$message" "$default" >&3
IFS= read -r REPLY <&3 || REPLY=""
if [[ -z "$REPLY" ]]; then
REPLY="$default"
fi
}
prompt_integer() {
local message="$1"
local default="$2"
local minimum="$3"
local maximum="$4"
local value
while true; do
prompt "$message" "$default"
if [[ "$REPLY" =~ ^[0-9]{1,9}$ ]]; then
value=$((10#$REPLY))
if (( value >= minimum && value <= maximum )); then
REPLY="$value"
return
fi
fi
printf \
'Invalid value. Enter a whole number from %d to %d.\n' \
"$minimum" \
"$maximum" >&3
done
}
prompt_time() {
local message="$1"
local default="$2"
while true; do
prompt "$message" "$default"
if [[ "$REPLY" =~ ^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$ ]]; then
return
fi
printf \
'Invalid time. Enter a 24-hour time in HH:MM:SS format, for example 01:00:00.\n' >&3
done
}
install_service() {
require_root
require_systemd
require_command docker
require_command install
require_command systemd-analyze
require_command mktemp
require_command rm
if [[ ! -x /usr/bin/docker ]]; then
die "/usr/bin/docker not found"
fi
if ! /usr/bin/docker buildx version >/dev/null 2>&1; then
die "Docker Buildx is not available"
fi
open_terminal
echo
echo "Docker GC configuration"
echo
prompt_integer "Minimum free disk space to maintain (GB)" "40" 1 999999
local min_free_gb="$REPLY"
prompt_integer "Minimum build cache space to preserve (GB)" "10" 1 999999
local reserved_gb="$REPLY"
prompt_integer "Minimum resource age before pruning (hours)" "168" 1 999999
local prune_hours="$REPLY"
prompt_time "Daily cleanup time (HH:MM:SS)" "03:30:00"
local cleanup_time="$REPLY"
prompt_integer "Randomized execution delay (minutes)" "30" 0 1440
local randomized_delay_minutes="$REPLY"
local min_free_space="${min_free_gb}gb"
local reserved_space="${reserved_gb}gb"
local prune_until="${prune_hours}h"
local randomized_delay="${randomized_delay_minutes}m"
local on_calendar="*-*-* ${cleanup_time}"
echo
echo "Configuration summary"
echo " Minimum free space: $min_free_gb GB"
echo " Reserved cache: $reserved_gb GB"
echo " Prune resources: older than $prune_hours hours"
echo " Cleanup time: $cleanup_time"
echo " Randomized delay: up to $randomized_delay_minutes minutes"
echo
echo "Generated systemd values"
echo " OnCalendar=$on_calendar"
echo " RandomizedDelaySec=$randomized_delay"
echo
prompt "Install with this configuration? (Y/n)" "Y"
case "$REPLY" in
Y|y|YES|Yes|yes)
;;
*)
echo "Installation cancelled."
return
;;
esac
TMP_DIR="$(mktemp -d)"
{
printf '%s\n' \
'[Unit]' \
'Description=Prune Docker data on GitHub Actions runner' \
'After=docker.service' \
'Requires=docker.service' \
'' \
'[Service]' \
'Type=oneshot'
printf \
'ExecStart=/usr/bin/docker buildx prune -af --min-free-space %s --reserved-space %s\n' \
"$min_free_space" \
"$reserved_space"
printf \
'ExecStart=/usr/bin/docker system prune -af --filter until=%s\n' \
"$prune_until"
} >"${TMP_DIR}/${SERVICE_UNIT}"
{
printf '%s\n' \
'[Unit]' \
'Description=Daily Docker cleanup' \
'' \
'[Timer]'
printf 'OnCalendar=%s\n' "$on_calendar"
printf 'RandomizedDelaySec=%s\n' "$randomized_delay"
printf '%s\n' \
'Persistent=true' \
'' \
'[Install]' \
'WantedBy=timers.target'
} >"${TMP_DIR}/${TIMER_UNIT}"
echo
echo "Validating systemd units..."
systemd-analyze verify \
"${TMP_DIR}/${SERVICE_UNIT}" \
"${TMP_DIR}/${TIMER_UNIT}"
echo "Installing systemd units..."
install -o root -g root -m 0644 \
"${TMP_DIR}/${SERVICE_UNIT}" \
"${UNIT_DIR}/${SERVICE_UNIT}"
install -o root -g root -m 0644 \
"${TMP_DIR}/${TIMER_UNIT}" \
"${UNIT_DIR}/${TIMER_UNIT}"
systemctl daemon-reload
systemctl enable "$TIMER_UNIT" >/dev/null
systemctl restart "$TIMER_UNIT"
if ! systemctl is-active --quiet "$TIMER_UNIT"; then
echo "error: $TIMER_UNIT failed to start" >&2
systemctl status "$TIMER_UNIT" --no-pager || true
exit 1
fi
echo
echo "Installation completed successfully."
echo
systemctl list-timers --all "$TIMER_UNIT"
}
uninstall_service() {
require_root
require_systemd
require_command rm
if [[ ! -e "${UNIT_DIR}/${SERVICE_UNIT}" && ! -e "${UNIT_DIR}/${TIMER_UNIT}" ]]; then
echo "Docker GC is not installed."
return
fi
systemctl disable --now "$TIMER_UNIT" >/dev/null 2>&1 || true
systemctl stop "$SERVICE_UNIT" >/dev/null 2>&1 || true
rm -f \
"${UNIT_DIR}/${SERVICE_UNIT}" \
"${UNIT_DIR}/${TIMER_UNIT}"
systemctl daemon-reload
systemctl reset-failed "$SERVICE_UNIT" "$TIMER_UNIT" >/dev/null 2>&1 || true
echo "Docker GC uninstalled successfully."
}
show_status() {
require_systemd
if [[ ! -e "${UNIT_DIR}/${SERVICE_UNIT}" && ! -e "${UNIT_DIR}/${TIMER_UNIT}" ]]; then
echo "Docker GC is not installed."
return
fi
local enabled
local active
enabled="$(systemctl is-enabled "$TIMER_UNIT" 2>/dev/null || true)"
active="$(systemctl is-active "$TIMER_UNIT" 2>/dev/null || true)"
echo "Docker GC status"
echo " Installed: yes"
echo " Enabled: ${enabled:-unknown}"
echo " Active: ${active:-unknown}"
echo
systemctl list-timers --all "$TIMER_UNIT" || true
echo
systemctl status "$TIMER_UNIT" "$SERVICE_UNIT" --no-pager || true
}
if (( $# != 1 )); then
usage >&2
exit 2
fi
case "$1" in
install)
install_service
;;
uninstall)
uninstall_service
;;
status)
show_status
;;
-h|--help|help)
usage
;;
*)
echo "error: unknown command: $1" >&2
echo >&2
usage >&2
exit 2
;;
esac
@monun

monun commented Jul 12, 2026

Copy link
Copy Markdown
Author

Download and run

wget -O svc.sh https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh
chmod +x svc.sh
sudo ./svc.sh install
sudo ./svc.sh status
sudo ./svc.sh uninstall

Run directly

Install:

curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s install

Status:

curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s status

Uninstall:

curl -fsSL https://gist.githubusercontent.com/monun/b8b81fe3e8c05cc87633867d3ff875f3/raw/svc.sh | sudo bash -s uninstall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment