first commit
This commit is contained in:
127
README.md
127
README.md
@@ -1,2 +1,127 @@
|
|||||||
# compose-backup
|
|
||||||
|
|
||||||
|
# 🐳 Compose Backup & Restore Tool
|
||||||
|
|
||||||
|
A simple backup & restore tool for Docker Compose projects, including volume data.
|
||||||
|
Ideal for migrating projects or creating point-in-time snapshots of your stack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Features
|
||||||
|
|
||||||
|
- 🔐 Full backup of your Docker Compose project folder
|
||||||
|
- 💾 Archives all named Docker volumes (excluding bind-mounts)
|
||||||
|
- 🧠 Automatic project detection and naming
|
||||||
|
- 🧙 Interactive restore menu with project/version selection
|
||||||
|
- ☁️ Stores backups in `~/.compose-backup`
|
||||||
|
- 🐍 Uses Python + Docker + Bash (no external services)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
compose-backup/
|
||||||
|
├── bin/
|
||||||
|
│ ├── compose-backup # Bash launcher (patched on install)
|
||||||
|
│ └── compose-restore # Bash launcher (patched on install)
|
||||||
|
├── src/
|
||||||
|
│ ├── backup.py # Backup logic
|
||||||
|
│ └── restore.py # Restore logic
|
||||||
|
├── requirements.txt
|
||||||
|
├── install.sh # Install script (creates venv & installs commands)
|
||||||
|
└── README.md
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://your.repo/compose-backup.git
|
||||||
|
cd compose-backup
|
||||||
|
./install.sh
|
||||||
|
````
|
||||||
|
|
||||||
|
This will:
|
||||||
|
|
||||||
|
* Set up a virtual environment in `.venv`
|
||||||
|
* Install required Python packages
|
||||||
|
* Patch & install `compose-backup` and `compose-restore` to `/usr/local/bin`
|
||||||
|
|
||||||
|
> 🔒 You’ll be prompted for your sudo password to copy binaries system-wide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔁 Usage
|
||||||
|
|
||||||
|
### 📤 Backup
|
||||||
|
|
||||||
|
From inside a folder containing your `docker-compose.yml`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose-backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates a full backup (project files + named volumes) into:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.compose-backup/
|
||||||
|
└── myproject_backup_20250711_183012.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📥 Restore
|
||||||
|
|
||||||
|
To restore a backup, anywhere on your system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose-restore
|
||||||
|
```
|
||||||
|
|
||||||
|
* Step 1: Choose project
|
||||||
|
* Step 2: Choose version
|
||||||
|
* Step 3: Project files are restored to your current folder
|
||||||
|
* Step 4: Volumes are restored via temporary containers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧼 Uninstall
|
||||||
|
|
||||||
|
To remove installed commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo rm /usr/local/bin/compose-backup /usr/local/bin/compose-restore
|
||||||
|
```
|
||||||
|
|
||||||
|
(Optional) remove `.venv` and the repo folder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Requirements
|
||||||
|
|
||||||
|
* Docker
|
||||||
|
* Python 3.8+
|
||||||
|
* Linux or macOS (tested on Ubuntu)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Notes
|
||||||
|
|
||||||
|
* Only **named volumes** (declared under `volumes:` in your `docker-compose.yml`) are backed up.
|
||||||
|
* **Bind mounts** like `./data:/app/data` are not included.
|
||||||
|
* The tool does **not** stop running containers before backup — ensure consistency manually if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 License
|
||||||
|
|
||||||
|
MIT — feel free to fork and improve!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💻 Author
|
||||||
|
|
||||||
|
Built by gmarth — Contributions welcome!
|
||||||
|
|||||||
9
bin/compose-backup
Normal file
9
bin/compose-backup
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Absoluter Pfad zum Skriptverzeichnis
|
||||||
|
SCRIPT_DIR="$HOME/scripts/compose-backup/src"
|
||||||
|
# Aktuelles Arbeitsverzeichnis behalten (z.B. ~/docker/portainer)
|
||||||
|
WORKDIR="$(pwd)"
|
||||||
|
# Venv aktivieren
|
||||||
|
source "$SCRIPT_DIR/.venv/bin/activate"
|
||||||
|
# Python-Skript aufrufen – im aktuellen Arbeitsverzeichnis
|
||||||
|
python "$SCRIPT_DIR/src/backup.py" "$@"
|
||||||
9
bin/compose-restore
Normal file
9
bin/compose-restore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Absoluter Pfad zum Skriptverzeichnis
|
||||||
|
SCRIPT_DIR="$HOME/scripts/compose-backup/src"
|
||||||
|
# Aktuelles Arbeitsverzeichnis behalten (z.B. ~/docker/portainer)
|
||||||
|
WORKDIR="$(pwd)"
|
||||||
|
# Venv aktivieren
|
||||||
|
source "$SCRIPT_DIR/.venv/bin/activate"
|
||||||
|
# Python-Skript aufrufen – im aktuellen Arbeitsverzeichnis
|
||||||
|
python "$SCRIPT_DIR/src/restore.py" "$@"
|
||||||
26
install.sh
Normal file
26
install.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[*] Installiere Compose-Backup Toolset..."
|
||||||
|
|
||||||
|
# Absoluter Pfad zum aktuellen Verzeichnis
|
||||||
|
BASE_DIR="$(pwd)"
|
||||||
|
VENV_PATH="$BASE_DIR/.venv"
|
||||||
|
|
||||||
|
echo "[*] Erstelle Virtual Environment unter $VENV_PATH..."
|
||||||
|
python3 -m venv "$VENV_PATH"
|
||||||
|
|
||||||
|
echo "[*] Installiere Python-Abhängigkeiten..."
|
||||||
|
source "$VENV_PATH/bin/activate"
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
echo "[*] Installiere nach $HOME/.local/bin..."
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
sed "s|^SCRIPT_DIR=.*|SCRIPT_DIR=\"$BASE_DIR\"|" bin/compose-backup > "$HOME/.local/bin/compose-backup"
|
||||||
|
sed "s|^SCRIPT_DIR=.*|SCRIPT_DIR=\"$BASE_DIR\"|" bin/compose-restore > "$HOME/.local/bin/compose-restore"
|
||||||
|
chmod +x "$HOME/.local/bin/compose-backup" "$HOME/.local/bin/compose-restore"
|
||||||
|
|
||||||
|
echo "[✔] Installation abgeschlossen!"
|
||||||
|
echo "→ Du kannst nun 'compose-backup' und 'compose-restore' global ausführen."
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pyaml==25.7.0
|
||||||
|
PyYAML==6.0.2
|
||||||
83
src/backup.py
Normal file
83
src/backup.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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"[!] Fehler bei Befehl: {cmd}")
|
||||||
|
print(result.stderr)
|
||||||
|
raise RuntimeError("Befehl fehlgeschlagen")
|
||||||
|
return result.stdout if capture_output else None
|
||||||
|
|
||||||
|
def get_project_name():
|
||||||
|
return Path(os.getcwd()).name
|
||||||
|
|
||||||
|
def get_volumes():
|
||||||
|
print("[*] Ermittele Volumes mit `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) # fallback if "name" not set
|
||||||
|
volumes.append(actual_name)
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
def archive_project(output_dir):
|
||||||
|
print("[*] Archiviere Projektordner...")
|
||||||
|
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"[*] Archiviere 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():
|
||||||
|
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"[*] Erstelle Gesamtarchiv: {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 abgeschlossen: {final_archive_path}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
86
src/restore.py
Normal file
86
src/restore.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BACKUP_DIR = Path.home() / ".compose-backup"
|
||||||
|
|
||||||
|
def run_cmd(cmd):
|
||||||
|
result = subprocess.run(cmd, shell=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Fehler beim Befehl: {cmd}")
|
||||||
|
|
||||||
|
def list_projects():
|
||||||
|
backups = BACKUP_DIR.glob("*.tar.gz")
|
||||||
|
projects = {}
|
||||||
|
|
||||||
|
for archive in backups:
|
||||||
|
name = archive.name.split("_backup_")[0]
|
||||||
|
projects.setdefault(name, []).append(archive)
|
||||||
|
|
||||||
|
return projects
|
||||||
|
|
||||||
|
def select_from_list(prompt, items):
|
||||||
|
for i, item in enumerate(items, 1):
|
||||||
|
print(f"{i}) {item}")
|
||||||
|
while True:
|
||||||
|
choice = input(f"{prompt} [1-{len(items)}]: ")
|
||||||
|
if choice.isdigit() and 1 <= int(choice) <= len(items):
|
||||||
|
return items[int(choice) - 1]
|
||||||
|
|
||||||
|
def restore_project_files(project, tempdir):
|
||||||
|
print("[*] Entpacke Projektdateien...")
|
||||||
|
with tarfile.open(tempdir / "project_files.tar.gz", "r:gz") as tar:
|
||||||
|
tar.extractall(f"./{project}/") # extract to current dir
|
||||||
|
|
||||||
|
def restore_volume(volume_name, archive_path):
|
||||||
|
print(f"[*] Stelle Volume wieder her: {volume_name}")
|
||||||
|
cmd = (
|
||||||
|
f"docker run --rm "
|
||||||
|
f"-v {volume_name}:/volume "
|
||||||
|
f"-v {archive_path.parent.absolute()}:/backup "
|
||||||
|
f"alpine sh -c \"cd /volume && tar xzf /backup/{archive_path.name}\""
|
||||||
|
)
|
||||||
|
run_cmd(cmd)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("📦 Verfügbare Projekte:")
|
||||||
|
projects = list_projects()
|
||||||
|
if not projects:
|
||||||
|
print("Keine Backups gefunden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
project = select_from_list("Projekt auswählen", list(projects.keys()))
|
||||||
|
versions = sorted(projects[project], reverse=True)
|
||||||
|
backup_file = select_from_list("Version auswählen", [p.name for p in versions])
|
||||||
|
backup_path = BACKUP_DIR / backup_file
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
tmp = Path(tmpdir)
|
||||||
|
print(f"[*] Entpacke Backup: {backup_path}")
|
||||||
|
with tarfile.open(backup_path, "r:gz") as tar:
|
||||||
|
tar.extractall(tmp)
|
||||||
|
|
||||||
|
restore_project_files(project, tmp)
|
||||||
|
|
||||||
|
# change into project folder and run the compose project a single time
|
||||||
|
os.chdir(f"{Path.cwd()}/{project}")
|
||||||
|
print("[*] Running docker compose up to create volumes...")
|
||||||
|
run_cmd("docker compose up -d")
|
||||||
|
|
||||||
|
print("[*] Stopping containers after volume creation...")
|
||||||
|
run_cmd("docker compose down")
|
||||||
|
|
||||||
|
|
||||||
|
for file in tmp.iterdir():
|
||||||
|
if file.name == "project_files.tar.gz":
|
||||||
|
continue
|
||||||
|
if file.suffix == ".gz" and file.name.endswith(".tar.gz"):
|
||||||
|
volume_name = file.name.replace(".tar.gz", "")
|
||||||
|
restore_volume(volume_name, file)
|
||||||
|
|
||||||
|
print("✅ Wiederherstellung abgeschlossen.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user