import os import subprocess import tarfile import tempfile import shutil import yaml from datetime import datetime from pathlib import Path def is_valid_compose_project(): result = subprocess.run( ["docker", "compose", "config"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) return result.returncode == 0 def run_cmd(cmd, capture_output=False): result = subprocess.run(cmd, shell=True, text=True, capture_output=capture_output) if result.returncode != 0: print(f"[!] Error while running command: {cmd}") print(result.stderr) raise RuntimeError("Command failed with return code:", result.returncode) return result.stdout if capture_output else None def get_project_name(): return Path(os.getcwd()).name def get_volumes(): print("[*] Determine Volumes with `docker compose config`...") output = run_cmd("docker compose config", capture_output=True) output = yaml.safe_load(output) volumes = [] for logical_name, attrs in output.get("volumes", {}).items(): actual_name = attrs.get("name", logical_name) volumes.append(actual_name) return volumes def archive_project(output_dir): print("[*] Archiving Project folder") with tarfile.open(output_dir / "project_files.tar.gz", "w:gz") as tar: for item in Path(".").iterdir(): if item.name in [output_dir.name, ".git", "__pycache__"]: continue tar.add(item, arcname=item.name) def archive_volume(volume_name, backup_path): print(f"[*] Archiving Volume: {volume_name}") archive_file = backup_path / f"{volume_name}.tar.gz" cmd = ( f"docker run --rm " f"-v {volume_name}:/volume " f"-v {backup_path.absolute()}:/backup " f"alpine tar czf /backup/{volume_name}.tar.gz -C /volume ." ) run_cmd(cmd) return archive_file def main(): if is_valid_compose_project(): print("[*] Valid Compose Project Found") else: print("[*] No Valid Compose Project Found") return return project_name = get_project_name() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") final_archive_name = f"{project_name}_backup_{timestamp}.tar.gz" # Step 1: Define and create backup target directory backup_root = Path.home() / ".compose-backup" backup_root.mkdir(parents=True, exist_ok=True) final_archive_path = backup_root / final_archive_name with tempfile.TemporaryDirectory() as tmpdir: print("[*] Running docker compose up to create volumes...") run_cmd("docker compose down") tmp_path = Path(tmpdir) archive_project(tmp_path) volumes = get_volumes() for vol in volumes: archive_volume(vol, tmp_path) print(f"[*] Creating full archive: {final_archive_path}") with tarfile.open(final_archive_path, "w:gz") as tar: for file in tmp_path.iterdir(): tar.add(file, arcname=file.name) print(f"[✔] Backup completed successfully. Archive saved as: {final_archive_path}") if __name__ == "__main__": main()