#!/bin/bash # # Docker Compose AppArmor Fix Script # # Adds 'security_opt: ["apparmor=unconfined"]' to all services in docker-compose.yml files # This is required for Docker containers running inside LXC containers. # # Usage: ./fix-docker-apparmor.sh [COMPOSE_DIR] # # Example: ./fix-docker-apparmor.sh 10.10.0.214 # Example: ./fix-docker-apparmor.sh 10.10.0.214 /home/cal/container-data # # Arguments: # LXC_IP - IP address of the LXC container to SSH into # COMPOSE_DIR - Optional directory containing docker-compose files (default: /home/cal/container-data) # # What this script does: # 1. SSHs into the LXC container # 2. Finds all docker-compose.yml files # 3. Adds security_opt configuration to each service # 4. Creates backups of original files # # Why this is needed: # Docker containers in LXC need AppArmor disabled to function properly. # Without this fix, containers may fail to start or have permission issues. # set -euo pipefail # Color codes for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Function to print colored messages log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_debug() { echo -e "${BLUE}[DEBUG]${NC} $1" } # Parse arguments if [[ $# -lt 1 ]]; then log_error "Insufficient arguments" echo "Usage: $0 [COMPOSE_DIR]" echo "" echo "Example: $0 10.10.0.214" echo "Example: $0 10.10.0.214 /home/cal/container-data" exit 1 fi LXC_IP=$1 COMPOSE_DIR=${2:-/home/cal/container-data} log_info "Starting AppArmor fix for Docker Compose files" log_info "Target: root@$LXC_IP" log_info "Directory: $COMPOSE_DIR" echo "" # Check SSH connectivity log_info "Testing SSH connection to $LXC_IP..." if ! ssh -o ConnectTimeout=5 -o BatchMode=yes root@"$LXC_IP" "echo 'SSH OK'" &>/dev/null; then log_error "Cannot connect to root@$LXC_IP via SSH" log_error "Please ensure:" echo " 1. SSH key is copied to the LXC container" echo " 2. Container is running" echo " 3. IP address is correct" exit 1 fi log_info "āœ… SSH connection successful" echo "" # Create Python script on remote host log_info "Creating AppArmor fix script on remote host..." ssh root@"$LXC_IP" "cat > /tmp/fix_apparmor.py" <<'PYTHON_SCRIPT' #!/usr/bin/env python3 """ Fix Docker Compose files to work in LXC by adding AppArmor unconfined security option. """ import yaml import glob import sys import os from pathlib import Path def add_apparmor_fix(compose_file): """Add security_opt to all services in a docker-compose file.""" print(f"\nšŸ“„ Processing: {compose_file}") # Create backup backup_file = f"{compose_file}.backup" if not os.path.exists(backup_file): os.system(f"cp '{compose_file}' '{backup_file}'") print(f" āœ… Backup created: {backup_file}") else: print(f" ā­ļø Backup already exists: {backup_file}") # Load compose file try: with open(compose_file, 'r') as f: compose_data = yaml.safe_load(f) except yaml.YAMLError as e: print(f" āŒ Error parsing YAML: {e}") return False except Exception as e: print(f" āŒ Error reading file: {e}") return False if not compose_data or 'services' not in compose_data: print(f" āš ļø No services found in compose file") return False # Track changes services_modified = 0 services_skipped = 0 # Add security_opt to each service for service_name, service_config in compose_data['services'].items(): if service_config is None: service_config = {} compose_data['services'][service_name] = service_config # Check if security_opt already exists existing_security = service_config.get('security_opt', []) if 'apparmor=unconfined' in existing_security or 'apparmor:unconfined' in existing_security: print(f" ā­ļø {service_name}: Already has AppArmor unconfined") services_skipped += 1 else: # Add apparmor=unconfined if not existing_security: service_config['security_opt'] = ['apparmor=unconfined'] else: if 'apparmor=unconfined' not in existing_security: existing_security.append('apparmor=unconfined') service_config['security_opt'] = existing_security print(f" āœ… {service_name}: Added AppArmor unconfined") services_modified += 1 # Write updated compose file try: with open(compose_file, 'w') as f: yaml.dump(compose_data, f, default_flow_style=False, sort_keys=False, indent=2) if services_modified > 0: print(f" šŸ’¾ Saved changes ({services_modified} services modified)") return services_modified > 0 except Exception as e: print(f" āŒ Error writing file: {e}") # Restore backup os.system(f"cp '{backup_file}' '{compose_file}'") print(f" šŸ”„ Restored from backup") return False def main(): """Main function to process all docker-compose files.""" compose_dir = sys.argv[1] if len(sys.argv) > 1 else "/home/cal/container-data" print(f"šŸ” Searching for docker-compose.yml files in {compose_dir}") # Find all docker-compose files patterns = [ f"{compose_dir}/**/docker-compose.yml", f"{compose_dir}/**/docker-compose.yaml", ] compose_files = [] for pattern in patterns: compose_files.extend(glob.glob(pattern, recursive=True)) # Remove duplicates and sort compose_files = sorted(set(compose_files)) if not compose_files: print(f"āš ļø No docker-compose files found in {compose_dir}") return 1 print(f"šŸ“‹ Found {len(compose_files)} docker-compose file(s)") # Process each file total_modified = 0 total_errors = 0 for compose_file in compose_files: try: if add_apparmor_fix(compose_file): total_modified += 1 except Exception as e: print(f" āŒ Unexpected error: {e}") total_errors += 1 # Summary print("\n" + "="*60) print("šŸ“Š SUMMARY") print("="*60) print(f"Total files found: {len(compose_files)}") print(f"Files modified: {total_modified}") print(f"Files with errors: {total_errors}") print(f"Files unchanged: {len(compose_files) - total_modified - total_errors}") print("="*60) if total_modified > 0: print("\nāœ… AppArmor fix applied successfully!") print("\nšŸ’” Next steps:") print(" 1. Review changes in modified files") print(" 2. Start containers: docker compose up -d") print(" 3. Check container status: docker compose ps") print("\nšŸ“ Note: Backups created with .backup extension") return 0 if total_errors == 0 else 1 if __name__ == "__main__": sys.exit(main()) PYTHON_SCRIPT log_info "āœ… Script uploaded to LXC container" echo "" # Install PyYAML if needed log_info "Ensuring Python and PyYAML are installed..." ssh root@"$LXC_IP" "apt-get update -qq && apt-get install -y -qq python3 python3-yaml > /dev/null 2>&1" || true log_info "āœ… Dependencies ready" echo "" # Run the fix script log_info "Running AppArmor fix script..." echo "" ssh root@"$LXC_IP" "python3 /tmp/fix_apparmor.py '$COMPOSE_DIR'" EXIT_CODE=$? echo "" # Cleanup log_info "Cleaning up temporary files..." ssh root@"$LXC_IP" "rm /tmp/fix_apparmor.py" log_info "āœ… Cleanup complete" echo "" if [[ $EXIT_CODE -eq 0 ]]; then log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log_info "šŸŽ‰ AppArmor Fix Complete!" log_info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Your docker-compose files have been updated to work in LXC." echo "" echo "Next steps:" echo " 1. SSH into container:" echo " ssh root@$LXC_IP" echo "" echo " 2. Navigate to a service directory:" echo " cd $COMPOSE_DIR/[service-name]" echo "" echo " 3. Start containers:" echo " docker compose up -d" echo "" echo " 4. Check status:" echo " docker compose ps" echo "" else log_error "AppArmor fix encountered errors. Please review output above." exit 1 fi