← posts
homelab · linux

Cómo hice mi propio mirror de Arch Linux

Hace un año y medio que uso Arch Linux como daily driver y me di cuenta que cada vez que actualizaba mi sistema las descargas eran lentas y tardaban bastante, investigando observé que no había ningún mirror en mi país (Argentina). Así que decidí, ya teniendo la infra necesaria para exponer un servicio a internet, crear uno propio.

En base a la wik de Arch y el post de Sindastra, creé mi propio Mirror. Actualmente es el único de Argentina y bastante usado en la región. Estas son las estadísticas del dia de ayer, la cuales se actualizan diariamente en base a los logs de Nginx:

cargando estadísticas...

Prerrequisitos

Para poder usar este post como guia, debes tener las siguientes cosas:

  • Un servidor con aproximadamente 300Gib de almacenamiento disponibles.
  • Una instancia de NPM funcionando.
  • Crear el directorio

    Lo primero es crear el directorio donde van a vivir los archivos del mirror en el servidor:

    mkdir -p /var/www/mirrors/archlinux/

    Elegir un mirror Tier 1

    Como puede observarse en la wiki de Arch Linux, los mirrors se manejan por tiers, cuando se empieza vamos a ser un tier 2 que es de donde la gente actualiza sus sistemas. A su vez nosotros deberemos sincronizar nuestro mirror desde un tier 1, ya que para facilitar las sincronizaciónes Arch pide que no sincronices directamente desde sus servidores. Debés elegir uno Tier 1 de esta lista:

    https://archlinux.org/mirrors/tier/1/

    Deberás tener en cuenta varios factores, como cercanía geografica, velocidad y uptime del Tier 1

    Primera sincronización

    La primera sync tarda bastante (en mi caso como 8hs) y ocupa aproximadamente 200 GiB. Recomiendo correrlo en tmux o usar un script junto a un cron para que lo haga en segundo plano. Arch solicita que usemos flags de rsync para realizar la sincronización, el comando completo sería el siguiente:

    rsync -rlptH --safe-links --delete-delay --delay-updates   rsync://TU_MIRROR_TIER1/archlinux/   /var/www/mirrors/archlinux/

    Sincronizaciones

    Yo decidí desarrollar un script (con la ayuda de Claude), para realizar las sincronizaciónes.

    #!/usr/bin/env bash
    # archmirror-sync.sh Sincroniza el mirror de Arch Linux con log completo.
    
    DEST="/var/www/mirrors/archlinux"
    SOURCE="rsync://archlinux.c3sl.ufpr.br/archlinux/"
    LOG_DIR="/var/log/archmirror"
    LOG_FILE="$LOG_DIR/sync.log"
    LOCK_FILE="/var/run/archmirror-sync.lock"
    MAX_LOG_MB=50          # rotar log si supera este tamaño en MB
    RSYNC_TIMEOUT=2700    # timeout máximo de la operación en segundos. En mi primer syn puse 10hs
    
    log() {
        local level="$1"; shift
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
    }
    
    rotate_log() {
        if [[ -f "$LOG_FILE" ]]; then
            local size_mb
            size_mb=$(du -m "$LOG_FILE" | cut -f1)
            if (( size_mb >= MAX_LOG_MB )); then
                mv "$LOG_FILE" "${LOG_FILE}.$(date '+%Y%m%d_%H%M%S').old"
                log "INFO" "Log rotado por tamaño (${size_mb}MB >= ${MAX_LOG_MB}MB)"
            fi
        fi
    }
    
    cleanup() {
        rm -f "$LOCK_FILE"
    }
    
    mkdir -p "$LOG_DIR"
    rotate_log
    
    if [[ -f "$LOCK_FILE" ]]; then
        old_pid=$(cat "$LOCK_FILE")
        if kill -0 "$old_pid" 2>/dev/null; then
            log "WARN" "Sync ya en ejecución (PID $old_pid). Abortando."
            exit 1
        else
            log "WARN" "Lock obsoleto encontrado (PID $old_pid ya no existe). Limpiando."
            rm -f "$LOCK_FILE"
        fi
    fi
    
    echo $$ > "$LOCK_FILE"
    trap cleanup EXIT
    
    log "INFO" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    log "INFO" "Iniciando sync | PID=$$"
    log "INFO" "Fuente:  $SOURCE"
    log "INFO" "Destino: $DEST"
    START_TIME=$(date +%s)
    
    timeout "$RSYNC_TIMEOUT" rsync \
        --recursive \
        --links \
        --perms \
        --times \
        --hard-links \
        --safe-links \
        --delete-delay \
        --delay-updates \
        --timeout=120 \
        --contimeout=30 \
        --info=progress2 \
        --log-file="$LOG_FILE" \
        --log-file-format="[%t] [RSYNC] %o %f %l bytes" \
        "$SOURCE" "$DEST" 2>&1 | while IFS= read -r line; do
            # Filtrar líneas vacías y de progreso puro (no las guardamos en el log)
            [[ -z "$line" || "$line" =~ ^[[:space:]]+[0-9] ]] && continue
            log "RSYNC" "$line"
        done
    
    RSYNC_EXIT="${PIPESTATUS[0]}"
    
    END_TIME=$(date +%s)
    ELAPSED=$(( END_TIME - START_TIME ))
    ELAPSED_FMT=$(printf '%02d:%02d:%02d' $((ELAPSED/3600)) $((ELAPSED%3600/60)) $((ELAPSED%60)))
    
    case "$RSYNC_EXIT" in
        0)
            log "INFO" "Sync completado OK en $ELAPSED_FMT"
            ;;
        24)
            # Código 24 = archivos desaparecieron durante la transferencia (normal)
            log "WARN" "Sync completado con advertencias (código 24 — archivos modificados durante sync) en $ELAPSED_FMT"
            ;;
        28)
            log "ERROR" "Sync interrumpido por timeout ($RSYNC_TIMEOUT segundos)"
            ;;
        *)
            log "ERROR" "Sync falló con código de salida $RSYNC_EXIT en $ELAPSED_FMT"
            ;;
    esac
    
    # Tamaño del mirror
    if [[ -d "$DEST" ]]; then
        MIRROR_SIZE=$(du -sh "$DEST" 2>/dev/null | cut -f1)
        log "INFO" "Tamaño del mirror: $MIRROR_SIZE"
    fi
    
    log "INFO" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
    exit "$RSYNC_EXIT"
    

    Sync automático con cron

    Cabe destacar que arch solicita que sincronicemos al menos una vez al día, pero no más de una vez por hora, estas nuevas sincronizaciónes tardarán MUCHO menos que la primera (entre 8 y 25 mins). En mi caso lo hago una vez por hora. A su vez, debido a que estamos generando logs habrá que eliminarlos cada X tiempo para que no nos llene el File System. Esta es mi configuración de Cron:

    #Sincronizo los repos
    21 */1 * * * /bin/bash /root/archmirror-sync.sh 
    
    #Elimino logs viejos:
    45 1 * * 0 find /var/log/archmirror -name "*.old" -mtime +30 -delete

    Configurar nginx

    Para poder exponer el mirror instalaremos los siguientes paquetes:

    sudo apt install nginx libnginx-mod-http-fancyindex

    Luego deberemos configurar nginx editando el archivo /etc/nginx/sites-available/tu-web, y pondremos la siguiente configuración:

    #Archlinux
    server {
        listen 80;
        listen [::]:80;
    	server_name archlinux.{TUDOMINIO.COM};
        root /var/www/mirrors/archlinux;
    
        #Oculta la versión de Nginx actual (por seguridad)
        server_tokens off;
    
        #OPCIONAL: Si Queres personalizar fancy Index, crea esas rutas y modifica el headery footer:
        #location = /header.html {
        #    root /var/www/mirrors/personalizar;
        #}
        #location = /footer.html {
        #    root /var/www/mirrors/personalizar;
        #}
    		
        #Unicamente permito metodos GET y HEAD
        location / {
            limit_except GET HEAD {
                deny all;
            }
    
            try_files $uri $uri/ =404;
            fancyindex on;
            fancyindex_exact_size off;
            fancyindex_localtime on;
            fancyindex_directories_first on;
    		
    	#opcionales - solo si se personalizo
            #fancyindex_header "/header.html";
            #fancyindex_footer "/footer.html";
        }
    
        
    }
    No configuro HTTPS recomiendo manejarlo con NPM

    Como exponerlo a Internet - SelfHosting

    Acá tendremos que tomar una decisión. Queremos exponerlo usando HTTP, HTTPS o ambas?

    Para esta seccion, usaremos un reverse proxy (NPM en mi caso), lo que debemos hacer es hacer un port forwarding de puertos en nuestro router domestico hacia la direccion IP de NPM. En base a la decisión que tomamos anteriormente, hacemos un port forwarding del puerto 80 (HTTP), 443 (HTTPS) o ambos. Luego en NPM manejamos otra redireccion hacia el servidor que aloja el mirror.

    Yo lo manejo de esta forma porque me permite redirigir el trafico a un servidor o a otro dependiendo de la URL que se consulto. Ademas, NPM me prmite forzar SSL/TLS.

    Esta seccion solo es util si decidiste hostearlo en tu casa y si usas un Reverse Proxy

    Registro oficial en archlinux.org

    Una vez que tengamos todo listo y funcionando - y en caso de que queramos - podemos oficializar el mirror para que aparezca en la lista de mirrors oficiales de Arch. Para esto seguimos los pasos detallados en la wiki, primero tenemos que crearon una cuenta en el gitlab de Arch y esperar que nos las aprueben, después debemos crear un nuevo issue y poner los datos que no solicitan en el punto 4.4 de la wiki; https://wiki.archlinux.org/title/DeveloperWiki:NewMirrors

    Mejora Continua: Monitoreo con alertas de Discord

    Hace unos días el mirror estaba presentando bajas velocidades de descarga, estamos hablando del rango de los Kbs (KiloBytes x Segundo), lo que generaba timeouts y que mi mirror no pueda ser utilizado.

    Posterior a arreglar el issue, que para el que le interese era un problema sobre como pfsense (mi router) maneja las sesiones, procedí a crear un script que se encarga de verificar que los archivos del mirror se puedan descargar y que este esté actualizado, en caso de que no, genera alertas por Discord.

    #!/bin/bash
    MIRROR="https://archlinux.juancho.com.ar"
    DISCORD_WEBHOOK="{}"
    TMPDIR=$(mktemp -d)
    STATE_FILE="/var/tmp/mirror-state"
    LOG_FILE="/var/log/archmirror/mirror-check.log"
    
    log() {
        echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
    }
    
    notify() {
        curl -s -H "Content-Type: application/json" \
            -d "{\"embeds\": [{
                \"title\": \"$1\",
                \"description\": \"$2\",
                \"color\": $3,
                \"footer\": {\"text\": \"$(date)\"}
            }]}" \
            "$DISCORD_WEBHOOK"
        log "NOTIFY: $1 - $2"
    }
    
    mkdir -p /var/log/archmirror
    PREV_STATE=$(cat $STATE_FILE 2>/dev/null || echo "ok")
    CURRENT_STATE="ok"
    log "=== Iniciando chequeo ==="
    
    # 1. HTTP check
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$MIRROR/lastsync")
    log "HTTP check: $HTTP_CODE"
    if [ "$HTTP_CODE" != "200" ]; then
        notify "❌ Mirror caído" "HTTP $HTTP_CODE en \`$MIRROR\`" 15158332
        echo "down" > $STATE_FILE
        rm -rf $TMPDIR; exit 1
    fi
    
    # 2. Lastsync viejo
    LASTSYNC=$(curl -s "$MIRROR/lastsync")
    NOW=$(date +%s)
    AGE=$((NOW - LASTSYNC))
    log "Lastsync age: ${AGE}s ($((AGE/3600))h)"
    if [ $AGE -gt 7200 ]; then
        notify "⚠️ Mirror desactualizado" "Última sync hace **$((AGE/3600))h**" 16776960
        CURRENT_STATE="warn"
    fi
    
    # 3. Descargar y verificar core.db
    log "Descargando core.db..."
    curl -s --max-time 30 -o "$TMPDIR/core.db" "$MIRROR/core/os/x86_64/core.db"
    if ! file "$TMPDIR/core.db" | grep -q "gzip"; then
        notify "❌ core.db corrupto" "El archivo descargado no es un gzip válido" 15158332
        log "ERROR: core.db no es gzip válido"
        CURRENT_STATE="down"
        rm -rf $TMPDIR; exit 1
    fi
    if ! tar -tzf "$TMPDIR/core.db" > /dev/null 2>&1; then
        notify "❌ core.db no extraíble" "El archivo está corrupto o incompleto" 15158332
        log "ERROR: core.db no se puede extraer"
        CURRENT_STATE="down"
        rm -rf $TMPDIR; exit 1
    fi
    log "core.db OK"
    
    # 4. Comparar con upstream
    UPSTREAM_SYNC=$(curl -s "https://geo.mirror.pkgbuild.com/lastsync")
    DIFF=$((UPSTREAM_SYNC - LASTSYNC))
    log "Diff con upstream: ${DIFF}s ($((DIFF/3600))h)"
    if [ $DIFF -gt 7200 ]; then
        notify "⚠️ Mirror atrasado" "**$((DIFF/3600))h** detrás del upstream" 16776960
        CURRENT_STATE="warn"
    fi
    
    # 5. Notificar recuperación
    if [ "$PREV_STATE" = "down" ] && [ "$CURRENT_STATE" = "ok" ]; then
        notify "✅ Mirror recuperado" "\`$MIRROR\` volvió a funcionar correctamente" 3066993
    fi
    
    echo $CURRENT_STATE > $STATE_FILE
    log "Estado final: $CURRENT_STATE"
    rm -rf $TMPDIR
    

    Resultados

    Esto nos deja un mirror con una copia de todo Arch Linux, 100% funcional y utilizable a traves de HTTP/HTTPS. Una vez que te aprueban oficialmente en Arch tu mirror aparecerá en la lista de disponibles para tu país y podrá ser rateado por la herramineta reflector. Como puede observarse, hice una clasificación de los mirrors de Aregntina, Brasil y Chile:

    
      17:17  juan  ~   ❯ reflector --country Argentina,Chile,Brazil --age 12 --protocol http --protocol https --sort rate --verbose
    [2026-06-17 17:18:34] INFO: rating 15 mirror(s) by download speed
    [2026-06-17 17:18:34] INFO: Server                                              Rate       Time
    [2026-06-17 17:18:34] INFO: http://archlinux.c3sl.ufpr.br/            18725.09 KiB/s     0.45 s
    [2026-06-17 17:18:35] INFO: https://archlinux.c3sl.ufpr.br/           15468.44 KiB/s     0.55 s
    [2026-06-17 17:18:35] INFO: http://br.mirrors.cicku.me/archlinux/     29214.14 KiB/s     0.29 s
    [2026-06-17 17:18:36] INFO: https://br.mirrors.cicku.me/archlinux/    29849.56 KiB/s     0.28 s
    [2026-06-17 17:18:36] INFO: http://mirror.ufscar.br/archlinux/        18610.91 KiB/s     0.45 s
    [2026-06-17 17:18:37] INFO: https://mirror.ufscar.br/archlinux/       14398.79 KiB/s     0.59 s
    [2026-06-17 17:18:38] INFO: http://mirror.anquan.cl/archlinux/         9694.44 KiB/s     0.87 s
    [2026-06-17 17:18:39] INFO: https://mirror.anquan.cl/archlinux/        9619.29 KiB/s     0.88 s
    [2026-06-17 17:18:39] INFO: http://mirror.hnd.cl/archlinux/           21537.76 KiB/s     0.39 s
    [2026-06-17 17:18:40] INFO: https://mirror.hnd.cl/archlinux/          24368.97 KiB/s     0.35 s
    [2026-06-17 17:18:40] INFO: http://elmirror.cl/archlinux/             23256.55 KiB/s     0.36 s
    [2026-06-17 17:18:41] INFO: https://elmirror.cl/archlinux/            16708.61 KiB/s     0.51 s
    [2026-06-17 17:18:42] INFO: http://mirrors.ic.unicamp.br/archlinux/   18047.63 KiB/s     0.47 s
    [2026-06-17 17:18:42] INFO: https://mirrors.ic.unicamp.br/archlinux/  18228.03 KiB/s     0.46 s
    [2026-06-17 17:18:42] INFO: https://archlinux.juancho.com.ar/         46606.56 KiB/s     0.18 s
    ################################################################################
    ################# Arch Linux mirrorlist generated by Reflector #################
    ################################################################################
    
    # With:       reflector --country Argentina,Chile,Brazil --age 12 --protocol http --protocol https --sort rate --verbose
    # When:       2026-06-17 20:18:42 UTC
    # From:       https://archlinux.org/mirrors/status/json/
    # Retrieved:  2026-06-17 20:14:19 UTC
    # Last Check: 2026-06-17 19:56:04 UTC
    
    Server = https://archlinux.juancho.com.ar/$repo/os/$arch
    Server = https://br.mirrors.cicku.me/archlinux/$repo/os/$arch
    Server = http://br.mirrors.cicku.me/archlinux/$repo/os/$arch
    Server = https://mirror.hnd.cl/archlinux/$repo/os/$arch
    Server = http://elmirror.cl/archlinux/$repo/os/$arch
    Server = http://mirror.hnd.cl/archlinux/$repo/os/$arch
    Server = http://archlinux.c3sl.ufpr.br/$repo/os/$arch
    Server = http://mirror.ufscar.br/archlinux/$repo/os/$arch
    Server = https://mirrors.ic.unicamp.br/archlinux/$repo/os/$arch
    Server = http://mirrors.ic.unicamp.br/archlinux/$repo/os/$arch
    Server = https://elmirror.cl/archlinux/$repo/os/$arch
    Server = https://archlinux.c3sl.ufpr.br/$repo/os/$arch
    Server = https://mirror.ufscar.br/archlinux/$repo/os/$arch
    Server = http://mirror.anquan.cl/archlinux/$repo/os/$arch
    Server = https://mirror.anquan.cl/archlinux/$repo/os/$arch	

    Además, nuestro mirror aparecerá en la página oficial de Arch:

    Te sirvio?

    Como Contribuir:

    Invitame un café en cafecito.app