Aller au contenu

Déploiement en production

Guide pas à pas pour déployer Repod sur un serveur Linux de production.


Prérequis système

Système d'exploitation

  • Debian 11/12 ou Ubuntu 22.04/24.04 LTS (OS hôte — Repod lui-même tourne en conteneurs et peut servir n'importe quelle combinaison d'APT/RPM/APK)
  • Accès root ou sudo

Logiciels requis

Logiciel Version minimale Commande de vérification
Docker Engine 24.0 docker --version
Docker Compose 2.20 (plugin) docker compose version
Git 2.x git --version
OpenSSL 1.1+ openssl version

Utilisez le plugin Compose v2

Utilisez docker compose (plugin v2), pas l'ancienne commande docker-compose.

Installer Docker (si absent)

sudo apt-get remove -y docker docker-engine docker.io containerd runc
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker

Prérequis matériels

Ressource Minimum Recommandé
CPU 2 vCPU 4 vCPU
RAM 3 Go 6 Go+
Disque 20 Go 100 Go+

La RAM couvre PostgreSQL, ClamAV (clamd charge ~800 Mo de signatures dans le conteneur backend) et Grype. L'espace disque doit accueillir tous les binaires de paquets sous repos/pool/ et repos/rpm/, plus le volume Docker postgres_data. Dimensionnez selon le volume de paquets attendu.

Accès Internet

Repod fonctionne entièrement hors ligne par défaut — rien dans le pipeline central upload → scan → publication → distribution n'exige de connectivité sortante. Quelques fonctionnalités optionnelles (mises à jour des bases ClamAV/Grype, enrichissement CVE, import de paquets depuis des sources publiques) sortent effectivement sur Internet ; si le serveur se trouve derrière un accès sortant restreint, consultez Configuration proxy → pour la liste complète des dépendances sortantes et un exemple concret de routage via une liste blanche Squid.


Ports réseau

Le docker-compose.yaml fourni exécute REPO_FORMAT=all par défaut — les serveurs de dépôt APT/APK et RPM sont tous deux démarrés.

Port Service Exposition
80 depot-apt — dépôts APT (.deb) et Alpine (.apk) LAN ou public (clients apt/apk)
8080 depot-rpm — dépôts RPM (.rpm) LAN ou public (clients dnf/zypper)
3003 Interface web (frontend-ui) Interne ou VPN
8000 API backend (backend-api) Reverse proxy uniquement — ne jamais exposer directement
PostgreSQL (repod-db) Réseau Docker interne uniquement, non publié

Si vous n'avez besoin que d'un seul format de paquet, définissez REPO_FORMAT en conséquence (apt, rpm ou apk) et retirez le service de dépôt inutilisé de docker-compose.yaml — voir Démarrage rapide — Étape 2.


Étape 1 — Cloner le dépôt

sudo mkdir -p /opt/repod
sudo chown $USER:$USER /opt/repod
cd /opt/repod

git clone https://github.com/getautoflow/repod .

Vérifier la structure du projet :

ls /opt/repod
# Attendu : backend/ frontend/ repos/ docker-compose.yaml .env.example backend.env.example

Étape 2 — Configurer les variables d'environnement

.env (ports, mot de passe de base de données, et URLs frontend en build-time)

cp .env.example .env
nano .env

Contenu typique — remplacez repo.example.com par votre domaine ou IP réel :

.env
# Se lier au loopback quand un reverse proxy gère le trafic externe
BIND_HOST=127.0.0.1

# URLs publiques intégrées au bundle frontend au moment du build
# REACT_APP_API_URL doit rester vide (appels relatifs /api/v1/...)
REACT_APP_API_URL=
REACT_APP_REPO_URL=https://repo.example.com
REACT_APP_RPM_REPO_URL=https://repo.example.com:8080

# Mappage des ports
BACKEND_PORT=8000
FRONTEND_PORT=3003
APT_PORT=80
RPM_REPO_PORT=8080

# PostgreSQL — doit correspondre à DATABASE_URL dans backend.env
POSTGRES_PASSWORD=<sortie de openssl rand -hex 24>

backend.env (secrets, connexion base de données, et configuration runtime)

cp backend.env.example backend.env

Générer les secrets requis (un openssl rand -hex 32 par secret) :

openssl rand -hex 32
backend.env
# ── Base de données ─────────────────────────────────────────────────────────
# Informatif uniquement — la chaîne de connexion réelle est construite par
# docker-compose.yaml à partir de POSTGRES_PASSWORD dans .env, toujours
# synchronisée avec le vrai mot de passe de la base. Ne définissez pas une
# valeur différente ici.
DATABASE_URL=postgresql://repod:<meme-mot-de-passe-que-.env>@db:5432/repod

# ── Format de dépôt ─────────────────────────────────────────────────────────
# Informatif uniquement — REPO_FORMAT est codé en dur dans docker-compose.yaml
# (service "backend", bloc environment:), pas lu depuis l'un ou l'autre des
# fichiers env. Pour le changer, modifiez directement cette ligne et retirez
# le service de dépôt désormais inutilisé (apt-repo/rpm-repo).
REPO_FORMAT=all

# ── Sécurité ─────────────────────────────────────────────────────────────────
JWT_SECRET_KEY=<sortie de openssl rand -hex 32>
JWT_EXPIRE_MINUTES=60
SETTINGS_ENCRYPTION_KEY=<sortie de openssl rand -hex 32>
WEBHOOK_SECRET=<sortie de openssl rand -hex 32>
CORS_ORIGINS=https://repo.example.com

# ── Environnement ────────────────────────────────────────────────────────────
ENV=production
APP_VERSION=v1.2.0

Sécuriser les fichiers :

chmod 600 /opt/repod/.env
chmod 600 /opt/repod/backend.env

Aucun compte admin n'est créé à ce stade. Une fois la pile démarrée, créez le premier admin via l'assistant de configuration :

curl -X POST http://localhost:8000/api/v1/setup/ \
  -H "Content-Type: application/json" \
  -d '{"admin_username":"admin","admin_password":"VotreMotDePasse!"}'

Pré-provisionner un admin (optionnel, déploiements automatisés)

Définissez ADMIN_USERNAME et ADMIN_PASSWORD_HASH (bcrypt, $ doublé en $$) dans backend.env avant le premier démarrage :

python3 -c "from passlib.hash import bcrypt; print(bcrypt.hash('VotreMotDePasse!'))"

Protéger l'assistant de configuration (optionnel)

Définissez SETUP_TOKEN=<sortie de openssl rand -hex 32> dans backend.env pour exiger un header X-Setup-Token sur POST /api/v1/setup jusqu'à la création du premier admin.

Optionnel — utiliser une base PostgreSQL externe (non conteneurisée)

DATABASE_URL est une simple chaîne de connexion PostgreSQL — rien dans le backend n'exige que la base tourne dans un container Docker. La faire pointer vers une instance PostgreSQL 16+ externe (VM dédiée, cluster HA existant, service de base de données managé) est pleinement supporté, et c'est même la configuration recommandée pour un déploiement on-premise qui dispose déjà de sa propre pratique opérationnelle PostgreSQL — outillage de sauvegarde/PITR dédié, supervision, tuning mémoire/disque et failover, le tout géré en dehors de Docker plutôt que lié à un volume de container. Le service db embarqué (postgres:16-alpine dans docker-compose.yaml) n'existe que comme option pratique sans administration pour les installations mono-nœud ; ce n'est pas une dépendance obligatoire.

Modifier backend.env seul ne suffit pas. Dans le docker-compose.yaml fourni, le bloc environment: du service backend fige DATABASE_URL en dur, dérivé de POSTGRES_PASSWORD dans .env :

docker-compose.yaml (extrait)
services:
  backend:
    environment:
      DATABASE_URL: postgresql://repod:${POSTGRES_PASSWORD:-repod_dev_password}@db:5432/repod

Une entrée environment: d'un service Compose est toujours prioritaire sur la même variable définie via env_file: (backend.env) — donc un DATABASE_URL modifié uniquement dans backend.env est silencieusement écrasé, et le backend continue de se connecter au container db embarqué. Pour utiliser une base externe :

  1. Provisionner la base — PostgreSQL 16 ou supérieur, une base repod et un rôle disposant de tous les privilèges dessus, joignable depuis le container backend-api sur le réseau (même hôte, réseau privé ou VPN — ne jamais exposer PostgreSQL directement sur internet).
  2. Éditer docker-compose.yaml : remplacez la ligne DATABASE_URL figée en dur dans le service backend par votre chaîne de connexion externe (ou supprimez purement cette ligne et laissez le DATABASE_URL de backend.env s'appliquer, puisque plus rien ne l'écrase alors), et supprimez le bloc du service db: — il démarrerait sinon sans être utilisé.
  3. Supprimer la dépendance au service db — le depends_on: [db] du service backend (et sa condition: service_healthy le cas échéant) doit être retiré en même temps que le bloc db:, sinon Compose refusera de démarrer.
  4. Poursuivre normalement avec l'Étape 3 ci-dessous — la création du schéma (migrations Alembic, exécutées automatiquement par entrypoint.sh au premier démarrage) fonctionne à l'identique contre n'importe quelle instance PostgreSQL 16+.

C'est exactement le même prérequis déjà exigé pour la haute disponibilité multi-réplicas — un point de terminaison PostgreSQL externe partagé auquel chaque réplique se connecte. Un déploiement mono-nœud avec une base externe et un déploiement HA multi-réplicas ne diffèrent que par le nombre d'instances backend-api qui pointent vers cette même base.


Étape 3 — Créer la structure des volumes de données

mkdir -p /opt/repod/repos/{audit,auth,backups,certs,clamav-db,conf,db,dists,\
gnupg,grype-db,imports,logs,manifests,maven,npm,package-index,pool,pypi,rpm,\
apk,secrets,security,settings,staging/incoming,staging/quarantine,templates,\
upstream-cache}
Répertoire Contenu
audit/ Journaux d'audit JSONL en écriture seule (un fichier par jour)
auth/ Jetons de réinitialisation de mot de passe
backups/ Archives de sauvegarde planifiées (pg_dump + configuration)
certs/ Certificat TLS auto-signé + CA LDAP, si utilisé
clamav-db/ Base de signatures ClamAV (~800 Mo)
conf/ Configuration des distributions reprepro (mode APT)
db/ Base de données interne reprepro (mode APT)
dists/ Arborescences des distributions APT, servies par depot-apt
rpm/ Arborescences des distributions RPM (<codename>/<arch>/repodata/), servies par depot-rpm
apk/ Dépôts Alpine (<codename>/main/<arch>/APKINDEX.tar.gz), servis par depot-apt sous /apk/
gnupg/ Trousseau GPG partagé entre le backend et les conteneurs de dépôt
grype-db/ Cache de la base CVE Grype
imports/ Répertoire de travail pour les imports de sync/mirror
logs/ Logs de téléchargement Nginx (analysés pour les statistiques)
manifests/ Manifestes JSON par paquet et index.json central
maven/ Artefacts Maven (layout GAV), checksums générés et maven-metadata.xml
npm/ Tarballs npm, par namespace
pool/ Binaires de paquets .deb / .rpm (stockage canonique)
pypi/ Wheels/sdists PyPI, par index
secrets/ Secrets générés automatiquement (clé de signature JWT, etc.) — doit persister, sinon chaque redémarrage les régénère et invalide les sessions existantes
security/ Décisions CVE, caches CISA KEV et EPSS
settings/ Paramètres runtime (settings.json)
staging/ Zone de dépôt des uploads et quarantaine
templates/ Modèles d'e-mails de notification personnalisables
upstream-cache/ Configuration nginx générée + fichiers en cache pour le cache amont

maven/, npm/ et pypi/ ne sont pas créés automatiquement

Contrairement aux répertoires des paquets OS, ces trois-là ne sont provisionnés par aucun code de démarrage — seules leurs permissions sont corrigées si le répertoire existe déjà. Si vous omettez de les créer avant la première utilisation, la publication d'un paquet Maven, PyPI ou npm échouera.

Les comptes utilisateurs, l'index des manifestes, les données d'inventaire et les empreintes de clés SSH hôte ne sont pas stockés sous /repos/ — ils vivent dans PostgreSQL, dans le volume Docker postgres_data géré par le service db. Aucune création manuelle de répertoire n'est nécessaire pour la base de données ; docker compose up crée le volume automatiquement et Alembic exécute les migrations au démarrage du backend.

Les images de conteneurs (OCI) et MinIO ne font pas partie de cette liste

L'overlay optionnel de registre de conteneurs OCI (docker-compose.oci.yml) gère son propre stockage d'images dans le conteneur du registre, pas sous /repos/. Voir Registre de conteneurs.


Étape 4 — Construire et démarrer les services

cd /opt/repod
docker compose up -d --build

Avec REPO_FORMAT=all par défaut, Docker construit et démarre cinq conteneurs :

Conteneur Rôle Port
repod-db PostgreSQL 16 — base de données applicative (interne uniquement)
depot-apt Nginx — dépôts APT (.deb) + Alpine (.apk) 80
depot-rpm Nginx — dépôts RPM (.rpm) 8080
backend-api FastAPI, pipeline de sécurité 8000
frontend-ui Interface web React + Nginx 3003

Vérifier que tous les conteneurs sont démarrés et en bonne santé :

docker compose ps

Surveiller les logs de démarrage :

docker compose logs -f backend-api
# Attendre : INFO:     Application startup complete.

Le premier démarrage est plus lent

ClamAV charge ~800 Mo de signatures au premier démarrage (jusqu'à 2 minutes — le endpoint de santé peut renvoyer "clamav": false jusqu'à ce que ce soit terminé). PostgreSQL effectue également sa propre initialisation initiale et Alembic applique les migrations. C'est normal.


Étape 5 — Configurer la clé de signature GPG

Les index de dépôt doivent être signés GPG pour que les clients APT/RPM/APK puissent vérifier les paquets.

  1. Ouvrir http://VOTRE_HOTE:3003 dans un navigateur
  2. Se connecter avec le compte admin créé via l'assistant de configuration
  3. Aller dans Paramètres → GPG
  4. Cliquer sur Générer une clé GPG
  5. Renseigner le nom réel et l'adresse e-mail, puis cliquer sur Générer
docker exec backend-api gpg --homedir /repos/gnupg \
  --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: Repod Repository
Name-Email: repod@example.com
Expire-Date: 2y
%commit
EOF

# Initialiser les distributions après la génération de la clé
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"VotreMotDePasse!"}' | jq -r .access_token)

curl -X POST http://localhost:8000/api/v1/distributions/init \
  -H "Authorization: Bearer $TOKEN"

Étape 6 — Vérifier l'installation

# 1. Tous les conteneurs démarrés et en bonne santé
docker compose ps

# 2. Sonde de vivacité de l'API
curl -s http://localhost:8000/health/live
# Attendu : {"status":"ok"}

# 3. Vérification de santé complète de l'API (inclut le statut PostgreSQL, ClamAV et Grype)
TOKEN=$(curl -s -X POST http://localhost:8000/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"VotreMotDePasse!"}' | jq -r .access_token)
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/health | jq .

# 4. Interface web accessible
curl -s -o /dev/null -w "%{http_code}" http://localhost:3003
# Attendu : 200

# 5. Serveur de dépôt APT/APK accessible
curl -s -o /dev/null -w "%{http_code}" http://localhost:80
# Attendu : 200

# 6. Serveur de dépôt RPM accessible (REPO_FORMAT=rpm/both/all)
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080
# Attendu : 200

# 7. Swagger UI désactivé en production
curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/docs
# Attendu : 404

Étape 7 — Configurer le pare-feu

sudo ufw enable
sudo ufw allow 22/tcp          # SSH — ne vous verrouillez pas dehors
sudo ufw allow 3003/tcp        # Interface web
sudo ufw allow 80/tcp          # Clients APT / APK
sudo ufw allow 8080/tcp        # Clients RPM (à ignorer si REPO_FORMAT=apt)
# Port 8000 — NE PAS ouvrir publiquement ; utilisez un reverse proxy
sudo ufw status numbered
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-port=3003/tcp   # Interface web
sudo firewall-cmd --permanent --add-port=80/tcp     # Clients APT / APK
sudo firewall-cmd --permanent --add-port=8080/tcp   # Clients RPM
# Port 8000 — NE PAS ouvrir ; utilisez un reverse proxy
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Ne jamais exposer directement le port 8000

L'API backend (port 8000) doit être accessible uniquement via un reverse proxy TLS. Une exposition publique directe transmet identifiants et jetons en clair. Voir Reverse proxy →


Étape 8 — Activer le démarrage automatique via systemd

cat > /etc/systemd/system/repod.service << 'EOF'
[Unit]
Description=Repod Package Repository Manager
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/repod
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable repod
sudo systemctl start repod

Alternative : pile RPM autonome

Si vous n'avez besoin que d'un dépôt RPM et souhaitez qu'il soit entièrement isolé (instance PostgreSQL propre, réseau propre, noms de conteneurs propres — aucun état partagé avec un déploiement APT/mode all), utilisez le fichier compose dédié à la place de l'étape 4 :

docker compose -f docker-compose.rpm.yml up -d --build

Cela démarre repod-db-rpm (PostgreSQL), depot-rpm, backend-api-rpm (REPO_FORMAT=rpm), et frontend-ui-rpm, sur les ports RPM_REPO_PORT (défaut 8080), BACKEND_PORT (défaut 8001), et FRONTEND_PORT (défaut 3004). Exécutez-la en autonome — ne la fusionnez pas avec docker-compose.yaml via -f.


Haute disponibilité (Enterprise)

Pour les déploiements ne pouvant tolérer aucun point de défaillance unique, backend-api peut tourner en plusieurs répliques dans une configuration active/passive derrière un load balancer :

  • Au démarrage, chaque réplique tente d'acquérir un verrou partagé auprès de PostgreSQL. Celle qui réussit devient leader — seul le leader exécute les jobs planifiés (synchronisation de sécurité, sauvegardes, rétention, vérifications SLA, scans de dérive) et accepte les requêtes démarrant un job d'arrière-plan de longue durée (imports, installations, scans d'inventaire). Les répliques passives renvoient 503 sur ces endpoints spécifiques, afin qu'un load balancer ou un client puisse réessayer contre le leader.
  • Le verrou est lié à la propre session de base de données du leader — si ce processus meurt, PostgreSQL libère automatiquement le verrou et une autre réplique l'acquiert. Le failover consiste simplement à redémarrer le conteneur du leader défaillant.
  • GET /health expose quelle réplique est actuellement le leader (checks.info.ha), pour la visibilité opérationnelle.

Cela nécessite un point de terminaison PostgreSQL HA externe (pas le conteneur db mono-nœud du docker-compose.yaml de base) et un système de fichiers partagé en lecture-écriture pour /repos sur toutes les répliques (NFS, EFS, Filestore, ou équivalent) — voir docker-compose.ha.yml pour un overlay de départ documenté.


Checklist post-déploiement

Étape Commande / Fichier Statut
DATABASE_URL configuré, mot de passe correspondant à POSTGRES_PASSWORD backend.env / .env
REPO_FORMAT défini à la valeur souhaitée (apt/rpm/apk/both/all) backend.env
Clé secrète JWT configurée JWT_SECRET_KEY dans backend.env
Clé de chiffrement des paramètres configurée SETTINGS_ENCRYPTION_KEY dans backend.env
Secret webhook configuré WEBHOOK_SECRET dans backend.env
Compte admin créé Assistant de configuration (POST /api/v1/setup) ou ADMIN_PASSWORD_HASH dans backend.env
BIND_HOST restreint BIND_HOST=127.0.0.1 dans .env
Origines CORS définies CORS_ORIGINS=https://… dans backend.env
ENV=production ENV=production dans backend.env
Clé GPG générée Paramètres → GPG dans l'interface web
Pare-feu configuré Port 8000 non exposé publiquement
Reverse proxy avec TLS Voir guide du reverse proxy
Proxy sortant configuré (si l'accès sortant est restreint) Voir guide de configuration proxy
Sauvegardes automatisées Voir guide de sauvegarde
Service systemd activé systemctl enable repod

Étapes suivantes