#!/bin/bash # Portainer Stack Backup Script # This script connects to a Portainer instance and exports all stacks with their configurations set -e # Configuration - Update these variables PORTAINER_URL="http://192.168.11.12:9000" # Change this to your Portainer URL USERNAME="admin" # Change this to your username PASSWORD="JohnWayne#21" # Change this to your password or leave empty for prompt OUTPUT_DIR="portainer-stacks-backup" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Function to print colored output print_status() { echo -e "${GREEN}[INFO]${NC} $1" } print_warning() { echo -e "${YELLOW}[WARN]${NC} $1" } print_error() { echo -e "${RED}[ERROR]${NC} $1" } # Function to check if required tools are installed check_dependencies() { print_status "Checking dependencies..." if ! command -v curl &> /dev/null; then print_error "curl is required but not installed." exit 1 fi if ! command -v jq &> /dev/null; then print_error "jq is required but not installed. Please install it with: brew install jq" exit 1 fi print_status "All dependencies are available." } # Function to prompt for configuration if not set get_config() { if [ -z "$PORTAINER_URL" ]; then read -p "Enter Portainer URL (e.g., http://localhost:9000): " PORTAINER_URL fi if [ -z "$USERNAME" ]; then read -p "Enter Portainer username: " USERNAME fi if [ -z "$PASSWORD" ]; then read -s -p "Enter Portainer password: " PASSWORD echo fi # Remove trailing slash from URL if present PORTAINER_URL=$(echo "$PORTAINER_URL" | sed 's/\/$//g') } # Function to authenticate with Portainer authenticate() { print_status "Authenticating with Portainer..." AUTH_RESPONSE=$(curl -s -X POST \ -H "Content-Type: application/json" \ -d "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}" \ "$PORTAINER_URL/api/auth") if [ $? -ne 0 ]; then print_error "Failed to connect to Portainer at $PORTAINER_URL" exit 1 fi JWT_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.jwt // empty') if [ -z "$JWT_TOKEN" ] || [ "$JWT_TOKEN" = "null" ]; then print_error "Authentication failed. Please check your credentials." print_error "Response: $AUTH_RESPONSE" exit 1 fi print_status "Authentication successful." } # Function to get all environments (endpoints) get_environments() { print_status "Getting environments..." ENVIRONMENTS_RESPONSE=$(curl -s -X GET \ -H "Authorization: Bearer $JWT_TOKEN" \ "$PORTAINER_URL/api/endpoints") if [ $? -ne 0 ]; then print_error "Failed to get environments" exit 1 fi echo "$ENVIRONMENTS_RESPONSE" | jq -r '.[].Id' 2>/dev/null || echo "1" } # Function to get all stacks get_stacks() { print_status "Getting list of stacks..." >&2 STACKS_RESPONSE=$(curl -s -X GET \ -H "Authorization: Bearer $JWT_TOKEN" \ "$PORTAINER_URL/api/stacks") if [ $? -ne 0 ]; then print_error "Failed to get stacks" >&2 exit 1 fi echo "$STACKS_RESPONSE" } # Function to get detailed stack information get_stack_details() { local stack_id=$1 local endpoint_id=$2 curl -s -X GET \ -H "Authorization: Bearer $JWT_TOKEN" \ "$PORTAINER_URL/api/stacks/$stack_id?endpointId=$endpoint_id" } # Function to get stack file content get_stack_file() { local stack_id=$1 curl -s -X GET \ -H "Authorization: Bearer $JWT_TOKEN" \ "$PORTAINER_URL/api/stacks/$stack_id/file" } # Function to save stack data save_stack_data() { local stack_info=$1 local stack_name=$(echo "$stack_info" | jq -r '.Name') local stack_id=$(echo "$stack_info" | jq -r '.Id') local endpoint_id=$(echo "$stack_info" | jq -r '.EndpointId') print_status "Processing stack: $stack_name (ID: $stack_id)" # Create directory for this stack local stack_dir="$OUTPUT_DIR/$stack_name" mkdir -p "$stack_dir" # Save basic stack information echo "$stack_info" | jq '.' > "$stack_dir/stack-info.json" # Extract and save environment variables echo "$stack_info" | jq '.Env // []' > "$stack_dir/environment-variables.json" # Save environment variables in .env format echo "$stack_info" | jq -r '.Env[]? | select(.name != null and .value != null) | "\(.name)=\(.value)"' > "$stack_dir/.env" # Get and save stack file content (docker-compose.yml) print_status "Getting stack file for: $stack_name" STACK_FILE_RESPONSE=$(get_stack_file "$stack_id") if [ $? -eq 0 ] && [ "$STACK_FILE_RESPONSE" != "null" ]; then echo "$STACK_FILE_RESPONSE" | jq -r '.StackFileContent // empty' > "$stack_dir/docker-compose.yml" else print_warning "Could not retrieve stack file for: $stack_name" fi # Save additional metadata cat > "$stack_dir/metadata.txt" << EOF Stack Name: $stack_name Stack ID: $stack_id Endpoint ID: $endpoint_id Creation Date: $(echo "$stack_info" | jq -r '.CreationDate // "N/A"') Update Date: $(echo "$stack_info" | jq -r '.UpdateDate // "N/A"') Status: $(echo "$stack_info" | jq -r '.Status // "N/A"') Type: $(echo "$stack_info" | jq -r '.Type // "N/A"') Entry Point: $(echo "$stack_info" | jq -r '.EntryPoint // "N/A"') EOF print_status "Stack $stack_name saved to $stack_dir" } # Main execution main() { print_status "Starting Portainer stack backup..." check_dependencies get_config authenticate # Create output directory mkdir -p "$OUTPUT_DIR" # Get all stacks STACKS_JSON=$(get_stacks) if [ -z "$STACKS_JSON" ] || [ "$STACKS_JSON" = "null" ]; then print_warning "No stacks found or unable to retrieve stacks." exit 1 fi # Count stacks STACK_COUNT=$(echo "$STACKS_JSON" | jq '. | length') print_status "Found $STACK_COUNT stack(s)" if [ "$STACK_COUNT" -eq 0 ]; then print_warning "No stacks to backup." exit 0 fi # Process each stack echo "$STACKS_JSON" | jq -c '.[]' | while read -r stack; do save_stack_data "$stack" echo "---" done print_status "Backup completed! All stacks saved to: $OUTPUT_DIR" print_status "Summary:" ls -la "$OUTPUT_DIR" } # Run main function main "$@"