Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ vendor/

# cloudflared credentials
credentials.json

CLAUDE.local.md
30 changes: 30 additions & 0 deletions registry-mirror/config.yml
Original file line number Diff line number Diff line change
@@ -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
189 changes: 189 additions & 0 deletions registry-mirror/install.sh
Original file line number Diff line number Diff line change
@@ -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 " [+] $*"; }

Check warning on line 28 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOH&open=AZ0lUshh_bGaREZrEHOH&pullRequest=174
warn() { echo " [!] $*"; }

Check warning on line 29 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOI&open=AZ0lUshh_bGaREZrEHOI&pullRequest=174
die() { echo " [✗] $*" >&2; exit 1; }

Check warning on line 30 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOJ&open=AZ0lUshh_bGaREZrEHOJ&pullRequest=174

require_root() {

Check warning on line 32 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOK&open=AZ0lUshh_bGaREZrEHOK&pullRequest=174
if [[ $EUID -ne 0 ]]; then
die "This script must be run as root (use sudo)."
fi
}

check_deps() {

Check warning on line 38 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOL&open=AZ0lUshh_bGaREZrEHOL&pullRequest=174
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() {

Check warning on line 46 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOM&open=AZ0lUshh_bGaREZrEHOM&pullRequest=174
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" <<EOF
[Unit]
Description=Docker Registry Mirror (pull-through cache for Docker Hub)
After=docker.service
Requires=docker.service

[Service]
Restart=always
RestartSec=5s
ExecStartPre=-/usr/bin/docker stop ${SERVICE_NAME}
ExecStartPre=-/usr/bin/docker rm ${SERVICE_NAME}
ExecStart=/usr/bin/docker run --rm \\
--name ${SERVICE_NAME} \\
-p 127.0.0.1:${MIRROR_PORT}:5000 \\
-v ${REGISTRY_DATA}:/var/lib/registry \\
-v /etc/docker-registry-mirror/config.yml:/etc/docker/registry/config.yml:ro \\
registry:2
ExecStop=/usr/bin/docker stop ${SERVICE_NAME}

[Install]
WantedBy=multi-user.target
EOF

# 5. Enable and start the service
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl start "$SERVICE_NAME"

# Wait briefly and verify
local retries=10
while ! curl -sf "${MIRROR_URL}/v2/" >/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() {

Check warning on line 116 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHON&open=AZ0lUshh_bGaREZrEHON&pullRequest=174
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" <<EOF
{
"registry-mirrors": ["${MIRROR_URL}"]
}
EOF
info "Created ${DAEMON_JSON}"
fi
}

# ── uninstall ──────────────────────────────────────────────────────────────────

uninstall() {

Check warning on line 138 in registry-mirror/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=PrestaShop_prestashop-flashlight&issues=AZ0lUshh_bGaREZrEHOO&open=AZ0lUshh_bGaREZrEHOO&pullRequest=174
info "Uninstalling Docker registry mirror..."

# Stop and disable the service
if systemctl is-active --quiet "$SERVICE_NAME" 2>/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
23 changes: 23 additions & 0 deletions registry-mirror/prune-cache.sh
Original file line number Diff line number Diff line change
@@ -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."
Loading