Add Docker application manager script for Debian VPS

This commit is contained in:
jeanotx32
2026-05-18 21:59:59 -04:00
parent ecebb45e0e
commit e7df09d72e

253
manage_vps.py Normal file
View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Gestionnaire d'applications Docker sur VPS Debian.
Autodétecte les applications sous /home/*/docker-compose.yml
"""
import os
import subprocess
import sys
from pathlib import Path
APPS_BASE_DIR = "/home"
COMPOSE_FILES = ["docker-compose.yml", "docker-compose.yaml"]
def find_docker_apps() -> list[dict]:
"""Détecte les applications Docker sous /home/[nom_app]/."""
apps = []
base = Path(APPS_BASE_DIR)
for entry in sorted(base.iterdir()):
if not entry.is_dir():
continue
for compose_file in COMPOSE_FILES:
compose_path = entry / compose_file
if compose_path.exists():
apps.append({
"name": entry.name,
"path": str(entry),
"compose_file": str(compose_path),
})
break
return apps
def get_running_containers(app: dict) -> list[str]:
"""Retourne les noms des conteneurs en cours d'exécution pour une app."""
try:
result = subprocess.run(
["docker", "compose", "-f", app["compose_file"], "ps", "--services", "--filter", "status=running"],
capture_output=True,
text=True,
cwd=app["path"],
)
services = [s.strip() for s in result.stdout.splitlines() if s.strip()]
return services
except FileNotFoundError:
return []
def get_container_id(app: dict, service: str) -> str | None:
"""Retourne l'ID du conteneur Docker pour un service donné."""
try:
result = subprocess.run(
["docker", "compose", "-f", app["compose_file"], "ps", "-q", service],
capture_output=True,
text=True,
cwd=app["path"],
)
container_id = result.stdout.strip()
return container_id if container_id else None
except FileNotFoundError:
return None
def print_separator():
print("-" * 50)
def print_header():
print("=" * 50)
print(" Gestionnaire Docker VPS")
print("=" * 50)
def select_from_list(items: list[str], prompt: str) -> int | None:
"""Affiche une liste numérotée et retourne l'index choisi, ou None si annulé."""
for i, item in enumerate(items, 1):
print(f" [{i}] {item}")
print(" [0] Retour")
print()
while True:
try:
choice = input(f"{prompt}: ").strip()
if choice == "0":
return None
idx = int(choice) - 1
if 0 <= idx < len(items):
return idx
print(f" Choix invalide (1-{len(items)} ou 0 pour retour).")
except (ValueError, EOFError):
print(" Entrée invalide.")
def action_bash(app: dict, service: str):
"""Ouvre un shell bash interactif dans le conteneur."""
container_id = get_container_id(app, service)
if not container_id:
print(f" Conteneur '{service}' introuvable ou arrêté.")
input(" Appuyez sur Entrée pour continuer...")
return
print(f"\n Connexion bash → {app['name']} / {service}")
print(" (tapez 'exit' pour revenir au menu)\n")
subprocess.run(["docker", "exec", "-it", container_id, "bash"])
def action_logs(app: dict, service: str):
"""Affiche les logs du conteneur (100 dernières lignes, avec suivi)."""
container_id = get_container_id(app, service)
if not container_id:
print(f" Conteneur '{service}' introuvable ou arrêté.")
input(" Appuyez sur Entrée pour continuer...")
return
print(f"\n Logs → {app['name']} / {service}")
print(" (Ctrl+C pour arrêter le suivi)\n")
try:
subprocess.run(["docker", "logs", "--tail", "100", "-f", container_id])
except KeyboardInterrupt:
print("\n Suivi des logs interrompu.")
def action_update(app: dict):
"""Met à jour l'application avec docker compose pull puis up -d."""
print(f"\n Mise à jour de '{app['name']}'...")
print_separator()
print(" → docker compose pull")
result_pull = subprocess.run(
["docker", "compose", "-f", app["compose_file"], "pull"],
cwd=app["path"],
)
if result_pull.returncode != 0:
print(" Erreur lors du pull.")
input(" Appuyez sur Entrée pour continuer...")
return
print("\n → docker compose up -d")
result_up = subprocess.run(
["docker", "compose", "-f", app["compose_file"], "up", "-d"],
cwd=app["path"],
)
if result_up.returncode == 0:
print("\n Mise à jour terminée avec succès.")
else:
print("\n Erreur lors du redémarrage des conteneurs.")
input("\n Appuyez sur Entrée pour continuer...")
def app_menu(app: dict):
"""Menu des actions disponibles pour une application."""
while True:
os.system("clear")
print_header()
print(f" Application : {app['name']}")
print(f" Répertoire : {app['path']}")
print_separator()
running = get_running_containers(app)
if running:
print(f" Services actifs : {', '.join(running)}")
else:
print(" Aucun service actif détecté.")
print()
print(" [1] Ouvrir un shell bash dans un conteneur")
print(" [2] Voir les logs d'un conteneur")
print(" [3] Mettre à jour (docker compose pull + up -d)")
print(" [0] Retour")
print()
choice = input(" Votre choix : ").strip()
if choice == "0":
break
elif choice in ("1", "2"):
os.system("clear")
print_header()
label = "shell bash" if choice == "1" else "logs"
print(f" Choisir le service ({label}) :")
print_separator()
all_services = get_all_services(app)
if not all_services:
print(" Aucun service trouvé dans le compose.")
input(" Appuyez sur Entrée pour continuer...")
continue
idx = select_from_list(all_services, " Service")
if idx is None:
continue
service = all_services[idx]
if choice == "1":
action_bash(app, service)
else:
action_logs(app, service)
elif choice == "3":
action_update(app)
else:
print(" Choix invalide.")
def get_all_services(app: dict) -> list[str]:
"""Retourne tous les services définis dans le docker-compose (actifs ou non)."""
try:
result = subprocess.run(
["docker", "compose", "-f", app["compose_file"], "config", "--services"],
capture_output=True,
text=True,
cwd=app["path"],
)
services = [s.strip() for s in result.stdout.splitlines() if s.strip()]
return services
except FileNotFoundError:
return []
def main():
if os.geteuid() != 0:
print("Attention : ce script nécessite des droits root pour interagir avec Docker.")
print("Relancez avec : sudo python3 manage_vps.py\n")
while True:
os.system("clear")
print_header()
apps = find_docker_apps()
if not apps:
print(f" Aucune application Docker trouvée sous {APPS_BASE_DIR}/")
print(" (recherche de docker-compose.yml dans chaque sous-dossier)")
sys.exit(0)
print(f" {len(apps)} application(s) détectée(s) :\n")
app_names = [a["name"] for a in apps]
idx = select_from_list(app_names, " Choisir une application")
if idx is None:
print("\n Au revoir !")
sys.exit(0)
app_menu(apps[idx])
if __name__ == "__main__":
main()