How to Build a Private Linux Mirror Repository from Scratch – Complete Production Guide
This step‑by‑step guide explains how to design, provision, and configure a private Linux package mirror for Ubuntu, CentOS, and Docker, covering hardware sizing, network layout, APT/YUM repository setup, Docker Registry deployment, HAProxy load balancing, Keepalived high‑availability, monitoring with Prometheus and Grafana, security hardening, automated synchronization scripts, and troubleshooting procedures.
Overview
In enterprise environments a private Linux mirror repository provides network optimization, security control, version management, offline deployment, and cost savings.
Network optimization : reduce external bandwidth and improve download speed.
Security control : safe software distribution inside the intranet.
Version management : unified package versions for consistent environments.
Offline deployment : support installations without internet access.
Cost saving : avoid duplicate downloads and lower bandwidth costs.
Environment Preparation
Hardware Requirements
Component
Minimum
Recommended
Production
CPU
2 cores
4 cores
8 cores+
Memory
4 GB
8 GB
16 GB+
Storage
500 GB
2 TB
10 TB+
Network
100 Mbps
1 Gbps
10 Gbps
Software Environment
# OS: Ubuntu 22.04 LTS or CentOS 8
# Web server: Nginx
# Sync tools: rsync, apt-mirror, reposync
# Monitoring: Prometheus + GrafanaAPT Private Mirror
Install apt-mirror
# Ubuntu/Debian
sudo apt update
sudo apt install -y apt-mirror nginx
# Create mirror user
sudo useradd -r -s /bin/false -d /data/mirrors aptmirror
sudo chown -R aptmirror:aptmirror /data/mirrorsConfigure apt-mirror
# /etc/apt/mirror.list
set base_path /data/mirrors/ubuntu
set mirror_path $base_path/mirror
set skel_path $base_path/skel
set var_path $base_path/var
set cleanscript $var_path/clean.sh
set defaultarch amd64
set postmirror_script $var_path/postmirror.sh
set run_postmirror 0
set nthreads 20
set _tilde 0
# Ubuntu 22.04 (Jammy)
deb http://archive.ubuntu.com/ubuntu jammy main restricted universe multiverse
... (other repos) ...
clean http://archive.ubuntu.com/ubuntu
clean http://security.ubuntu.com/ubuntuCreate sync script
# /data/mirrors/scripts/sync-ubuntu.sh
#!/bin/bash
set -e
LOGFILE="/data/mirrors/logs/ubuntu-sync-$(date +%Y%m%d-%H%M%S).log"
LOCKFILE="/var/run/ubuntu-mirror.lock"
if [ -f "$LOCKFILE" ]; then
echo "Sync already running, exiting..."
exit 1
fi
echo $$ > "$LOCKFILE"
trap "rm -f \"$LOCKFILE\"" EXIT
echo "Starting Ubuntu mirror sync: $(date)" | tee -a "$LOGFILE"
sudo -u aptmirror apt-mirror /etc/apt/mirror.list 2>&1 | tee -a "$LOGFILE"
echo "$(date)" > /data/mirrors/ubuntu/last_update
echo "Ubuntu mirror sync completed: $(date)" | tee -a "$LOGFILE"
find /data/mirrors/logs -name "ubuntu-sync-*.log" -mtime +30 -deleteYUM Private Mirror
Install reposync
# CentOS/RHEL
sudo yum install -y yum-utils createrepo nginx
# Or on Ubuntu
sudo apt install -y yum-utils createrepo-c nginxCreate sync script
# /data/mirrors/scripts/sync-centos.sh
#!/bin/bash
set -e
MIRROR_BASE="/data/mirrors/centos"
LOGFILE="/data/mirrors/logs/centos-sync-$(date +%Y%m%d-%H%M%S).log"
LOCKFILE="/var/run/centos-mirror.lock"
if [ -f "$LOCKFILE" ]; then
echo "Sync already running, exiting..."
exit 1
fi
echo $$ > "$LOCKFILE"
cleanup() { rm -f "$LOCKFILE"; }
trap cleanup EXIT
echo "Starting CentOS mirror sync: $(date)" | tee -a "$LOGFILE"
# Sync streams
sync_centos_stream() {
local version=$1
local repo_dir="$MIRROR_BASE/$version"
mkdir -p "$repo_dir"
for repo in baseos appstream extras powertools; do
echo "Syncing CentOS $version $repo..." | tee -a "$LOGFILE"
reposync --download-path="$repo_dir" --repo="$repo" --arch=x86_64 --newest-only --delete 2>&1 | tee -a "$LOGFILE"
createrepo_c "$repo_dir/$repo/" 2>&1 | tee -a "$LOGFILE"
done
}
sync_centos_stream "8-stream"
sync_centos_stream "9-stream"
echo "$(date)" > "$MIRROR_BASE/last_update"
echo "CentOS mirror sync completed: $(date)" | tee -a "$LOGFILE"
find /data/mirrors/logs -name "centos-sync-*.log" -mtime +30 -deleteDocker Private Registry
Install Docker and Registry
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Create directories
sudo mkdir -p /data/mirrors/docker/{registry,auth,certs}Configure Registry
# /data/mirrors/docker/config.yml
version: 0.1
log:
accesslog:
disabled: false
level: info
formatter: text
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['HEAD','GET','OPTIONS','DELETE']
Access-Control-Allow-Headers: ['Authorization','Accept','Cache-Control']
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
proxy:
remoteurl: https://registry-1.docker.io
username: your-dockerhub-username
password: your-dockerhub-passwordStart Registry with Docker‑Compose
# /data/mirrors/docker/docker-compose.yml
version: '3.8'
services:
registry:
image: registry:2.8
container_name: docker-registry
restart: unless-stopped
ports:
- "5000:5000"
environment:
REGISTRY_CONFIG_PATH: /etc/docker/registry/config.yml
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
volumes:
- /data/mirrors/docker/registry:/var/lib/registry
- /data/mirrors/docker/config.yml:/etc/docker/registry/config.yml:ro
networks:
- registry-net
registry-ui:
image: joxit/docker-registry-ui:latest
container_name: registry-ui
restart: unless-stopped
ports:
- "8080:80"
environment:
REGISTRY_TITLE: "Private Docker Registry"
REGISTRY_URL: http://registry:5000
DELETE_IMAGES: "true"
SHOW_CONTENT_DIGEST: "true"
depends_on:
- registry
networks:
- registry-net
networks:
registry-net:
driver: bridgeHigh Availability and Load Balancing
HAProxy Configuration
# Install HAProxy
sudo apt install -y haproxy
# /etc/haproxy/haproxy.cfg
global
daemon
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option httplog
option dontlognull
errorfile 400 /etc/haproxy/errors/400.http
...
frontend mirror_frontend
bind *:80
bind *:443 ssl crt /etc/ssl/certs/mirror.pem
redirect scheme https if !{ ssl_fc }
acl is_ubuntu hdr(host) -i ubuntu-mirror.example.com
acl is_centos hdr(host) -i centos-mirror.example.com
acl is_docker hdr(host) -i docker-registry.example.com
use_backend ubuntu_backend if is_ubuntu
use_backend centos_backend if is_centos
use_backend docker_backend if is_docker
default_backend ubuntu_backend
backend ubuntu_backend
balance roundrobin
option httpchk GET /status
server ubuntu1 192.168.1.10:80 check
server ubuntu2 192.168.1.11:80 check backup
backend centos_backend
balance roundrobin
option httpchk GET /status
server centos1 192.168.1.10:80 check
server centos2 192.168.1.11:80 check backup
backend docker_backend
balance roundrobin
option httpchk GET /v2/
server docker1 192.168.1.10:5000 check
server docker2 192.168.1.11:5000 check backup
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUEKeepalived for HA
# Install Keepalived
sudo apt install -y keepalived
# /etc/keepalived/keepalived.conf
vrrp_script chk_haproxy {
script "/bin/kill -0 `cat /var/run/haproxy.pid`"
interval 2
weight 2
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 101
advert_int 1
authentication {
auth_type PASS
auth_pass 1111
}
virtual_ipaddress {
192.168.1.100
}
track_script {
chk_haproxy
}
}Monitoring and Alerting
Prometheus Configuration
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['localhost:9100']
- job_name: 'nginx'
static_configs:
- targets: ['localhost:9113']
- job_name: 'haproxy'
static_configs:
- targets: ['localhost:8404']
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093Alert Rules
# /etc/prometheus/mirror_rules.yml
groups:
- name: mirror_alerts
rules:
- alert: HighDiskUsage
expr: (node_filesystem_size_bytes{mountpoint="/data"} - node_filesystem_free_bytes{mountpoint="/data"}) / node_filesystem_size_bytes{mountpoint="/data"} * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "磁盘使用率过高"
description: "镜像存储磁盘使用率超过85%"
- alert: ServiceDown
expr: up == 0
for: 2m
labels:
severity: critical
annotations:
summary: "服务不可用"
description: "{{ $labels.instance }} 服务已停止"
- alert: HighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 5m
labels:
severity: warning
annotations:
summary: "内存使用率过高"
description: "内存使用率超过90%"
- alert: SyncJobFailed
expr: increase(sync_job_failures_total[1h]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "镜像同步失败"
description: "镜像同步任务执行失败"Grafana Dashboard (JSON snippet)
{"dashboard":{"id":null,"title":"Linux Mirror Repository Dashboard","tags":["mirror","linux"],"timezone":"browser","panels":[{"title":"磁盘使用率","type":"stat","targets":[{"expr":"(node_filesystem_size_bytes{mountpoint=\"/data\"} - node_filesystem_free_bytes{mountpoint=\"/data\"}) / node_filesystem_size_bytes{mountpoint=\"/data\"} * 100","legendFormat":"磁盘使用率"}],"fieldConfig":{"defaults":{"unit":"percent","thresholds":{"steps":[{"color":"green","value":null},{"color":"yellow","value":70},{"color":"red","value":85}]}}}}],"time":{"from":"now-1h","to":"now"},"refresh":"30s"}}Security Hardening
SSL/TLS
# Generate self‑signed certificate
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/mirror.key \
-out /etc/ssl/certs/mirror.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=Company/CN=mirror.example.com"
# Combine for HAProxy
sudo cat /etc/ssl/certs/mirror.crt /etc/ssl/private/mirror.key > /etc/ssl/certs/mirror.pemAccess Control (Nginx)
# IP whitelist
geo $allowed_ip {
default 0;
192.168.0.0/16 1;
10.0.0.0/8 1;
172.16.0.0/12 1;
}
server {
listen 80;
server_name mirror.example.com;
if ($allowed_ip = 0) { return 403; }
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn conn_limit_per_ip 10;
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=10r/s;
limit_req zone=req_limit_per_ip burst=20 nodelay;
location / {
auth_basic "Private Mirror";
auth_basic_user_file /etc/nginx/.htpasswd;
}
}Firewall (UFW)
# Default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH
sudo ufw allow ssh
# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow internal network
sudo ufw allow from 192.168.0.0/16 to any port 80
sudo ufw allow from 10.0.0.0/8 to any port 80
# Enable
sudo ufw enableTroubleshooting
Common Issues
Sync failures – check network connectivity, disk space, permissions, and view sync logs.
Service access problems – verify Nginx status, configuration syntax, port listening, and firewall rules.
Performance bottlenecks – monitor system load, network traffic, and disk I/O.
Health‑Check Script
# /data/mirrors/scripts/health-check.sh
#!/bin/bash
SERVICES=("nginx" "docker")
LOG_FILE="/data/mirrors/logs/health-check.log"
log(){ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; }
check_service(){
local service="$1"
if systemctl is-active --quiet "$service"; then
log "$service service running"
else
log "$service service down, attempting restart"
systemctl restart "$service"
sleep 5
if systemctl is-active --quiet "$service"; then
log "$service restarted successfully"
else
log "$service restart failed"
fi
fi
}
check_disk_space(){
local usage=$(df /data/mirrors | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$usage" -gt 85 ]; then
log "Disk space low: ${usage}%"
else
log "Disk space OK: ${usage}%"
fi
}
main(){
local failed=0
for service in "${SERVICES[@]}"; do
check_service "$service" || ((failed++))
done
check_disk_space || ((failed++))
if curl -s --max-time 10 http://localhost/status > /dev/null; then
:
else
log "Web service check failed"
((failed++))
fi
if [ $failed -eq 0 ]; then
log "All checks passed"
else
log "$failed checks failed"
fi
}
main "$@"Recovery Script
# /data/mirrors/scripts/recovery.sh
#!/bin/bash
SERVICES=("nginx" "docker" "haproxy")
BACKUP_DIR="/data/backup"
recover_services(){
for service in "${SERVICES[@]}"; do
if ! systemctl is-active --quiet "$service"; then
echo "Recovering $service"
systemctl restart "$service"
sleep 5
fi
done
}
recover_configs(){
if [ -d "$BACKUP_DIR" ]; then
echo "Restoring configs..."
if [ -f "$BACKUP_DIR/nginx.conf" ]; then
cp "$BACKUP_DIR/nginx.conf" /etc/nginx/nginx.conf && nginx -t && systemctl reload nginx
fi
if [ -f "$BACKUP_DIR/haproxy.cfg" ]; then
cp "$BACKUP_DIR/haproxy.cfg" /etc/haproxy/haproxy.cfg && systemctl reload haproxy
fi
fi
}
check_data_integrity(){
if [ -f "/data/mirrors/ubuntu/mirror/dists/jammy/Release" ]; then
echo "Ubuntu mirror OK"
else
echo "Ubuntu mirror corrupted, resyncing"
/data/mirrors/scripts/sync-ubuntu.sh
fi
if [ -f "/data/mirrors/centos/8-stream/baseos/repodata/repomd.xml" ]; then
echo "CentOS mirror OK"
else
echo "CentOS mirror corrupted, resyncing"
/data/mirrors/scripts/sync-centos.sh
fi
}
main(){
echo "Starting recovery..."
recover_services
recover_configs
check_data_integrity
echo "Recovery complete"
}
main "$@"Conclusion
By following this comprehensive guide you can deploy a full‑featured private Linux mirror system that supports Ubuntu, CentOS, and Docker images, provides high availability with HAProxy and Keepalived, offers monitoring and alerting via Prometheus and Grafana, and includes security hardening, automated sync, and robust troubleshooting procedures.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
