mirror of
				https://github.com/kevinveenbirkenbach/docker-volume-backup.git
				synced 2025-11-03 18:17:56 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			556cb17433
			...
			18d6136de0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 18d6136de0 | |||
| 3ed89a59a8 | |||
| 7d3f0a3ae3 | |||
| 5762754ed7 | 
							
								
								
									
										2
									
								
								Todo.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Todo.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
# Todo
 | 
			
		||||
- Verify that restore backup is correct implemented
 | 
			
		||||
							
								
								
									
										170
									
								
								restore_backup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								restore_backup.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
# @todo Not tested yet. Needs to be tested
 | 
			
		||||
"""
 | 
			
		||||
restore_backup.py
 | 
			
		||||
 | 
			
		||||
A script to recover Docker volumes and database dumps from local backups.
 | 
			
		||||
Supports an --empty flag to clear the database objects before import (drops all tables/functions etc.).
 | 
			
		||||
"""
 | 
			
		||||
import argparse
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import subprocess
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_command(cmd, capture_output=False, input=None, **kwargs):
 | 
			
		||||
    """Run a subprocess command and handle errors."""
 | 
			
		||||
    try:
 | 
			
		||||
        result = subprocess.run(cmd, check=True, capture_output=capture_output, input=input, **kwargs)
 | 
			
		||||
        return result
 | 
			
		||||
    except subprocess.CalledProcessError as e:
 | 
			
		||||
        print(f"ERROR: Command '{' '.join(cmd)}' failed with exit code {e.returncode}")
 | 
			
		||||
        if e.stdout:
 | 
			
		||||
            print(e.stdout.decode())
 | 
			
		||||
        if e.stderr:
 | 
			
		||||
            print(e.stderr.decode())
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def recover_postgres(container, password, db_name, user, backup_sql, empty=False):
 | 
			
		||||
    print("Recovering PostgreSQL dump...")
 | 
			
		||||
    os.environ['PGPASSWORD'] = password
 | 
			
		||||
    if empty:
 | 
			
		||||
        print("Dropping existing PostgreSQL objects...")
 | 
			
		||||
        # Drop all tables, views, sequences, functions in public schema
 | 
			
		||||
        drop_sql = """
 | 
			
		||||
DO $$ DECLARE r RECORD;
 | 
			
		||||
BEGIN
 | 
			
		||||
  FOR r IN (
 | 
			
		||||
    SELECT table_name AS name, 'TABLE' AS type FROM information_schema.tables WHERE table_schema='public'
 | 
			
		||||
    UNION ALL
 | 
			
		||||
    SELECT routine_name AS name, 'FUNCTION' AS type FROM information_schema.routines WHERE specific_schema='public'
 | 
			
		||||
    UNION ALL
 | 
			
		||||
    SELECT sequence_name AS name, 'SEQUENCE' AS type FROM information_schema.sequences WHERE sequence_schema='public'
 | 
			
		||||
  ) LOOP
 | 
			
		||||
    -- Use %s for type to avoid quoting the SQL keyword
 | 
			
		||||
    EXECUTE format('DROP %s public.%I CASCADE', r.type, r.name);
 | 
			
		||||
  END LOOP;
 | 
			
		||||
END
 | 
			
		||||
$$;
 | 
			
		||||
"""
 | 
			
		||||
        run_command([
 | 
			
		||||
            'docker', 'exec', '-i', container,
 | 
			
		||||
            'psql', '-v', 'ON_ERROR_STOP=1', '-U', user, '-d', db_name
 | 
			
		||||
        ], input=drop_sql.encode())
 | 
			
		||||
        print("Existing objects dropped.")
 | 
			
		||||
    print("Importing the dump...")
 | 
			
		||||
    with open(backup_sql, 'rb') as f:
 | 
			
		||||
        run_command([
 | 
			
		||||
            'docker', 'exec', '-i', container,
 | 
			
		||||
            'psql', '-v', 'ON_ERROR_STOP=1', '-U', user, '-d', db_name
 | 
			
		||||
        ], stdin=f)
 | 
			
		||||
    print("PostgreSQL recovery complete.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def recover_mariadb(container, password, db_name, user, backup_sql, empty=False):
 | 
			
		||||
    print("Recovering MariaDB dump...")
 | 
			
		||||
    if empty:
 | 
			
		||||
        print("Dropping existing MariaDB tables...")
 | 
			
		||||
        # Disable foreign key checks
 | 
			
		||||
        run_command([
 | 
			
		||||
            'docker', 'exec', container,
 | 
			
		||||
            'mysql', '-u', user, f"--password={password}", '-e', 'SET FOREIGN_KEY_CHECKS=0;'
 | 
			
		||||
        ])
 | 
			
		||||
        # Get all table names
 | 
			
		||||
        result = run_command([
 | 
			
		||||
            'docker', 'exec', container,
 | 
			
		||||
            'mysql', '-u', user, f"--password={password}", '-N', '-e',
 | 
			
		||||
            f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{db_name}';"
 | 
			
		||||
        ], capture_output=True)
 | 
			
		||||
        tables = result.stdout.decode().split()
 | 
			
		||||
        for tbl in tables:
 | 
			
		||||
            run_command([
 | 
			
		||||
                'docker', 'exec', container,
 | 
			
		||||
                'mysql', '-u', user, f"--password={password}", '-e',
 | 
			
		||||
                f"DROP TABLE IF EXISTS `{db_name}`.`{tbl}`;"
 | 
			
		||||
            ])
 | 
			
		||||
        # Enable foreign key checks
 | 
			
		||||
        run_command([
 | 
			
		||||
            'docker', 'exec', container,
 | 
			
		||||
            'mysql', '-u', user, f"--password={password}", '-e', 'SET FOREIGN_KEY_CHECKS=1;'
 | 
			
		||||
        ])
 | 
			
		||||
        print("Existing tables dropped.")
 | 
			
		||||
    print("Importing the dump...")
 | 
			
		||||
    with open(backup_sql, 'rb') as f:
 | 
			
		||||
        run_command([
 | 
			
		||||
            'docker', 'exec', '-i', container,
 | 
			
		||||
            'mariadb', '-u', user, f"--password={password}", db_name
 | 
			
		||||
        ], stdin=f)
 | 
			
		||||
    print("MariaDB recovery complete.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def recover_files(volume_name, backup_files):
 | 
			
		||||
    print(f"Inspecting volume {volume_name}...")
 | 
			
		||||
    inspect = subprocess.run(['docker', 'volume', 'inspect', volume_name], stdout=subprocess.DEVNULL)
 | 
			
		||||
    if inspect.returncode != 0:
 | 
			
		||||
        print(f"Volume {volume_name} does not exist. Creating...")
 | 
			
		||||
        run_command(['docker', 'volume', 'create', volume_name])
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"Volume {volume_name} already exists.")
 | 
			
		||||
 | 
			
		||||
    if not os.path.isdir(backup_files):
 | 
			
		||||
        print(f"ERROR: Backup files folder '{backup_files}' does not exist.")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    print("Recovering files...")
 | 
			
		||||
    run_command([
 | 
			
		||||
        'docker', 'run', '--rm',
 | 
			
		||||
        '-v', f"{volume_name}:/recover/",
 | 
			
		||||
        '-v', f"{backup_files}:/backup/",
 | 
			
		||||
        'kevinveenbirkenbach/alpine-rsync',
 | 
			
		||||
        'sh', '-c', 'rsync -avv --delete /backup/ /recover/'
 | 
			
		||||
    ])
 | 
			
		||||
    print("File recovery complete.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description='Recover Docker volumes and database dumps from local backups.'
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument('volume_name', help='Name of the Docker volume')
 | 
			
		||||
    parser.add_argument('backup_hash', help='Hashed Machine ID')
 | 
			
		||||
    parser.add_argument('version', help='Version to recover')
 | 
			
		||||
 | 
			
		||||
    parser.add_argument('--db-type', choices=['postgres', 'mariadb'], help='Type of database backup')
 | 
			
		||||
    parser.add_argument('--db-container', help='Docker container name for the database')
 | 
			
		||||
    parser.add_argument('--db-password', help='Password for the database user')
 | 
			
		||||
    parser.add_argument('--db-name', help='Name of the database')
 | 
			
		||||
    parser.add_argument('--empty', action='store_true', help='Drop existing database objects before importing')
 | 
			
		||||
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    volume = args.volume_name
 | 
			
		||||
    backup_hash = args.backup_hash
 | 
			
		||||
    version = args.version
 | 
			
		||||
 | 
			
		||||
    backup_folder = os.path.join('Backups', backup_hash, 'backup-docker-to-local', version, volume)
 | 
			
		||||
    backup_files = os.path.join(os.sep, backup_folder, 'files')
 | 
			
		||||
    backup_sql = None
 | 
			
		||||
    if args.db_name:
 | 
			
		||||
        backup_sql = os.path.join(os.sep, backup_folder, 'sql', f"{args.db_name}.backup.sql")
 | 
			
		||||
 | 
			
		||||
    # Database recovery
 | 
			
		||||
    if args.db_type:
 | 
			
		||||
        if not (args.db_container and args.db_password and args.db_name):
 | 
			
		||||
            print("ERROR: A database backup exists, aber ein Parameter fehlt.")
 | 
			
		||||
            sys.exit(1)
 | 
			
		||||
 | 
			
		||||
        user = args.db_name
 | 
			
		||||
        if args.db_type == 'postgres':
 | 
			
		||||
            recover_postgres(args.db_container, args.db_password, args.db_name, user, backup_sql, empty=args.empty)
 | 
			
		||||
        else:
 | 
			
		||||
            recover_mariadb(args.db_container, args.db_password, args.db_name, user, backup_sql, empty=args.empty)
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
 | 
			
		||||
    # File recovery
 | 
			
		||||
    recover_files(volume, backup_files)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										96
									
								
								restore_postgres_databases.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								restore_postgres_databases.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
"""
 | 
			
		||||
Restore multiple PostgreSQL databases from .backup.sql files via a Docker container.
 | 
			
		||||
 | 
			
		||||
Usage:
 | 
			
		||||
  ./restore_databases.py /path/to/backup_dir container_name
 | 
			
		||||
"""
 | 
			
		||||
import argparse
 | 
			
		||||
import subprocess
 | 
			
		||||
import sys
 | 
			
		||||
import os
 | 
			
		||||
import glob
 | 
			
		||||
 | 
			
		||||
def run_command(cmd, stdin=None):
 | 
			
		||||
    """
 | 
			
		||||
    Run a subprocess command and abort immediately on any failure.
 | 
			
		||||
    :param cmd: list of command parts
 | 
			
		||||
    :param stdin: file-like object to use as stdin
 | 
			
		||||
    """
 | 
			
		||||
    subprocess.run(cmd, stdin=stdin, check=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description="Restore Postgres databases from backup SQL files via Docker container."
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "backup_dir",
 | 
			
		||||
        help="Path to directory containing .backup.sql files"
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        "container",
 | 
			
		||||
        help="Name of the Postgres Docker container"
 | 
			
		||||
    )
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    backup_dir = args.backup_dir
 | 
			
		||||
    container = args.container
 | 
			
		||||
 | 
			
		||||
    pattern = os.path.join(backup_dir, "*.backup.sql")
 | 
			
		||||
    sql_files = sorted(glob.glob(pattern))
 | 
			
		||||
    if not sql_files:
 | 
			
		||||
        print(f"No .backup.sql files found in {backup_dir}", file=sys.stderr)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    for sqlfile in sql_files:
 | 
			
		||||
        # Extract database name by stripping the full suffix '.backup.sql'
 | 
			
		||||
        filename = os.path.basename(sqlfile)
 | 
			
		||||
        if not filename.endswith('.backup.sql'):
 | 
			
		||||
            continue
 | 
			
		||||
        dbname = filename[:-len('.backup.sql')]
 | 
			
		||||
        print(f"=== Processing {sqlfile} → database: {dbname} ===")
 | 
			
		||||
 | 
			
		||||
        # Drop the database, forcing disconnect of sessions if necessary
 | 
			
		||||
        run_command([
 | 
			
		||||
            "docker", "exec", "-i", container,
 | 
			
		||||
            "psql", "-U", "postgres", "-c",
 | 
			
		||||
            f"DROP DATABASE IF EXISTS \"{dbname}\" WITH (FORCE);"
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        # Create a fresh database
 | 
			
		||||
        run_command([
 | 
			
		||||
            "docker", "exec", "-i", container,
 | 
			
		||||
            "psql", "-U", "postgres", "-c",
 | 
			
		||||
            f"CREATE DATABASE \"{dbname}\";"
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        # Ensure the ownership role exists
 | 
			
		||||
        print(f"Ensuring role '{dbname}' exists...")
 | 
			
		||||
        run_command([
 | 
			
		||||
            "docker", "exec", "-i", container,
 | 
			
		||||
            "psql", "-U", "postgres", "-c",
 | 
			
		||||
            (
 | 
			
		||||
                "DO $$BEGIN "
 | 
			
		||||
                f"IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{dbname}') THEN "
 | 
			
		||||
                f"CREATE ROLE \"{dbname}\"; "
 | 
			
		||||
                "END IF; "
 | 
			
		||||
                "END$$;"
 | 
			
		||||
            )
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        # Restore the dump into the database by streaming file (will abort on first error)
 | 
			
		||||
        print(f"Restoring dump into {dbname} (this may take a while)…")
 | 
			
		||||
        with open(sqlfile, 'rb') as infile:
 | 
			
		||||
            run_command([
 | 
			
		||||
                "docker", "exec", "-i", container,
 | 
			
		||||
                "psql", "-U", "postgres", "-d", dbname
 | 
			
		||||
            ], stdin=infile)
 | 
			
		||||
 | 
			
		||||
        print(f"✔ {dbname} restored.")
 | 
			
		||||
 | 
			
		||||
    print("All databases have been restored.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
		Reference in New Issue
	
	Block a user