Operations 27 min read

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.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
How to Build a Private Linux Mirror Repository from Scratch – Complete Production Guide

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 + Grafana

APT 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/mirrors

Configure 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/ubuntu

Create 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 -delete

YUM 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 nginx

Create 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 -delete

Docker 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-password

Start 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: bridge

High 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 TRUE

Keepalived 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:9093

Alert 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.pem

Access 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 enable

Troubleshooting

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

APTDocker RegistryyumMirror Repository
MaGe Linux Operations
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.