|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Install a Docker Hub pull-through cache (registry mirror) on this machine. |
| 3 | +# |
| 4 | +# What this does: |
| 5 | +# - Runs a registry:2 container on localhost:5000 as a systemd service |
| 6 | +# - Configures /etc/docker/daemon.json to route all pulls through it |
| 7 | +# - Schedules a daily cron job to prune blobs older than 30 days |
| 8 | +# |
| 9 | +# Usage: |
| 10 | +# ./registry-mirror/install.sh # install |
| 11 | +# ./registry-mirror/install.sh uninstall # remove everything |
| 12 | +# |
| 13 | +# Requirements: docker, sudo, systemd, cron |
| 14 | + |
| 15 | +set -euo pipefail |
| 16 | + |
| 17 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 18 | +REGISTRY_DATA=/var/lib/docker-registry-mirror |
| 19 | +SERVICE_NAME=docker-registry-mirror |
| 20 | +SERVICE_FILE=/etc/systemd/system/${SERVICE_NAME}.service |
| 21 | +CRON_FILE=/etc/cron.d/${SERVICE_NAME}-prune |
| 22 | +DAEMON_JSON=/etc/docker/daemon.json |
| 23 | +MIRROR_PORT=5000 |
| 24 | +MIRROR_URL="http://localhost:${MIRROR_PORT}" |
| 25 | + |
| 26 | +# ── helpers ──────────────────────────────────────────────────────────────────── |
| 27 | + |
| 28 | +info() { echo " [+] $*"; } |
| 29 | +warn() { echo " [!] $*"; } |
| 30 | +die() { echo " [✗] $*" >&2; exit 1; } |
| 31 | + |
| 32 | +require_root() { |
| 33 | + if [[ $EUID -ne 0 ]]; then |
| 34 | + die "This script must be run as root (use sudo)." |
| 35 | + fi |
| 36 | +} |
| 37 | + |
| 38 | +check_deps() { |
| 39 | + command -v docker >/dev/null 2>&1 || die "docker is not installed or not in PATH." |
| 40 | + command -v systemctl >/dev/null 2>&1 || die "systemd is required." |
| 41 | + command -v jq >/dev/null 2>&1 || die "jq is required (apt-get install jq)." |
| 42 | +} |
| 43 | + |
| 44 | +# ── install ──────────────────────────────────────────────────────────────────── |
| 45 | + |
| 46 | +install() { |
| 47 | + info "Installing Docker registry mirror..." |
| 48 | + |
| 49 | + # 1. Pull the registry image up-front so the service starts cleanly |
| 50 | + info "Pulling registry:2 image..." |
| 51 | + docker pull registry:2 |
| 52 | + |
| 53 | + # 2. Create data directory |
| 54 | + mkdir -p "$REGISTRY_DATA" |
| 55 | + |
| 56 | + # 3. Install config and prune script |
| 57 | + mkdir -p /etc/docker-registry-mirror |
| 58 | + cp "${SCRIPT_DIR}/config.yml" /etc/docker-registry-mirror/config.yml |
| 59 | + cp "${SCRIPT_DIR}/prune-cache.sh" /etc/docker-registry-mirror/prune-cache.sh |
| 60 | + chmod +x /etc/docker-registry-mirror/prune-cache.sh |
| 61 | + |
| 62 | + # 4. Write the systemd unit (references /etc/docker-registry-mirror, not the repo) |
| 63 | + cat > "$SERVICE_FILE" <<EOF |
| 64 | +[Unit] |
| 65 | +Description=Docker Registry Mirror (pull-through cache for Docker Hub) |
| 66 | +After=docker.service |
| 67 | +Requires=docker.service |
| 68 | +
|
| 69 | +[Service] |
| 70 | +Restart=always |
| 71 | +RestartSec=5s |
| 72 | +ExecStartPre=-/usr/bin/docker stop ${SERVICE_NAME} |
| 73 | +ExecStartPre=-/usr/bin/docker rm ${SERVICE_NAME} |
| 74 | +ExecStart=/usr/bin/docker run --rm \\ |
| 75 | + --name ${SERVICE_NAME} \\ |
| 76 | + -p 127.0.0.1:${MIRROR_PORT}:5000 \\ |
| 77 | + -v ${REGISTRY_DATA}:/var/lib/registry \\ |
| 78 | + -v /etc/docker-registry-mirror/config.yml:/etc/docker/registry/config.yml:ro \\ |
| 79 | + registry:2 |
| 80 | +ExecStop=/usr/bin/docker stop ${SERVICE_NAME} |
| 81 | +
|
| 82 | +[Install] |
| 83 | +WantedBy=multi-user.target |
| 84 | +EOF |
| 85 | + |
| 86 | + # 5. Enable and start the service |
| 87 | + systemctl daemon-reload |
| 88 | + systemctl enable "$SERVICE_NAME" |
| 89 | + systemctl start "$SERVICE_NAME" |
| 90 | + |
| 91 | + # Wait briefly and verify |
| 92 | + local retries=10 |
| 93 | + while ! curl -sf "${MIRROR_URL}/v2/" >/dev/null 2>&1; do |
| 94 | + retries=$((retries - 1)) |
| 95 | + [[ $retries -eq 0 ]] && die "Mirror did not start in time. Check: journalctl -u ${SERVICE_NAME}" |
| 96 | + sleep 1 |
| 97 | + done |
| 98 | + info "Mirror is up at ${MIRROR_URL}" |
| 99 | + |
| 100 | + # 6. Configure Docker daemon to use the mirror |
| 101 | + configure_daemon |
| 102 | + |
| 103 | + # 7. Restart Docker so the mirror config takes effect |
| 104 | + info "Restarting Docker daemon..." |
| 105 | + systemctl restart docker |
| 106 | + |
| 107 | + # 8. Install daily prune cron |
| 108 | + echo "0 3 * * * root /etc/docker-registry-mirror/prune-cache.sh >> /var/log/docker-registry-prune.log 2>&1" \ |
| 109 | + > "$CRON_FILE" |
| 110 | + info "Daily prune cron installed at ${CRON_FILE} (runs 03:00 every day)" |
| 111 | + |
| 112 | + echo "" |
| 113 | + info "Done. All docker pull / FROM calls will now be cached locally for 30 days." |
| 114 | +} |
| 115 | + |
| 116 | +configure_daemon() { |
| 117 | + if [[ -f "$DAEMON_JSON" ]]; then |
| 118 | + # Merge: add/replace registry-mirrors key, preserve the rest |
| 119 | + local tmp |
| 120 | + tmp=$(mktemp) |
| 121 | + jq --arg mirror "$MIRROR_URL" \ |
| 122 | + '. + {"registry-mirrors": [$mirror]}' \ |
| 123 | + "$DAEMON_JSON" > "$tmp" |
| 124 | + mv "$tmp" "$DAEMON_JSON" |
| 125 | + info "Updated existing ${DAEMON_JSON}" |
| 126 | + else |
| 127 | + cat > "$DAEMON_JSON" <<EOF |
| 128 | +{ |
| 129 | + "registry-mirrors": ["${MIRROR_URL}"] |
| 130 | +} |
| 131 | +EOF |
| 132 | + info "Created ${DAEMON_JSON}" |
| 133 | + fi |
| 134 | +} |
| 135 | + |
| 136 | +# ── uninstall ────────────────────────────────────────────────────────────────── |
| 137 | + |
| 138 | +uninstall() { |
| 139 | + info "Uninstalling Docker registry mirror..." |
| 140 | + |
| 141 | + # Stop and disable the service |
| 142 | + if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then |
| 143 | + systemctl stop "$SERVICE_NAME" |
| 144 | + fi |
| 145 | + if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then |
| 146 | + systemctl disable "$SERVICE_NAME" |
| 147 | + fi |
| 148 | + [[ -f "$SERVICE_FILE" ]] && rm -f "$SERVICE_FILE" |
| 149 | + systemctl daemon-reload |
| 150 | + |
| 151 | + # Remove mirror from daemon.json |
| 152 | + if [[ -f "$DAEMON_JSON" ]]; then |
| 153 | + local tmp |
| 154 | + tmp=$(mktemp) |
| 155 | + jq 'del(."registry-mirrors")' "$DAEMON_JSON" > "$tmp" |
| 156 | + # If the file is now just "{}", remove it entirely |
| 157 | + if [[ "$(cat "$tmp")" == "{}" ]]; then |
| 158 | + rm -f "$DAEMON_JSON" |
| 159 | + info "Removed ${DAEMON_JSON} (was empty after cleanup)" |
| 160 | + else |
| 161 | + mv "$tmp" "$DAEMON_JSON" |
| 162 | + info "Removed registry-mirrors entry from ${DAEMON_JSON}" |
| 163 | + fi |
| 164 | + systemctl restart docker |
| 165 | + fi |
| 166 | + |
| 167 | + # Remove cron |
| 168 | + [[ -f "$CRON_FILE" ]] && rm -f "$CRON_FILE" && info "Removed cron job" |
| 169 | + |
| 170 | + # Remove installed config files |
| 171 | + rm -rf /etc/docker-registry-mirror |
| 172 | + info "Removed /etc/docker-registry-mirror" |
| 173 | + |
| 174 | + warn "Registry data at ${REGISTRY_DATA} was NOT deleted (may be large)." |
| 175 | + warn "To free disk space: sudo rm -rf ${REGISTRY_DATA}" |
| 176 | + |
| 177 | + info "Done." |
| 178 | +} |
| 179 | + |
| 180 | +# ── main ─────────────────────────────────────────────────────────────────────── |
| 181 | + |
| 182 | +require_root |
| 183 | +check_deps |
| 184 | + |
| 185 | +case "${1:-install}" in |
| 186 | + install) install ;; |
| 187 | + uninstall) uninstall ;; |
| 188 | + *) die "Unknown command '${1}'. Use: install | uninstall" ;; |
| 189 | +esac |
0 commit comments