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)¶
Contenu typique — remplacez repo.example.com par votre domaine ou IP réel :
# 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)¶
Générer les secrets requis (un openssl rand -hex 32 par secret) :
# ── 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 :
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 :
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 :
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 :
- Provisionner la base — PostgreSQL 16 ou supérieur, une base
repodet un rôle disposant de tous les privilèges dessus, joignable depuis le containerbackend-apisur le réseau (même hôte, réseau privé ou VPN — ne jamais exposer PostgreSQL directement sur internet). - Éditer
docker-compose.yaml: remplacez la ligneDATABASE_URLfigée en dur dans le servicebackendpar votre chaîne de connexion externe (ou supprimez purement cette ligne et laissez leDATABASE_URLdebackend.envs'appliquer, puisque plus rien ne l'écrase alors), et supprimez le bloc du servicedb:— il démarrerait sinon sans être utilisé. - Supprimer la dépendance au service
db— ledepends_on: [db]du servicebackend(et sacondition: service_healthyle cas échéant) doit être retiré en même temps que le blocdb:, sinon Compose refusera de démarrer. - Poursuivre normalement avec l'Étape 3 ci-dessous — la création du
schéma (migrations Alembic, exécutées automatiquement par
entrypoint.shau 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¶
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é :
Surveiller les logs de démarrage :
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.
- Ouvrir
http://VOTRE_HOTE:3003dans un navigateur - Se connecter avec le compte admin créé via l'assistant de configuration
- Aller dans Paramètres → GPG
- Cliquer sur Générer une clé GPG
- 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: [email protected]
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 :
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
503sur 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 /healthexpose 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 |