Déploiement de dynamic.alpinux.org : décrire ce qui tourne réellement
Installation faite le 22/10/2026 sur owni : /opt/dynamic-alpinux, service dynamic-alpinux sur 127.0.0.1:5001, configuration dans .env, mise en ligne par scripts/deploy.sh (dépôt privé, pas de git pull sur le serveur). Ajoute l'état de la connexion AlpID (client Keycloak à créer) et renvoie au DEPLOIEMENT.md du dépôt pour le détail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019V4mhmHoJD3b8YfQy249dC
This commit is contained in:
parent
f0884d9a21
commit
4ff7390d6b
1 changed files with 88 additions and 177 deletions
|
|
@ -1,13 +1,16 @@
|
|||
---
|
||||
description: Déploiement de dynamic.alpinux.org — app Flask, Gunicorn, Apache reverse proxy, AlpID OIDC.
|
||||
description: Déploiement de dynamic.alpinux.org — application Flask derrière Apache (ISPConfig), Gunicorn, SQLite, AlpID.
|
||||
---
|
||||
|
||||
# Déploiement de dynamic.alpinux.org
|
||||
|
||||
Application Flask de quiz interactifs, partiellement publique et partiellement réservée aux membres AlpID.
|
||||
Le site « apprendre Linux en jouant » : cours, jeux et quiz. Application Flask, publique,
|
||||
avec une connexion AlpID pour enregistrer ses scores.
|
||||
|
||||
!!! note "Pour qui ?"
|
||||
Procédure pour les mainteneurs avec accès SSH au serveur.
|
||||
Les mainteneurs qui ont un accès SSH au serveur. La référence détaillée est le fichier
|
||||
`DEPLOIEMENT.md` du dépôt [alpinux-dynamic](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-dynamic),
|
||||
tenu à jour avec le code ; cette page en donne l'essentiel.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -17,214 +20,122 @@ Application Flask de quiz interactifs, partiellement publique et partiellement r
|
|||
Navigateur
|
||||
│ HTTPS
|
||||
▼
|
||||
Apache (reverse proxy, SSL)
|
||||
│ HTTP (port local dédié)
|
||||
Apache (ISPConfig, proxy, Let's Encrypt)
|
||||
│ HTTP, 127.0.0.1:5001
|
||||
▼
|
||||
Gunicorn (2 workers)
|
||||
Gunicorn (2 workers) — service dynamic-alpinux
|
||||
│
|
||||
▼
|
||||
Flask app (alpinux-dynamic)
|
||||
Flask (/opt/dynamic-alpinux)
|
||||
│ │
|
||||
▼ ▼
|
||||
SQLite AlpID OIDC
|
||||
(scores.db) (alpid.alpinux.org)
|
||||
SQLite AlpID (OIDC)
|
||||
/var/lib/dynamic-alpinux/scores.db
|
||||
```
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Code | `/opt/dynamic-alpinux`, utilisateur système `dynamic-alpinux` |
|
||||
| Service | `dynamic-alpinux`, Gunicorn sur `127.0.0.1:5001` |
|
||||
| Configuration | `/opt/dynamic-alpinux/.env` (droits `600`) |
|
||||
| Base de données | `/var/lib/dynamic-alpinux/scores.db` |
|
||||
| Journaux | `sudo journalctl -u dynamic-alpinux` |
|
||||
|
||||
---
|
||||
|
||||
## Installation (première fois)
|
||||
## Mettre à jour le site
|
||||
|
||||
### 1. Cloner le dépôt sur le serveur
|
||||
Toute modification passe par une pull request fusionnée dans `main`. Ensuite, depuis sa
|
||||
copie locale du dépôt :
|
||||
|
||||
```bash
|
||||
ssh <user>@alpinux.org
|
||||
git clone https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-dynamic.git \
|
||||
$APP_DIR
|
||||
scripts/deploy.sh -n # quel commit de main va partir
|
||||
scripts/deploy.sh # mise en ligne
|
||||
```
|
||||
|
||||
### 2. Créer l'environnement Python
|
||||
Le script envoie `origin/main` sur le serveur, installe les dépendances, redémarre le
|
||||
service et vérifie que les pages répondent.
|
||||
|
||||
```bash
|
||||
cd $APP_DIR
|
||||
python3 -m venv venv
|
||||
venv/bin/pip install -r requirements.txt gunicorn
|
||||
```
|
||||
!!! info "Pourquoi pas un `git pull` sur le serveur ?"
|
||||
Le dépôt est privé et le serveur n'a pas d'identifiant pour la forge. Le code part donc
|
||||
du poste du mainteneur, comme pour static.alpinux.org.
|
||||
|
||||
### 3. Configurer les variables d'environnement
|
||||
|
||||
```bash
|
||||
sudo mkdir /etc/dynamic-alpinux
|
||||
sudo cp $APP_DIR/.env.example /etc/dynamic-alpinux/config.env
|
||||
sudo nano /etc/dynamic-alpinux/config.env
|
||||
```
|
||||
|
||||
Remplissez les valeurs :
|
||||
|
||||
```bash
|
||||
SECRET_KEY=<générer avec : python3 -c "import secrets; print(secrets.token_hex(32))">
|
||||
ALPID_CLIENT_ID=<client-id-configuré-dans-keycloak>
|
||||
ALPID_CLIENT_SECRET=<obtenir depuis la console Keycloak AlpID>
|
||||
ALPID_DISCOVERY_URL=https://alpid.alpinux.org/realms/alpinux/.well-known/openid-configuration
|
||||
DATABASE=<chemin vers le fichier scores.db>
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo chmod 600 /etc/dynamic-alpinux/config.env
|
||||
```
|
||||
|
||||
### 4. Créer les répertoires de données et de logs
|
||||
|
||||
```bash
|
||||
sudo mkdir -p <répertoire données>
|
||||
sudo mkdir -p <répertoire logs>
|
||||
sudo chown <user>:<group> <répertoire données> <répertoire logs>
|
||||
```
|
||||
|
||||
### 5. Configurer AlpID (Keycloak)
|
||||
|
||||
Dans la console d'administration Keycloak (`https://alpid.alpinux.org`) :
|
||||
|
||||
1. **Clients** → **Créer un client**
|
||||
2. **Client ID** : le client ID choisi pour cette app
|
||||
3. **Client authentication** : activé (pour obtenir un `client_secret`)
|
||||
4. **Valid redirect URIs** : `https://dynamic.alpinux.org/auth/callback`
|
||||
5. **Web origins** : `https://dynamic.alpinux.org`
|
||||
6. Notez le **Client secret** dans l'onglet *Credentials*
|
||||
|
||||
### 6. Installer le service systemd
|
||||
|
||||
Les fichiers de configuration systemd et Apache sont conservés **hors dépôt** (`infra/` local).
|
||||
|
||||
```bash
|
||||
sudo cp infra/services/dynamic.alpinux.org.service \
|
||||
/etc/systemd/system/dynamic-alpinux.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now dynamic-alpinux
|
||||
sudo systemctl status dynamic-alpinux
|
||||
```
|
||||
|
||||
### 7. Configurer le site dans ISPConfig
|
||||
|
||||
Dans **ISPConfig** (`https://owni.alpinux.org:8080`) :
|
||||
|
||||
1. *Sites → Ajouter un site web* — domaine `dynamic.alpinux.org`
|
||||
2. Activer **Let's Encrypt SSL** dans l'onglet SSL
|
||||
3. Dans *Options → Directives Apache personnalisées (HTTPS)*, ajouter :
|
||||
|
||||
```apache
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://127.0.0.1:<port>/
|
||||
ProxyPassReverse / http://127.0.0.1:<port>/
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
```
|
||||
|
||||
ISPConfig gère le VirtualHost et le renouvellement automatique du certificat.
|
||||
Les jeux, les cours et les quiz sont lus au démarrage : modifier le contenu demande, comme
|
||||
le code, une mise en ligne.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour
|
||||
|
||||
```bash
|
||||
ssh <user>@alpinux.org
|
||||
cd $APP_DIR
|
||||
git pull
|
||||
venv/bin/pip install -r requirements.txt # si requirements.txt a changé
|
||||
sudo systemctl restart dynamic-alpinux
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gestion du service
|
||||
## Gérer le service
|
||||
|
||||
| Action | Commande |
|
||||
|---|---|
|
||||
| Démarrer | `sudo systemctl start dynamic-alpinux` |
|
||||
| Arrêter | `sudo systemctl stop dynamic-alpinux` |
|
||||
| État | `systemctl status dynamic-alpinux` |
|
||||
| Redémarrer | `sudo systemctl restart dynamic-alpinux` |
|
||||
| État | `sudo systemctl status dynamic-alpinux` |
|
||||
| Logs en direct | `sudo journalctl -u dynamic-alpinux -f` |
|
||||
| Journal en direct | `sudo journalctl -u dynamic-alpinux -f` |
|
||||
| Tester sans Apache | `curl -sI http://127.0.0.1:5001/jeux/` (sur le serveur) |
|
||||
|
||||
---
|
||||
|
||||
## Structure de l'application
|
||||
## Le site dans ISPConfig
|
||||
|
||||
Le proxy se déclare dans *Sites → dynamic.alpinux.org → Options → Directives Apache*, et
|
||||
pas dans le fichier `.vhost`, qu'ISPConfig régénère :
|
||||
|
||||
```apache
|
||||
ProxyPass /.well-known/ !
|
||||
ProxyPreserveHost On
|
||||
ProxyPass / http://127.0.0.1:5001/
|
||||
ProxyPassReverse / http://127.0.0.1:5001/
|
||||
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
|
||||
```
|
||||
alpinux-dynamic/
|
||||
├── app.py ← point d'entrée Flask
|
||||
├── auth_utils.py ← décorateur @login_required
|
||||
├── db.py ← SQLite (scores)
|
||||
├── quiz.py ← chargement du JSON
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
├── data/
|
||||
│ └── quizzes.json ← toutes les questions (source de vérité)
|
||||
├── routes/
|
||||
│ ├── public.py ← accueil, liste, jeu, résultat
|
||||
│ ├── auth.py ← /auth/login, /auth/callback, /auth/logout
|
||||
│ └── protected.py ← /profil/
|
||||
├── static/
|
||||
│ ├── style.css
|
||||
│ └── quiz.js
|
||||
└── templates/
|
||||
├── base.html
|
||||
├── index.html
|
||||
├── quiz/
|
||||
│ ├── intro.html
|
||||
│ ├── play.html
|
||||
│ └── result.html
|
||||
└── profil/
|
||||
└── index.html
|
||||
```
|
||||
|
||||
La première ligne laisse passer le renouvellement du certificat Let's Encrypt.
|
||||
|
||||
---
|
||||
|
||||
## Accès public vs membres
|
||||
## La connexion AlpID
|
||||
|
||||
| URL | Accès |
|
||||
L'application utilise le realm `master` d'AlpID, comme admin.alpinux.org. Son client
|
||||
Keycloak, `dynamic`, se crée dans la console d'AlpID :
|
||||
|
||||
1. *Clients → Create client*, **Client ID** `dynamic` ;
|
||||
2. **Client authentication** activé ;
|
||||
3. **Valid redirect URIs** : `https://dynamic.alpinux.org/auth/callback` ;
|
||||
4. **Web origins** : `https://dynamic.alpinux.org` ;
|
||||
5. le secret de l'onglet *Credentials* va dans `ALPID_CLIENT_SECRET`, dans
|
||||
`/opt/dynamic-alpinux/.env`, puis on redémarre le service.
|
||||
|
||||
!!! warning "État au 22/10/2026"
|
||||
Le client n'est pas encore créé : les cours, les jeux et les quiz publics fonctionnent,
|
||||
mais pas la connexion. Il faut aussi que l'application tienne compte du proxy HTTPS
|
||||
(`ProxyFix`) avant de l'ouvrir, sinon AlpID refuse l'adresse de retour.
|
||||
|
||||
---
|
||||
|
||||
## Ajouter du contenu
|
||||
|
||||
Les procédures pour ajouter un jeu, un cours ou un quiz sont dans le `README.md` du dépôt.
|
||||
En résumé :
|
||||
|
||||
| Contenu | Fichiers |
|
||||
|---|---|
|
||||
| `/` | Public |
|
||||
| `/quiz/` | Public (aperçu de tous les quiz) |
|
||||
| `/quiz/<id>/` | Public (page intro) |
|
||||
| `/quiz/<id>/jouer` | Public si `members_only: false`, sinon AlpID requis |
|
||||
| `/profil/` | AlpID requis |
|
||||
| `/auth/login` | Redirect → AlpID |
|
||||
| `/auth/callback` | Retour OIDC (interne) |
|
||||
| Quiz | `data/quizzes.json` |
|
||||
| Jeu | une entrée dans `data/jeux.json`, et `data/jeux/<id>.json` |
|
||||
| Cours | `data/cours/<id>.md` |
|
||||
|
||||
Les quiz avancés (niveau 4) et experts (niveau 5) ont `"members_only": true` dans `data/quizzes.json`.
|
||||
Puis pull request, fusion, et `scripts/deploy.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Ajouter un quiz
|
||||
## Réinstaller de zéro
|
||||
|
||||
Éditez `data/quizzes.json` et ajoutez un objet au tableau :
|
||||
Le détail, commande par commande, est dans `DEPLOIEMENT.md`. Dans l'ordre :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "mon-quiz",
|
||||
"title": "Titre du quiz",
|
||||
"description": "Description courte.",
|
||||
"level": "Intermédiaire",
|
||||
"level_id": 3,
|
||||
"members_only": false,
|
||||
"duration_min": 5,
|
||||
"icon": "🐧",
|
||||
"questions": [
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Question ?",
|
||||
"choices": ["Réponse A", "Réponse B", "Réponse C", "Réponse D"],
|
||||
"answer": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `level_id` : 1=Découverte, 2=Débutant, 3=Intermédiaire, 4=Avancé, 5=Expert
|
||||
- `answer` : index 0-based de la bonne réponse dans `choices`
|
||||
- `members_only` : `true` pour restreindre aux membres AlpID
|
||||
|
||||
Après modification, redémarrez le service pour recharger le JSON :
|
||||
|
||||
```bash
|
||||
sudo systemctl restart dynamic-alpinux
|
||||
```
|
||||
1. créer l'utilisateur système `dynamic-alpinux`, les dossiers `/opt/dynamic-alpinux` et
|
||||
`/var/lib/dynamic-alpinux`, et le `venv` ;
|
||||
2. écrire `/opt/dynamic-alpinux/.env` (droits `600`) ;
|
||||
3. installer l'unité systemd, dont une copie de référence est dans `deploiement/` ;
|
||||
4. lancer `scripts/deploy.sh` depuis sa copie locale ;
|
||||
5. créer le site dans ISPConfig, avec Let's Encrypt et les directives ci-dessus.
|
||||
|
|
|
|||
Loading…
Reference in a new issue