#!/usr/bin/env python3 """ Docker Manager Application This script manages Docker containers defined in subdirectories of `~/docker/`. Each subdirectory is treated as a project and must contain a `docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, or `compose.yaml`. Usage Examples: 1. List all running containers: $ dm list 2. Update a specific project (pulls latest images and recreates containers): $ dm update media-server 3. Stop all projects (with confirmation prompt): $ dm stop 4. Restart all projects without confirmation: $ dm restart --all 5. Stop a specific project: $ dm stop web-app Purpose: - Simplifies management of multiple Docker Compose projects. - Provides a unified interface for common operations (list, stop, update, restart). - Eliminates the need to navigate to specific directories to run docker commands. To make the command available as dm from any directory, run: chmod +x ~/shell/docker-manager/docker-manager.py mkdir -p ~/.local/bin ln -s ~/shell/docker-manager/docker-manager.py ~/.local/bin/dm """ import os import argparse import subprocess from pathlib import Path # Configuration DOCKER_ROOT = Path(os.path.expanduser("~/docker")) def get_projects(): """Scan DOCKER_ROOT for subdirectories with compose files.""" projects = {} if not DOCKER_ROOT.exists(): print(f"Error: Directory {DOCKER_ROOT} does not exist.") return projects for item in DOCKER_ROOT.iterdir(): if item.is_dir(): if (item / "docker-compose.yml").exists() or \ (item / "docker-compose.yaml").exists() or \ (item / "compose.yml").exists() or \ (item / "compose.yaml").exists(): projects[item.name] = item return projects def run_command(cmd, cwd, capture_output=False): """Run a shell command in a specific directory.""" try: result = subprocess.run( cmd, cwd=cwd, check=True, text=True, capture_output=capture_output ) return result except subprocess.CalledProcessError as e: if capture_output: return e print(f"Error running command: {' '.join(cmd)}") print(f"cwd: {cwd}") return None def list_containers(projects): """List running containers for all projects.""" print(f"{'Project':<20} | {'Container Name':<40} | {'State':<10}") print("-" * 75) found_any = False for name, path in sorted(projects.items()): # Get running container names cmd = ["docker", "compose", "ps", "--format", "{{.Names}}", "--filter", "status=running"] res = run_command(cmd, path, capture_output=True) if res and res.returncode == 0: containers = res.stdout.strip().split('\n') containers = [c for c in containers if c] # filter empty if containers: found_any = True for container in containers: print(f"{name:<20} | {container:<40} | Running") if not found_any: print("No running containers found in managed projects.") def is_project_running(path): """Check if any containers in the project are running.""" cmd = ["docker", "compose", "ps", "--format", "json", "--filter", "status=running"] res = run_command(cmd, path, capture_output=True) if res and res.returncode == 0: # If output is not empty/just brackets, something is running output = res.stdout.strip() return output and output != "[]" return False def manage_project(projects, action, target, extra_args=None): """ Execute the specified action (stop, update, restart, logs) on target project(s). Args: projects (dict): Dictionary of project names to their paths. action (str): The action to perform ('stop', 'update', 'restart', 'logs'). target (str): The target project name or 'all'. extra_args (list): Additional arguments for the command (e.g. for logs). """ targets = [] # Determine which projects to target if target == "all": targets = sorted(projects.items()) elif target in projects: targets = [(target, projects[target])] else: print(f"Error: Project '{target}' not found in {DOCKER_ROOT}") print("Available projects:", ", ".join(sorted(projects.keys()))) return # Logs are special, usually run on one project interactively if action == "logs": if len(targets) > 1: print("Error: Logs can only be viewed for one project at a time.") return name, path = targets[0] print(f"Viewing logs for {name}...") cmd = ["docker", "compose", "logs"] + (extra_args or []) # For logs, we want to stream output directly to stdout, so no capture_output # and we might want to allow Ctrl+C to exit gracefully try: subprocess.run(cmd, cwd=path, check=False) except KeyboardInterrupt: print("\nLog view stopped.") return print(f"Performing '{action}' on {len(targets)} project(s)...") for name, path in targets: print(f"\n[{name}] -> {action}...") # Execute the requested action if action == "stop": # Stop containers without removing them run_command(["docker", "compose", "stop"], path) elif action == "restart": # Restart containers run_command(["docker", "compose", "restart"], path) elif action == "update": # Update process: Check running -> Stop -> Pull -> Start if was running was_running = is_project_running(path) if was_running: print(f" Stopping running containers for {name}...") run_command(["docker", "compose", "stop"], path) print(f" Pulling latest images for {name}...") pull_res = run_command(["docker", "compose", "pull"], path) if pull_res and pull_res.returncode == 0: if was_running: print(f" Restarting containers for {name}...") run_command(["docker", "compose", "up", "-d"], path) else: print( f" Project {name} was not running. Images updated, but not started.") else: print(f" Failed to pull images for {name}. Skipping update.") def main(): """Main entry point for the application.""" parser = argparse.ArgumentParser( description="Manage Docker containers in ~/docker/") subparsers = parser.add_subparsers( dest="command", help="Command to execute") # List command configuration subparsers.add_parser("list", help="List running containers") # Stop command configuration stop_parser = subparsers.add_parser("stop", help="Stop containers") stop_parser.add_argument("project", nargs="?", default=None, help="Project name (optional). If omitted, asks for confirmation to stop ALL.") stop_parser.add_argument("--all", action="store_true", help="Stop all projects without confirmation prompt if specified") # Update command configuration update_parser = subparsers.add_parser( "update", help="Pull and update containers") update_parser.add_argument("project", nargs="?", default=None, help="Project name (optional). If omitted, asks for confirmation to update ALL.") update_parser.add_argument("--all", action="store_true", help="Update all projects without confirmation prompt if specified") # Restart command configuration restart_parser = subparsers.add_parser( "restart", help="Restart containers") restart_parser.add_argument("project", nargs="?", default=None, help="Project name (optional). If omitted, asks for confirmation to restart ALL.") restart_parser.add_argument("--all", action="store_true", help="Restart all projects without confirmation prompt if specified") # Logs command configuration logs_parser = subparsers.add_parser("logs", help="View container logs") logs_parser.add_argument("project", help="Project name to view logs for") logs_parser.add_argument( "-f", "--follow", action="store_true", help="Follow log output") logs_parser.add_argument( "--tail", default="all", help="Number of lines to show from the end of the logs (default: all)") args = parser.parse_args() # Scan for projects projects = get_projects() if not projects: print(f"No docker projects found in {DOCKER_ROOT}") return # Dispatch commands if args.command == "list": list_containers(projects) elif args.command == "logs": # Prepare extra args for logs extra_args = [] if args.follow: extra_args.append("-f") if args.tail: extra_args.extend(["--tail", args.tail]) manage_project(projects, "logs", args.project, extra_args=extra_args) elif args.command in ["stop", "update", "restart"]: target = args.project # Handle "all" logic safely with user confirmation if target is None: if args.all: target = "all" else: # Interactive check if user wants to apply to all print( f"No project specified. Do you want to {args.command} ALL projects? (y/N)") choice = input().lower() if choice == 'y': target = "all" else: print("Operation cancelled. Specify a project name or use --all.") return manage_project(projects, args.command, target) else: # Show help if no command provided parser.print_help() if __name__ == "__main__": main()