Skip to content

Commit 247795d

Browse files
Install local docker registry
1 parent b039ed3 commit 247795d

4 files changed

Lines changed: 244 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ vendor/
1414

1515
# cloudflared credentials
1616
credentials.json
17+
18+
CLAUDE.local.md

registry-mirror/config.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
version: 0.1
2+
log:
3+
fields:
4+
service: registry-mirror
5+
6+
storage:
7+
cache:
8+
blobdescriptor: inmemory
9+
filesystem:
10+
rootdirectory: /var/lib/registry
11+
maintenance:
12+
uploadpurging:
13+
enabled: true
14+
age: 168h # purge incomplete uploads after 7 days
15+
interval: 24h
16+
dryrun: false
17+
18+
http:
19+
addr: :5000
20+
headers:
21+
X-Content-Type-Options: [nosniff]
22+
23+
proxy:
24+
remoteurl: https://registry-1.docker.io
25+
26+
health:
27+
storagedriver:
28+
enabled: true
29+
interval: 10s
30+
threshold: 3

registry-mirror/install.sh

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

registry-mirror/prune-cache.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
# Prune registry cache: delete blobs older than 30 days, then garbage-collect.
3+
set -euo pipefail
4+
5+
REGISTRY_DATA=/var/lib/docker-registry-mirror
6+
7+
echo "[$(date -Iseconds)] Starting cache prune (blobs older than 30 days)..."
8+
9+
# Delete repository manifest revision files older than 30 days.
10+
# This marks images as unreferenced so GC can remove their blobs.
11+
find "$REGISTRY_DATA/docker/registry/v2/repositories" \
12+
-name "*.json" -mtime +30 -delete 2>/dev/null || true
13+
14+
find "$REGISTRY_DATA/docker/registry/v2/repositories" \
15+
-name "link" -mtime +30 -delete 2>/dev/null || true
16+
17+
# Run registry garbage collect inside the container
18+
docker exec docker-registry-mirror \
19+
/bin/registry garbage-collect \
20+
--delete-untagged \
21+
/etc/docker/registry/config.yml
22+
23+
echo "[$(date -Iseconds)] Cache prune complete."

0 commit comments

Comments
 (0)