refactor(mirror): replace 3 services with single Nexus Repository Manager

Consolidates BaGet + Verdaccio + registry:2 into one Sonatype Nexus OSS
instance with a REST API provisioning script.

docker-compose.mirror.yml: single nexus service, ports 8081 (UI/NuGet/npm)
  and 8083 (Docker Hub pull-through proxy)
mirrors/nexus/provision.sh: idempotent setup — changes admin password,
  enables anonymous access, creates nuget-proxy / npm-proxy / docker-hub-proxy
nuget.mirror.config: updated source URL to Nexus NuGet proxy endpoint
ci-cd.yml: updated npm --registry to Nexus npm proxy endpoint

Run once on server: docker compose -f docker-compose.mirror.yml up -d
  then: ./mirrors/nexus/provision.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-28 14:35:55 +03:30
parent 6f85cfe4d3
commit 61e44c63ab
4 changed files with 210 additions and 87 deletions
+8 -8
View File
@@ -24,11 +24,11 @@ concurrency:
# node:20-alpine → no git → checkout fails # node:20-alpine → no git → checkout fails
# Fix: plain shell git clone via http.extraheader (token never in process list). # Fix: plain shell git clone via http.extraheader (token never in process list).
# #
# Local mirrors (started via docker-compose.mirror.yml): # Local mirrors — Nexus Repository Manager (docker-compose.mirror.yml):
# "mirror" hostname → host-gateway (docker bridge IP 172.17.0.1) # "mirror" hostname → host-gateway (docker bridge IP 172.17.0.1)
# NuGet → http://mirror:5101 (BaGet — nuget.mirror.config) # NuGet → http://mirror:8081/repository/nuget-proxy/ (nuget.mirror.config)
# npm → http://mirror:4873 (Verdaccio — --registry flag) # npm → http://mirror:8081/repository/npm-proxy/ (--registry flag)
# Docker Hub → configured in /etc/docker/daemon.json on the host # Docker → http://mirror:8083 (daemon.json registry-mirrors)
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
jobs: jobs:
@@ -136,7 +136,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web/dashboard working-directory: web/dashboard
run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:4873 run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/
- name: TypeScript check - name: TypeScript check
working-directory: web/dashboard working-directory: web/dashboard
@@ -168,7 +168,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web/admin working-directory: web/admin
run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:4873 run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/
- name: TypeScript check - name: TypeScript check
working-directory: web/admin working-directory: web/admin
@@ -200,7 +200,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web/website working-directory: web/website
run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:4873 run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/
- name: TypeScript check - name: TypeScript check
working-directory: web/website working-directory: web/website
@@ -232,7 +232,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
working-directory: web/finder working-directory: web/finder
run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:4873 run: npm install --legacy-peer-deps --ignore-scripts --registry http://mirror:8081/repository/npm-proxy/
- name: TypeScript check - name: TypeScript check
working-directory: web/finder working-directory: web/finder
+28 -75
View File
@@ -1,90 +1,43 @@
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Local pull-through mirrors # Nexus Repository Manager OSS — single pull-through mirror for everything
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Start: docker compose -f docker-compose.mirror.yml up -d # FIRST-TIME SETUP (run once after starting):
# Stop: docker compose -f docker-compose.mirror.yml down # docker compose -f docker-compose.mirror.yml up -d
# ./mirrors/nexus/provision.sh # creates all proxy repos + enables anon access
# #
# Endpoints (reachable from CI containers via host-gateway as "mirror"): # Endpoints (after provisioning):
# NuGet → http://SERVER_IP:5101/v3/index.json # UI → http://SERVER_IP:8081 (admin / see provision.sh output)
# npm → http://SERVER_IP:4873 # NuGet → http://SERVER_IP:8081/repository/nuget-proxy/index.json
# Docker → http://SERVER_IP:5100 (add to /etc/docker/daemon.json) # npm → http://SERVER_IP:8081/repository/npm-proxy/
# Docker → http://SERVER_IP:8083 (add to /etc/docker/daemon.json)
# #
# First request for any package fetches from upstream and caches locally. # Memory: needs ~2 GB JVM heap — recommended on a server with 4 GB+ total RAM.
# Subsequent requests are served from disk — no upstream needed. # Adjust INSTALL4J_ADD_VM_PARAMS below if your server has more/less RAM.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
services: services:
nexus:
# ── NuGet mirror (BaGet) ──────────────────────────────────────────────────── image: sonatype/nexus3:latest
# Proxies → https://api.nuget.org/v3/index.json container_name: meezi-mirror-nexus
# CI usage: dotnet restore --configfile nuget.mirror.config
baget:
image: loicsharma/baget:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
ApiKey: "ci-mirror-key" # only needed for package *publish*; reads are open # Heap: Xmx = max Java heap. MaxDirectMemorySize = off-heap (blob cache).
Storage__Type: FileSystem # Total Nexus RAM ≈ Xmx + MaxDirectMemorySize + ~512 MB OS/JVM overhead.
Storage__Path: /var/baget/packages # 4 GB server: values below (2 GB heap + 1 GB off-heap + 512 MB overhead ≈ 3.5 GB)
Database__Type: Sqlite # 8 GB server: -Xms1g -Xmx4g -XX:MaxDirectMemorySize=2g
Database__ConnectionString: "Data Source=/var/baget/db/baget.db" INSTALL4J_ADD_VM_PARAMS: "-Xms512m -Xmx2g -XX:MaxDirectMemorySize=1g -Djava.util.prefs.userRoot=/nexus-data/javaprefs"
Mirror__Enabled: "true"
Mirror__PackageSource: "https://api.nuget.org/v3/index.json"
volumes: volumes:
- baget-packages:/var/baget/packages - nexus-data:/nexus-data
- baget-db:/var/baget/db
ports: ports:
- "5101:80" - "8081:8081" # Web UI + NuGet + npm REST API
- "8083:8083" # Docker Hub pull-through proxy (dedicated port required by Docker protocol)
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/health"] test: ["CMD", "curl", "-sf", "http://localhost:8081/service/rest/v1/status"]
interval: 30s interval: 30s
timeout: 10s timeout: 15s
retries: 3 retries: 10
start_period: 120s # Nexus JVM startup takes ~2 min on first boot
# ── npm mirror (Verdaccio) ──────────────────────────────────────────────────
# Proxies → https://registry.npmjs.org
# CI usage: npm install --registry http://mirror:4873
verdaccio:
image: verdaccio/verdaccio:latest
restart: unless-stopped
volumes:
- verdaccio-storage:/verdaccio/storage
- ./mirrors/verdaccio/config.yaml:/verdaccio/conf/config.yaml:ro
ports:
- "4873:4873"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4873/-/ping"]
interval: 30s
timeout: 10s
retries: 3
# ── Docker Hub pull-through cache ───────────────────────────────────────────
# Proxies → https://registry-1.docker.io (Docker Hub only)
# Activate by adding to /etc/docker/daemon.json on the server:
# { "registry-mirrors": ["http://localhost:5100"] }
# then: systemctl restart docker
registry:
image: registry:2
restart: unless-stopped
environment:
REGISTRY_PROXY_REMOTEURL: "https://registry-1.docker.io"
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
REGISTRY_PROXY_TTL: "168h" # cache pulled layers for 7 days
volumes:
- registry-data:/data
ports:
- "5100:5000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:5000/v2/"]
interval: 30s
timeout: 10s
retries: 3
volumes: volumes:
baget-packages: nexus-data:
name: meezi-mirror-baget-packages name: meezi-mirror-nexus-data
baget-db:
name: meezi-mirror-baget-db
verdaccio-storage:
name: meezi-mirror-verdaccio
registry-data:
name: meezi-mirror-registry
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Nexus first-time provisioning
# Run once on the server after: docker compose -f docker-compose.mirror.yml up -d
#
# What this does:
# 1. Waits for Nexus to finish starting up
# 2. Reads the one-time admin password, changes it to NEXUS_ADMIN_PASS
# 3. Enables anonymous (unauthenticated) read access — needed for CI
# 4. Creates proxy repos: nuget-proxy, npm-proxy, docker-hub-proxy
#
# Usage:
# ./mirrors/nexus/provision.sh
# NEXUS_ADMIN_PASS=MySecret ./mirrors/nexus/provision.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
NEXUS_URL="http://localhost:8081"
NEW_PASS="${NEXUS_ADMIN_PASS:-Mirror@2024!}" # change this or set env var
CONTAINER="meezi-mirror-nexus"
# ── 1. Wait for Nexus ────────────────────────────────────────────────────────
echo "⏳ Waiting for Nexus (can take 2-3 min on first boot)..."
until curl -sf "$NEXUS_URL/service/rest/v1/status" | grep -q '"edition"'; do
printf "."
sleep 6
done
echo ""
echo "✅ Nexus is up"
# ── 2. Resolve admin password ────────────────────────────────────────────────
INIT_PASS_FILE=$(docker exec "$CONTAINER" sh -c 'cat /nexus-data/admin.password 2>/dev/null || true')
if [ -n "$INIT_PASS_FILE" ]; then
echo "🔐 First-time password found — changing to configured password..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-u "admin:$INIT_PASS_FILE" -X PUT \
"$NEXUS_URL/service/rest/v1/security/users/admin/change-password" \
-H "Content-Type: text/plain" \
-d "$NEW_PASS")
if [ "$HTTP_CODE" = "204" ]; then
echo "✅ Password updated"
else
echo "⚠️ Password change returned HTTP $HTTP_CODE — continuing with original password"
NEW_PASS="$INIT_PASS_FILE"
fi
ADMIN_PASS="$NEW_PASS"
else
echo "️ admin.password not present — using NEXUS_ADMIN_PASS (already provisioned?)"
ADMIN_PASS="$NEW_PASS"
fi
AUTH="-u admin:$ADMIN_PASS"
# ── 3. Enable anonymous read access ─────────────────────────────────────────
echo "🔓 Enabling anonymous access..."
curl -sf $AUTH -X PUT "$NEXUS_URL/service/rest/v1/security/anonymous" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"userId":"anonymous","realmName":"NexusAuthorizingRealm"}' \
&& echo "✅ Anonymous access enabled"
# Enable NuGet + npm token realms
curl -sf $AUTH -X PUT "$NEXUS_URL/service/rest/v1/security/realms/active" \
-H "Content-Type: application/json" \
-d '["NexusAuthenticatingRealm","NexusAuthorizingRealm","NuGetApiKey","NpmToken"]' \
&& echo "✅ Realms configured (NuGet, npm)"
# ── Helper: create repo (skip if already exists) ────────────────────────────
create_repo() {
local TYPE="$1"
local JSON="$2"
local NAME
NAME=$(echo "$JSON" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
HTTP=$(curl -s -o /dev/null -w "%{http_code}" $AUTH \
-X POST "$NEXUS_URL/service/rest/v1/repositories/$TYPE" \
-H "Content-Type: application/json" \
-d "$JSON")
case "$HTTP" in
201) echo "$NAME created" ;;
400) echo "⚠️ $NAME already exists (skipped)" ;;
*) echo "$NAME failed — HTTP $HTTP" ;;
esac
}
# ── 4. NuGet proxy ──────────────────────────────────────────────────────────
echo ""
echo "📦 Creating NuGet proxy → nuget.org ..."
create_repo "nuget/proxy" '{
"name": "nuget-proxy",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"proxy": {
"remoteUrl": "https://api.nuget.org/v3/index.json",
"contentMaxAge": 1440,
"metadataMaxAge": 1440
},
"negativeCache": { "enabled": true, "timeToLive": 1440 },
"httpClient": { "blocked": false, "autoBlock": true }
}'
# ── 5. npm proxy ─────────────────────────────────────────────────────────────
echo "📦 Creating npm proxy → registry.npmjs.org ..."
create_repo "npm/proxy" '{
"name": "npm-proxy",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": false
},
"proxy": {
"remoteUrl": "https://registry.npmjs.org",
"contentMaxAge": 1440,
"metadataMaxAge": 1440
},
"negativeCache": { "enabled": true, "timeToLive": 1440 },
"httpClient": { "blocked": false, "autoBlock": true }
}'
# ── 6. Docker Hub proxy (port 8083) ─────────────────────────────────────────
echo "🐳 Creating Docker Hub proxy → registry-1.docker.io (port 8083) ..."
create_repo "docker/proxy" '{
"name": "docker-hub-proxy",
"online": true,
"storage": {
"blobStoreName": "default",
"strictContentTypeValidation": true
},
"proxy": {
"remoteUrl": "https://registry-1.docker.io",
"contentMaxAge": 1440,
"metadataMaxAge": 1440
},
"negativeCache": { "enabled": true, "timeToLive": 1440 },
"httpClient": { "blocked": false, "autoBlock": true },
"docker": {
"v1Enabled": false,
"forceBasicAuth": false,
"httpPort": 8083
},
"dockerProxy": {
"indexType": "HUB",
"cacheForeignLayers": false
}
}'
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════"
echo "🎉 Nexus provisioned successfully!"
echo "═══════════════════════════════════════════════════════"
echo ""
echo " UI → http://SERVER_IP:8081"
echo " admin / $ADMIN_PASS"
echo ""
echo " NuGet → http://SERVER_IP:8081/repository/nuget-proxy/index.json"
echo " npm → http://SERVER_IP:8081/repository/npm-proxy/"
echo " Docker → http://SERVER_IP:8083"
echo ""
echo "To activate Docker Hub mirror — edit /etc/docker/daemon.json:"
cat << 'EOF'
{
"insecure-registries": ["SERVER_IP:8083"],
"registry-mirrors": ["http://SERVER_IP:8083"]
}
EOF
echo " then: systemctl restart docker"
echo ""
+4 -4
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- NuGet config for CI: routes all restores through the local BaGet mirror. <!-- NuGet config for CI: routes all restores through the local Nexus mirror.
Usage: dotnet restore --configfile nuget.mirror.config Usage: dotnet restore --configfile nuget.mirror.config
BaGet fetches from nuget.org on first request, then caches locally. Nexus fetches from nuget.org on first request, then caches locally.
DO NOT use this file for local development (mirror must be running). --> DO NOT use for local development (mirror must be running on the server). -->
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="local-mirror" value="http://mirror:5101/v3/index.json" protocolVersion="3" /> <add key="nexus-nuget" value="http://mirror:8081/repository/nuget-proxy/index.json" protocolVersion="3" />
</packageSources> </packageSources>
<config> <config>
<add key="http_retry_count" value="8" /> <add key="http_retry_count" value="8" />