Хочете свій локальний APT-репозиторій для Debian/Ubuntu, де всі пакети під рукою і без зайвого трафіку? З reprepro це реально просто, а завдяки systemd timers можна автоматично чистити старі версії пакетів і тримати репозиторій в ідеальному стані. Усе робимо в термінал Linux, акуратно і без магії 🪄

Що будемо робити

Короткий план: встановлюємо reprepro, створюємо структуру репозиторію, генеруємо GPG-ключ, налаштовуємо конфіг, додаємо .deb-пакети, публікуємо через HTTP (nginx опціонально) та налаштовуємо автоматичну ротацію через cron та systemd timers. По дорозі використаємо зручні apt команди і трохи bash скрипти.

Вимоги та підготовка

Підійде будь-який сервер чи VM з Debian/Ubuntu (або інший сумісний сервер Linux). Потрібні права sudo та відкритий TCP-порт 80 (якщо публікуєте через HTTP).

sudo apt update
sudo apt install -y reprepro gnupg nginx curl

Налаштування reprepro

Створюємо каталоги

sudo mkdir -p /srv/apt-repo/{conf,dists,pool}
sudo chown -R "$USER":"$USER" /srv/apt-repo

Генеруємо GPG-ключ для підпису

Ключ буде використовуватися для підпису метаданих репозиторію. Його публічну частину потім додамо на клієнти.

gpg --quick-generate-key "Local Repo <repo@example.com>" default default never
# Подивитися ключі та забрати KEYID
KEYID=$(gpg --list-keys --with-colons | awk -F: '/^pub:/ {print $5; exit}')
echo "Ваш KEYID: $KEYID"

Файли конфігурації reprepro

Далі опишемо дистрибутив (codename — наприклад, bookworm для Debian 12 або jammy для Ubuntu 22.04).

cat <<'EOF' | sed "s/@KEYID@/$KEYID/" | sudo tee /srv/apt-repo/conf/distributions > /dev/null
Origin: Local
Label: Local
Suite: stable
Codename: bookworm
Architectures: amd64 source
Components: main
SignWith: @KEYID@
Description: Local APT repo (bookworm)
EOF

cat <<'EOF' | sudo tee /srv/apt-repo/conf/options > /dev/null
basedir /srv/apt-repo
verbose
ask-passphrase
EOF

Додаємо пакети до репозиторію

Скопіюйте свої .deb у зручне місце і включайте їх за допомогою reprepro. Після кожного додавання репозиторій оновлюється автоматично.

# приклад: додаємо пакет для bookworm
reprepro -b /srv/apt-repo includedeb bookworm /path/to/package_1.0.0_amd64.deb

Публікація через HTTP (nginx, опціонально)

Найпростіше — віддати репозиторій статично через nginx. Зробимо symlink у веб-каталог і невеличкий сайт.

sudo ln -s /srv/apt-repo /var/www/html/repo

cat <<'EOF' | sudo tee /etc/nginx/sites-available/localrepo > /dev/null
server {
    listen 80;
    server_name _;
    access_log /var/log/nginx/localrepo.access.log;
    error_log  /var/log/nginx/localrepo.error.log;
    location /repo/ {
        autoindex on;
        alias /var/www/html/repo/;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/localrepo /etc/nginx/sites-enabled/localrepo
sudo nginx -t && sudo systemctl reload nginx

Підключення клієнтів до вашого репозиторію

Експортуємо публічний ключ і додамо його на клієнтах з опцією signed-by (найкраща практика безпечних apt команди).

# на сервері: покласти ключ у веб-каталог
gpg --export -a "$KEYID" | sudo tee /var/www/html/repo/KEY.gpg > /dev/null

# на клієнті:
sudo curl -fsSL http://<SERVER-IP>/repo/KEY.gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/localrepo.gpg

echo "deb [signed-by=/usr/share/keyrings/localrepo.gpg] http://<SERVER-IP>/repo bookworm main" \
  | sudo tee /etc/apt/sources.list.d/localrepo.list

sudo apt update
sudo apt install your-package-name

Автоматична ротація старих пакетів через systemd timers

Репозиторії ростуть швидко. Збережемо лише N останніх версій кожного пакета й прибиратимемо інші. Для цього зробимо невеликий bash-скрипт і таймер systemd. Це куди надійніше, ніж cron, бо має журналювання і облік станів.

Скрипт ротації

sudo tee /usr/local/bin/repo-rotate.sh > /dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
BASE="/srv/apt-repo"
DIST="bookworm"
KEEP_N=3
LOGTAG="repo-rotate"

# Отримати список "pkg version" для поточного DIST (без source рядків)
mapfile -t entries < <(reprepro -b "$BASE" list "$DIST" | awk -F': ' '{print $2}' | awk '{print $1, $2}' | sed '/^$/d')

# Побудувати асоціативний масив pkg -> версії
declare -A pkg_versions
for line in "${entries[@]}"; do
  pkg=$(awk '{print $1}' <<<"$line")
  ver=$(awk '{print $2}' <<<"$line")
  pkg_versions[$pkg]="${pkg_versions[$pkg]:-} $ver"
done

removed=0
for pkg in "${!pkg_versions[@]}"; do
  # Відсортувати за версіями (спадно), залишити перші KEEP_N
  all_vers=$(echo "${pkg_versions[$pkg]}" | xargs -n1 | sort -Vr || true)
  keep=$(echo "$all_vers" | head -n "$KEEP_N")
  drop=$(comm -23 <(echo "$all_vers" | sort -V) <(echo "$keep" | sort -V) || true)

  while read -r ver; do
    [[ -z "$ver" ]] && continue
    # Видалити конкретну версію пакета з дистрибутива
    if reprepro -b "$BASE" remove "$DIST" "${pkg}=$ver"; then
      logger -t "$LOGTAG" "removed $pkg=$ver"
      removed=$((removed+1))
    fi
  done < <(echo "$drop")

done

# Прибрати нереференсні файли з pool/
reprepro -b "$BASE" deleteunreferenced || true
logger -t "$LOGTAG" "rotation done; versions removed: $removed"
EOF

sudo chmod +x /usr/local/bin/repo-rotate.sh

Юніт і таймер systemd

# Сервіс
sudo tee /etc/systemd/system/repo-rotate.service > /dev/null <<'EOF'
[Unit]
Description=Rotate old packages in local APT repo
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/repo-rotate.sh
User=root
Group=root
Nice=10
EOF

# Таймер (щодня о 03:17)
sudo tee /etc/systemd/system/repo-rotate.timer > /dev/null <<'EOF'
[Unit]
Description=Daily rotation for local APT repo

[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
RandomizedDelaySec=5m

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now repo-rotate.timer
systemctl list-timers --all | grep repo-rotate || true

Альтернативні способи

  • Інструмент aptly — потужний конкурент reprepro з власними механізмами snapshots та публікації. Якщо плануєте складні сценарії, розгляньте його.
  • Замість systemd timers можна використати cron, але я раджу timers заради логів і гнучкості.
  • Публікація без nginx: достатньо віддати /srv/apt-repo через будь-який статичний сервер або навіть через Python http.server у тестових умовах.

GUI-спосіб (якщо не любите суцільний CLI)

  • Утиліта Seahorse допоможе керувати GPG-ключами графічно: створіть ключ, експортуйте публічну частину як KEY.gpg.
  • Через файловий менеджер можна просто копіювати .deb у підготовлену папку і запускати включення пакетів командою в термінал Linux.
  • Для моніторингу доступу з клієнтів зручно скористатися веб-логами в лог-в’юверах типу gnome-logs.

FAQ

Чому клієнт каже, що репозиторій не підписаний?

Перевірте, що ви експортували правильний публічний ключ і джерело у .list використовує опцію signed-by з шляхом до ключа. Також упевніться, що SignWith у distributions вказує коректний KEYID.

Помилка 403 у клієнта

Перевірте права на /srv/apt-repo і що nginx має доступ до файлів (читайте error.log). Symlink має вести в коректний каталог і не блокуватися SELinux/AppArmor.

Клієнт каже «Release file is not valid»

Невірна комбінація Suite/Codename або зіпсутий підпис. Заново запустіть включення пакета або явно виконайте reprepro -b /srv/apt-repo export і перевірте, що ключ існує та доступний gpg.

Як додати пакети для іншої версії, наприклад jammy?

Створіть ще один блок у conf/distributions з Codename: jammy і повторіть включення: reprepro includedeb jammy your.deb.

Скрипт ротації видаляє «замало» або «забагато»

Відкоригуйте KEEP_N. Якщо формати версій дуже кастомні, замініть sort -V на порівняння через dpkg --compare-versions у власній функції.

Чи можна зберігати джерельні пакети?

Так, додайте Architectures: source і використовуйте reprepro includedsc для .dsc. Скрипт ротації можна розширити, щоб обробляти й source.

Чи варто ставити пароль на GPG-ключ?

Так, бажано. Тоді при підписі буде запитуватися пароль (або використовуйте gpg-agent). У варіантах CI/CD — окремий менш привілейований ключ.

Порада від Kernelka

Тримайте Codename і Suite узгодженими: якщо у клієнта «bookworm», то й у conf/distributions має бути Codename: bookworm. А ще — версії з префіксами типу 1.0.0~rc1 будуть сортуватися інакше; перевірте свою стратегічку ротації на тестовому репо перед продом 🚀

Підсумок

  • Встановили reprepro і підготували структуру репозиторію.
  • Згенерували GPG-ключ та налаштували підпис.
  • Додали .deb пакети й опційно видали репозиторій через nginx.
  • Налаштували автоматичну ротацію старих версій через systemd timers.
  • Підключили клієнтів через signed-by і перевірили apt update/apt install.