From 247795d621c0ee4fef162e3e1ebefe2eda4c2c00 Mon Sep 17 00:00:00 2001 From: Alexandre Lematre Date: Wed, 25 Mar 2026 14:30:55 +0100 Subject: [PATCH] Install local docker registry --- .gitignore | 2 + registry-mirror/config.yml | 30 ++++++ registry-mirror/install.sh | 189 +++++++++++++++++++++++++++++++++ registry-mirror/prune-cache.sh | 23 ++++ 4 files changed, 244 insertions(+) create mode 100644 registry-mirror/config.yml create mode 100755 registry-mirror/install.sh create mode 100755 registry-mirror/prune-cache.sh diff --git a/.gitignore b/.gitignore index ad76784..5c2a192 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ vendor/ # cloudflared credentials credentials.json + +CLAUDE.local.md \ No newline at end of file diff --git a/registry-mirror/config.yml b/registry-mirror/config.yml new file mode 100644 index 0000000..2f96d6e --- /dev/null +++ b/registry-mirror/config.yml @@ -0,0 +1,30 @@ +version: 0.1 +log: + fields: + service: registry-mirror + +storage: + cache: + blobdescriptor: inmemory + filesystem: + rootdirectory: /var/lib/registry + maintenance: + uploadpurging: + enabled: true + age: 168h # purge incomplete uploads after 7 days + interval: 24h + dryrun: false + +http: + addr: :5000 + headers: + X-Content-Type-Options: [nosniff] + +proxy: + remoteurl: https://registry-1.docker.io + +health: + storagedriver: + enabled: true + interval: 10s + threshold: 3 diff --git a/registry-mirror/install.sh b/registry-mirror/install.sh new file mode 100755 index 0000000..d4cadd7 --- /dev/null +++ b/registry-mirror/install.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Install a Docker Hub pull-through cache (registry mirror) on this machine. +# +# What this does: +# - Runs a registry:2 container on localhost:5000 as a systemd service +# - Configures /etc/docker/daemon.json to route all pulls through it +# - Schedules a daily cron job to prune blobs older than 30 days +# +# Usage: +# ./registry-mirror/install.sh # install +# ./registry-mirror/install.sh uninstall # remove everything +# +# Requirements: docker, sudo, systemd, cron + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REGISTRY_DATA=/var/lib/docker-registry-mirror +SERVICE_NAME=docker-registry-mirror +SERVICE_FILE=/etc/systemd/system/${SERVICE_NAME}.service +CRON_FILE=/etc/cron.d/${SERVICE_NAME}-prune +DAEMON_JSON=/etc/docker/daemon.json +MIRROR_PORT=5000 +MIRROR_URL="http://localhost:${MIRROR_PORT}" + +# ── helpers ──────────────────────────────────────────────────────────────────── + +info() { echo " [+] $*"; } +warn() { echo " [!] $*"; } +die() { echo " [✗] $*" >&2; exit 1; } + +require_root() { + if [[ $EUID -ne 0 ]]; then + die "This script must be run as root (use sudo)." + fi +} + +check_deps() { + command -v docker >/dev/null 2>&1 || die "docker is not installed or not in PATH." + command -v systemctl >/dev/null 2>&1 || die "systemd is required." + command -v jq >/dev/null 2>&1 || die "jq is required (apt-get install jq)." +} + +# ── install ──────────────────────────────────────────────────────────────────── + +install() { + info "Installing Docker registry mirror..." + + # 1. Pull the registry image up-front so the service starts cleanly + info "Pulling registry:2 image..." + docker pull registry:2 + + # 2. Create data directory + mkdir -p "$REGISTRY_DATA" + + # 3. Install config and prune script + mkdir -p /etc/docker-registry-mirror + cp "${SCRIPT_DIR}/config.yml" /etc/docker-registry-mirror/config.yml + cp "${SCRIPT_DIR}/prune-cache.sh" /etc/docker-registry-mirror/prune-cache.sh + chmod +x /etc/docker-registry-mirror/prune-cache.sh + + # 4. Write the systemd unit (references /etc/docker-registry-mirror, not the repo) + cat > "$SERVICE_FILE" </dev/null 2>&1; do + retries=$((retries - 1)) + [[ $retries -eq 0 ]] && die "Mirror did not start in time. Check: journalctl -u ${SERVICE_NAME}" + sleep 1 + done + info "Mirror is up at ${MIRROR_URL}" + + # 6. Configure Docker daemon to use the mirror + configure_daemon + + # 7. Restart Docker so the mirror config takes effect + info "Restarting Docker daemon..." + systemctl restart docker + + # 8. Install daily prune cron + echo "0 3 * * * root /etc/docker-registry-mirror/prune-cache.sh >> /var/log/docker-registry-prune.log 2>&1" \ + > "$CRON_FILE" + info "Daily prune cron installed at ${CRON_FILE} (runs 03:00 every day)" + + echo "" + info "Done. All docker pull / FROM calls will now be cached locally for 30 days." +} + +configure_daemon() { + if [[ -f "$DAEMON_JSON" ]]; then + # Merge: add/replace registry-mirrors key, preserve the rest + local tmp + tmp=$(mktemp) + jq --arg mirror "$MIRROR_URL" \ + '. + {"registry-mirrors": [$mirror]}' \ + "$DAEMON_JSON" > "$tmp" + mv "$tmp" "$DAEMON_JSON" + info "Updated existing ${DAEMON_JSON}" + else + cat > "$DAEMON_JSON" </dev/null; then + systemctl stop "$SERVICE_NAME" + fi + if systemctl is-enabled --quiet "$SERVICE_NAME" 2>/dev/null; then + systemctl disable "$SERVICE_NAME" + fi + [[ -f "$SERVICE_FILE" ]] && rm -f "$SERVICE_FILE" + systemctl daemon-reload + + # Remove mirror from daemon.json + if [[ -f "$DAEMON_JSON" ]]; then + local tmp + tmp=$(mktemp) + jq 'del(."registry-mirrors")' "$DAEMON_JSON" > "$tmp" + # If the file is now just "{}", remove it entirely + if [[ "$(cat "$tmp")" == "{}" ]]; then + rm -f "$DAEMON_JSON" + info "Removed ${DAEMON_JSON} (was empty after cleanup)" + else + mv "$tmp" "$DAEMON_JSON" + info "Removed registry-mirrors entry from ${DAEMON_JSON}" + fi + systemctl restart docker + fi + + # Remove cron + [[ -f "$CRON_FILE" ]] && rm -f "$CRON_FILE" && info "Removed cron job" + + # Remove installed config files + rm -rf /etc/docker-registry-mirror + info "Removed /etc/docker-registry-mirror" + + warn "Registry data at ${REGISTRY_DATA} was NOT deleted (may be large)." + warn "To free disk space: sudo rm -rf ${REGISTRY_DATA}" + + info "Done." +} + +# ── main ─────────────────────────────────────────────────────────────────────── + +require_root +check_deps + +case "${1:-install}" in + install) install ;; + uninstall) uninstall ;; + *) die "Unknown command '${1}'. Use: install | uninstall" ;; +esac diff --git a/registry-mirror/prune-cache.sh b/registry-mirror/prune-cache.sh new file mode 100755 index 0000000..d414058 --- /dev/null +++ b/registry-mirror/prune-cache.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Prune registry cache: delete blobs older than 30 days, then garbage-collect. +set -euo pipefail + +REGISTRY_DATA=/var/lib/docker-registry-mirror + +echo "[$(date -Iseconds)] Starting cache prune (blobs older than 30 days)..." + +# Delete repository manifest revision files older than 30 days. +# This marks images as unreferenced so GC can remove their blobs. +find "$REGISTRY_DATA/docker/registry/v2/repositories" \ + -name "*.json" -mtime +30 -delete 2>/dev/null || true + +find "$REGISTRY_DATA/docker/registry/v2/repositories" \ + -name "link" -mtime +30 -delete 2>/dev/null || true + +# Run registry garbage collect inside the container +docker exec docker-registry-mirror \ + /bin/registry garbage-collect \ + --delete-untagged \ + /etc/docker/registry/config.yml + +echo "[$(date -Iseconds)] Cache prune complete."