Compare commits
No commits in common. "doc-deploiement-dynamic" and "main" have entirely different histories.
doc-deploi
...
main
1 changed files with 177 additions and 88 deletions
|
|
@ -1,16 +1,13 @@
|
||||||
---
|
---
|
||||||
description: Déploiement de dynamic.alpinux.org — application Flask derrière Apache (ISPConfig), Gunicorn, SQLite, AlpID.
|
description: Déploiement de dynamic.alpinux.org — app Flask, Gunicorn, Apache reverse proxy, AlpID OIDC.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Déploiement de dynamic.alpinux.org
|
# Déploiement de dynamic.alpinux.org
|
||||||
|
|
||||||
Le site « apprendre Linux en jouant » : cours, jeux et quiz. Application Flask, publique,
|
Application Flask de quiz interactifs, partiellement publique et partiellement réservée aux membres AlpID.
|
||||||
avec une connexion AlpID pour enregistrer ses scores.
|
|
||||||
|
|
||||||
!!! note "Pour qui ?"
|
!!! note "Pour qui ?"
|
||||||
Les mainteneurs qui ont un accès SSH au serveur. La référence détaillée est le fichier
|
Procédure pour les mainteneurs avec accès SSH au serveur.
|
||||||
`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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -20,122 +17,214 @@ avec une connexion AlpID pour enregistrer ses scores.
|
||||||
Navigateur
|
Navigateur
|
||||||
│ HTTPS
|
│ HTTPS
|
||||||
▼
|
▼
|
||||||
Apache (ISPConfig, proxy, Let's Encrypt)
|
Apache (reverse proxy, SSL)
|
||||||
│ HTTP, 127.0.0.1:5001
|
│ HTTP (port local dédié)
|
||||||
▼
|
▼
|
||||||
Gunicorn (2 workers) — service dynamic-alpinux
|
Gunicorn (2 workers)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Flask (/opt/dynamic-alpinux)
|
Flask app (alpinux-dynamic)
|
||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
SQLite AlpID (OIDC)
|
SQLite AlpID OIDC
|
||||||
/var/lib/dynamic-alpinux/scores.db
|
(scores.db) (alpid.alpinux.org)
|
||||||
```
|
```
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| 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` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mettre à jour le site
|
## Installation (première fois)
|
||||||
|
|
||||||
Toute modification passe par une pull request fusionnée dans `main`. Ensuite, depuis sa
|
### 1. Cloner le dépôt sur le serveur
|
||||||
copie locale du dépôt :
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/deploy.sh -n # quel commit de main va partir
|
ssh <user>@alpinux.org
|
||||||
scripts/deploy.sh # mise en ligne
|
git clone https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-dynamic.git \
|
||||||
|
$APP_DIR
|
||||||
```
|
```
|
||||||
|
|
||||||
Le script envoie `origin/main` sur le serveur, installe les dépendances, redémarre le
|
### 2. Créer l'environnement Python
|
||||||
service et vérifie que les pages répondent.
|
|
||||||
|
|
||||||
!!! info "Pourquoi pas un `git pull` sur le serveur ?"
|
```bash
|
||||||
Le dépôt est privé et le serveur n'a pas d'identifiant pour la forge. Le code part donc
|
cd $APP_DIR
|
||||||
du poste du mainteneur, comme pour static.alpinux.org.
|
python3 -m venv venv
|
||||||
|
venv/bin/pip install -r requirements.txt gunicorn
|
||||||
|
```
|
||||||
|
|
||||||
Les jeux, les cours et les quiz sont lus au démarrage : modifier le contenu demande, comme
|
### 3. Configurer les variables d'environnement
|
||||||
le code, une mise en ligne.
|
|
||||||
|
```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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Gérer le service
|
## 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
|
||||||
|
|
||||||
| Action | Commande |
|
| Action | Commande |
|
||||||
|---|---|
|
|---|---|
|
||||||
| État | `systemctl status dynamic-alpinux` |
|
| Démarrer | `sudo systemctl start dynamic-alpinux` |
|
||||||
|
| Arrêter | `sudo systemctl stop dynamic-alpinux` |
|
||||||
| Redémarrer | `sudo systemctl restart dynamic-alpinux` |
|
| Redémarrer | `sudo systemctl restart dynamic-alpinux` |
|
||||||
| Journal en direct | `sudo journalctl -u dynamic-alpinux -f` |
|
| État | `sudo systemctl status dynamic-alpinux` |
|
||||||
| Tester sans Apache | `curl -sI http://127.0.0.1:5001/jeux/` (sur le serveur) |
|
| Logs en direct | `sudo journalctl -u dynamic-alpinux -f` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Le site dans ISPConfig
|
## Structure de l'application
|
||||||
|
|
||||||
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 :
|
alpinux-dynamic/
|
||||||
|
├── app.py ← point d'entrée Flask
|
||||||
```apache
|
├── auth_utils.py ← décorateur @login_required
|
||||||
ProxyPass /.well-known/ !
|
├── db.py ← SQLite (scores)
|
||||||
ProxyPreserveHost On
|
├── quiz.py ← chargement du JSON
|
||||||
ProxyPass / http://127.0.0.1:5001/
|
├── requirements.txt
|
||||||
ProxyPassReverse / http://127.0.0.1:5001/
|
├── .env.example
|
||||||
|
├── data/
|
||||||
RequestHeader set X-Forwarded-Proto "https"
|
│ └── quizzes.json ← toutes les questions (source de vérité)
|
||||||
RequestHeader set X-Forwarded-For "%{REMOTE_ADDR}s"
|
├── 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## La connexion AlpID
|
## Accès public vs membres
|
||||||
|
|
||||||
L'application utilise le realm `master` d'AlpID, comme admin.alpinux.org. Son client
|
| URL | Accès |
|
||||||
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 |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| Quiz | `data/quizzes.json` |
|
| `/` | Public |
|
||||||
| Jeu | une entrée dans `data/jeux.json`, et `data/jeux/<id>.json` |
|
| `/quiz/` | Public (aperçu de tous les quiz) |
|
||||||
| Cours | `data/cours/<id>.md` |
|
| `/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) |
|
||||||
|
|
||||||
Puis pull request, fusion, et `scripts/deploy.sh`.
|
Les quiz avancés (niveau 4) et experts (niveau 5) ont `"members_only": true` dans `data/quizzes.json`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Réinstaller de zéro
|
## Ajouter un quiz
|
||||||
|
|
||||||
Le détail, commande par commande, est dans `DEPLOIEMENT.md`. Dans l'ordre :
|
Éditez `data/quizzes.json` et ajoutez un objet au tableau :
|
||||||
|
|
||||||
1. créer l'utilisateur système `dynamic-alpinux`, les dossiers `/opt/dynamic-alpinux` et
|
```json
|
||||||
`/var/lib/dynamic-alpinux`, et le `venv` ;
|
{
|
||||||
2. écrire `/opt/dynamic-alpinux/.env` (droits `600`) ;
|
"id": "mon-quiz",
|
||||||
3. installer l'unité systemd, dont une copie de référence est dans `deploiement/` ;
|
"title": "Titre du quiz",
|
||||||
4. lancer `scripts/deploy.sh` depuis sa copie locale ;
|
"description": "Description courte.",
|
||||||
5. créer le site dans ISPConfig, avec Let's Encrypt et les directives ci-dessus.
|
"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
|
||||||
|
```
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue