#!/bin/bash
# Desktop Commander Docker Installation Script
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Docker image - can be changed to latest
DOCKER_IMAGE="mcp/desktop-commander:latest"
CONTAINER_NAME="desktop-commander"
# Global flag for verbose output
VERBOSE=false
print_header() {
echo
echo -e "${BLUE}██████╗ ███████╗███████╗██╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ███████╗██████╗${NC}"
echo -e "${BLUE}██╔══██╗██╔════╝██╔════╝██║ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗ ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝██╔══██╗${NC}"
echo -e "${BLUE}██║ ██║█████╗ ███████╗█████╔╝ ██║ ██║ ██║██████╔╝ ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║█████╗ ██████╔╝${NC}"
echo -e "${BLUE}██║ ██║██╔══╝ ╚════██║██╔═██╗ ██║ ██║ ██║██╔═══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗${NC}"
echo -e "${BLUE}██████╔╝███████╗███████║██║ ██╗ ██║ ╚██████╔╝██║ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝███████╗██║ ██║${NC}"
echo -e "${BLUE}╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝${NC}"
echo
echo -e "${BLUE}🐳 Docker Installation${NC}"
echo
print_info "Experiment with AI in secure sandbox environment that won't mess up your main computer"
echo
}
print_success() {
echo -e "${GREEN}✅ $1${NC}"
}
print_error() {
echo -e "${RED}❌ Error: $1${NC}" >&2
}
print_warning() {
echo -e "${YELLOW}⚠️ Warning: $1${NC}"
}
print_info() {
echo -e "${BLUE}ℹ️ $1${NC}"
}
print_verbose() {
if [ "$VERBOSE" = true ]; then
echo -e "${BLUE}ℹ️ $1${NC}"
fi
}
# Detect OS
detect_os() {
case "$OSTYPE" in
darwin*) OS="macos" ;;
linux*) OS="linux" ;;
*) print_error "Unsupported OS: $OSTYPE" ; exit 1 ;;
esac
}
# Get Claude config path based on OS
get_claude_config_path() {
case "$OS" in
"macos")
CLAUDE_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
;;
"linux")
CLAUDE_CONFIG="$HOME/.config/claude/claude_desktop_config.json"
;;
esac
}
# Check if Docker is available
check_docker() {
while true; do
if ! command -v docker >/dev/null 2>&1; then
print_error "Docker is not installed or not found"
echo
print_error "Please install Docker first:"
case "$OS" in
"macos")
print_error "• Download Docker Desktop: https://www.docker.com/products/docker-desktop/"
;;
"linux")
print_error "• Install Docker Engine: https://docs.docker.com/engine/install/"
;;
esac
echo
echo -n "Press Enter when Docker Desktop is running or Ctrl+C to exit: "
read -r
continue
fi
if ! docker info >/dev/null 2>&1; then
print_error "Docker is installed but not running"
echo
print_error "Please start Docker Desktop and try again"
echo
echo -n "Press Enter when Docker Desktop is running or Ctrl+C to exit: "
read -r
continue
fi
# If we get here, Docker is working
break
done
print_success "Docker is available and running"
}
# Pull the Docker image
pull_docker_image() {
print_info "Pulling latest Docker image (this may take a moment)..."
if docker pull "$DOCKER_IMAGE"; then
print_success "Docker image ready: $DOCKER_IMAGE"
else
print_error "Failed to pull Docker image"
print_info "Check your internet connection and Docker Hub access"
exit 1
fi
}
# Ask user which folders to mount
ask_for_folders() {
echo
echo -e "${BLUE}📁 Folder Access Setup${NC}"
print_info "By default, Desktop Commander will have access to your user folder:"
print_info "📂 $HOME"
echo
echo -n "Press Enter to accept user folder access or 'y' to customize: "
read -r response
FOLDERS=()
if [[ $response =~ ^[Yy]$ ]]; then
# Custom folder selection
echo
print_info "Custom folder selection:"
echo -n "Mount your complete home directory ($HOME)? [Y/n]: "
read -r home_response
case "$home_response" in
[nN]|[nN][oO])
print_info "Skipping home directory"
;;
*)
FOLDERS+=("$HOME")
print_success "Added home directory access"
;;
esac
# Ask for additional folders
echo
print_info "Add extra folders outside home directory (optional):"
while true; do
echo -n "Enter folder path (or Enter to finish): "
read -r custom_dir
if [ -z "$custom_dir" ]; then
break
fi
custom_dir="${custom_dir/#\~/$HOME}"
if [ -d "$custom_dir" ]; then
FOLDERS+=("$custom_dir")
print_success "Added: $custom_dir"
else
echo -n "Folder doesn't exist. Add anyway? [y/N]: "
read -r add_anyway
if [[ $add_anyway =~ ^[Yy]$ ]]; then
FOLDERS+=("$custom_dir")
print_info "Added: $custom_dir (will create if needed)"
fi
fi
done
if [ ${#FOLDERS[@]} -eq 0 ]; then
echo
print_warning "⚠️ No folders selected - Desktop Commander will have NO file access"
echo
print_info "This means:"
echo " • Desktop Commander cannot read or write any files on your computer"
echo " • It cannot help with coding projects, file management, or document editing"
echo " • It will only work for system commands and package installation"
echo " • This makes Desktop Commander much less useful than intended"
echo
print_info "You probably want to share at least some folder to work with files"
print_info "Most users share their home directory: $HOME"
echo
echo -n "Continue with NO file access? [y/N]: "
read -r confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
print_info "Restarting folder selection..."
ask_for_folders
return
fi
print_warning "Proceeding with no file access - Desktop Commander will be limited"
fi
else
# Default: use home directory
FOLDERS+=("$HOME")
print_success "Using default access to your user folder"
fi
}
# Setup essential volumes for maximum persistence
setup_persistent_volumes() {
print_verbose "🔧 Setting up persistent development environment"
# Essential volumes that cover everything a developer needs
ESSENTIAL_VOLUMES=(
"dc-system:/usr" # All system packages, binaries, libraries
"dc-home:/root" # User configs, dotfiles, SSH keys, git config
"dc-workspace:/workspace" # Development files and projects
"dc-packages:/var" # Package databases, caches, logs
)
for volume in "${ESSENTIAL_VOLUMES[@]}"; do
volume_name=$(echo "$volume" | cut -d':' -f1)
if ! docker volume inspect "$volume_name" >/dev/null 2>&1; then
docker volume create "$volume_name" >/dev/null 2>&1
fi
done
print_verbose "Persistent environment ready - your tools will survive restarts"
}
# Build Docker run arguments
build_docker_args() {
print_verbose "Building Docker configuration..."
# Start with base arguments (use --rm so containers auto-remove after each use)
DOCKER_ARGS=("run" "-i" "--rm")
# Add essential persistent volumes
for volume in "${ESSENTIAL_VOLUMES[@]}"; do
DOCKER_ARGS+=("-v" "$volume")
done
# Add user folder mounts with absolute path structure
for folder in "${FOLDERS[@]}"; do
# Remove leading /Users/username or /home/username and keep absolute structure
if [[ "$folder" =~ ^/Users/[^/]+(/.+)$ ]]; then
# Mac: /Users/john/projects/data → /home/projects/data
absolute_path="${BASH_REMATCH[1]}"
DOCKER_ARGS+=("-v" "$folder:/home$absolute_path")
elif [[ "$folder" =~ ^/home/[^/]+(/.+)$ ]]; then
# Linux: /home/john/projects/data → /home/projects/data
absolute_path="${BASH_REMATCH[1]}"
DOCKER_ARGS+=("-v" "$folder:/home$absolute_path")
else
# Fallback for other paths - use basename
folder_name=$(basename "$folder")
DOCKER_ARGS+=("-v" "$folder:/home/$folder_name")
fi
done
# Add the image
DOCKER_ARGS+=("$DOCKER_IMAGE")
print_verbose "Docker configuration ready"
print_verbose "Essential volumes: ${#ESSENTIAL_VOLUMES[@]} volumes"
print_verbose "Mounted folders: ${#FOLDERS[@]} folders"
print_verbose "Container mode: Auto-remove after each use (--rm)"
}
# Update Claude desktop config
update_claude_config() {
print_verbose "Updating Claude Desktop configuration..."
# Create config directory if it doesn't exist
CONFIG_DIR=$(dirname "$CLAUDE_CONFIG")
if [[ ! -d "$CONFIG_DIR" ]]; then
mkdir -p "$CONFIG_DIR"
print_verbose "Created config directory: $CONFIG_DIR"
fi
# Create config if it doesn't exist
if [[ ! -f "$CLAUDE_CONFIG" ]]; then
echo '{"mcpServers": {}}' > "$CLAUDE_CONFIG"
print_verbose "Created new Claude config file"
fi
# Convert DOCKER_ARGS array to JSON format
ARGS_JSON="["
for i in "${!DOCKER_ARGS[@]}"; do
if [[ $i -gt 0 ]]; then
ARGS_JSON+=", "
fi
ARGS_JSON+="\"${DOCKER_ARGS[$i]}\""
done
ARGS_JSON+="]"
# Use Python to update JSON (preserves existing MCP servers)
python3 -c "
import json
import sys
config_path = '$CLAUDE_CONFIG'
docker_args = $ARGS_JSON
try:
with open(config_path, 'r') as f:
config = json.load(f)
except:
config = {'mcpServers': {}}
if 'mcpServers' not in config:
config['mcpServers'] = {}
# Configure to use docker run with essential volumes
config['mcpServers']['desktop-commander'] = {
'command': 'docker',
'args': docker_args
}
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
print('Successfully updated Claude config')
" || {
print_error "Failed to update Claude config with Python"
exit 1
}
print_verbose "Updated Claude config: $CLAUDE_CONFIG"
print_verbose "Desktop Commander will be available as 'desktop-commander' in Claude"
}
# Test the persistent setup
test_persistence() {
print_verbose "Testing persistent container setup..."
print_verbose "Testing essential volumes with a temporary container..."
# Test that essential paths are available for persistence
if docker "${DOCKER_ARGS[@]}" /bin/bash -c "
echo 'Testing persistence paths...'
mkdir -p /workspace/test
echo 'test-data' > /workspace/test/file.txt &&
echo 'Workspace persistence: OK'
touch /root/.test_config &&
echo 'Home persistence: OK'
echo 'Container test completed successfully'
" >/dev/null 2>&1; then
print_verbose "Essential persistence test passed"
print_verbose "Volumes are working correctly"
else
print_verbose "Some persistence tests had issues (might still work)"
fi
}
# Show container management commands
show_management_info() {
echo
print_success "🎉 Installation successfully completed! Thank you for using Desktop Commander!"
echo
print_info "How it works:"
echo "• Desktop Commander runs in isolated containers"
echo "• Your development tools and configs persist between uses"
echo "• Each command creates a fresh, clean container"
echo
print_info "🤔 Need help or have feedback? Happy to jump on a quick call:"
echo " https://calendar.app.google/SHMNZN5MJznJWC5A7"
echo
print_info "💬 Join our community: https://discord.com/invite/kQ27sNnZr7"
echo
print_info "💡 If you broke the Docker container or need a fresh start:"
echo "• Run: $0 --reset && $0"
echo "• This will reset everything and reinstall from scratch"
}
# Reset all persistent data
reset_persistence() {
echo
print_warning "This will remove ALL persistent container data!"
echo "This includes:"
echo " • All installed packages and software"
echo " • All user configurations and settings"
echo " • All development projects in /workspace"
echo " • All package caches and databases"
echo
print_info "Your mounted folders will NOT be affected."
echo
read -p "Are you sure you want to reset everything? [y/N]: " -r
case "$REPLY" in
[yY]|[yY][eE][sS])
print_info "Cleaning up containers and volumes..."
# Stop and remove any containers that might be using our volumes
print_verbose "Stopping any running Desktop Commander containers..."
docker ps -q --filter "ancestor=$DOCKER_IMAGE" | xargs -r docker stop >/dev/null 2>&1 || true
docker ps -a -q --filter "ancestor=$DOCKER_IMAGE" | xargs -r docker rm >/dev/null 2>&1 || true
# Also try by container name if it exists
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
print_info "Removing persistent volumes..."
local volumes=("dc-system" "dc-home" "dc-workspace" "dc-packages")
local failed_volumes=()
for volume in "${volumes[@]}"; do
if docker volume rm "$volume" >/dev/null 2>&1; then
print_success "✅ Removed volume: $volume"
else
failed_volumes+=("$volume")
print_warning "⚠️ Volume $volume is still in use or doesn't exist"
fi
done
# If some volumes failed, try harder cleanup
if [ ${#failed_volumes[@]} -gt 0 ]; then
print_info "Attempting force cleanup of remaining volumes..."
# Remove ALL containers that might be holding references (more aggressive)
docker container prune -f >/dev/null 2>&1 || true
for volume in "${failed_volumes[@]}"; do
if docker volume rm "$volume" >/dev/null 2>&1; then
print_success "✅ Force removed volume: $volume"
else
print_error "❌ Could not remove volume: $volume"
print_info "Manual cleanup needed: docker volume rm $volume"
fi
done
fi
print_success "🎉 Persistent data reset complete!"
echo
print_info "Run the installer again to create a fresh environment"
;;
*)
print_info "Reset cancelled"
;;
esac
}
# Show status of current setup
show_status() {
echo
print_header
# Check essential volumes
local volumes=("dc-system" "dc-home" "dc-workspace" "dc-packages")
local volumes_found=0
echo "Essential volumes status:"
for volume in "${volumes[@]}"; do
if docker volume inspect "$volume" >/dev/null 2>&1; then
local mountpoint
mountpoint=$(docker volume inspect "$volume" --format '{{.Mountpoint}}' 2>/dev/null || echo "unknown")
local size
size=$(sudo du -sh "$mountpoint" 2>/dev/null | cut -f1 || echo "unknown")
echo " ✅ $volume ($size)"
((volumes_found++))
else
echo " ❌ $volume (missing)"
fi
done
echo
echo "Status Summary:"
echo " Essential volumes: $volumes_found/4 found"
echo " Container mode: Auto-remove (--rm)"
echo " Persistence: Data stored in volumes"
echo
if [ "$volumes_found" -eq 4 ]; then
echo "✅ Ready to use with Claude!"
echo "Each command creates a fresh container that uses your persistent volumes."
elif [ "$volumes_found" -gt 0 ]; then
echo "⚠️ Some volumes missing - may need to reinstall"
else
echo "🚀 Run the installer to create your persistent volumes"
fi
}
# Try to restart Claude automatically
restart_claude() {
print_info "Attempting to restart Claude..."
case "$OS" in
macos)
# Kill Claude if running
if pgrep -f "Claude" > /dev/null; then
killall "Claude" 2>/dev/null || true
sleep 2
print_info "Stopped Claude"
fi
# Try to start Claude
if command -v open &> /dev/null; then
if open -a "Claude" 2>/dev/null; then
print_success "Claude restarted successfully"
else
print_warning "Could not auto-start Claude. Please start it manually."
fi
else
print_warning "Could not auto-restart Claude. Please start it manually."
fi
;;
linux)
# Kill Claude if running
if pgrep -f "claude" > /dev/null; then
pkill -f "claude" 2>/dev/null || true
sleep 2
print_info "Stopped Claude"
fi
# Try to start Claude
if command -v claude &> /dev/null; then
if claude &>/dev/null & disown; then
print_success "Claude restarted successfully"
else
print_warning "Could not auto-start Claude. Please start it manually."
fi
else
print_warning "Could not auto-restart Claude. Please start it manually."
fi
;;
esac
}
# Help message
show_help() {
print_header
echo "Usage: $0 [OPTION]"
echo
echo "Options:"
echo " (no args) Interactive installation"
echo " --verbose Show detailed technical output"
echo " --reset Remove all persistent data"
echo " --status Show current status"
echo " --help Show this help"
echo
echo "Creates a persistent development container using 4 essential volumes:"
echo " • dc-system: System packages and binaries (/usr)"
echo " • dc-home: User configurations (/root)"
echo " • dc-workspace: Development projects (/workspace)"
echo " • dc-packages: Package databases and caches (/var)"
echo
echo "This covers 99% of development persistence needs with simple management."
echo
}
# Main execution logic
case "${1:-}" in
--reset)
print_header
reset_persistence
exit 0
;;
--status)
show_status
exit 0
;;
--help)
show_help
exit 0
;;
--verbose)
VERBOSE=true
# Continue to main installation flow
;;
""|--install)
# Main installation flow
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
# Main installation flow
print_header
detect_os
print_success "Detected OS: $OS"
get_claude_config_path
print_info "Claude config path: $CLAUDE_CONFIG"
check_docker
pull_docker_image
ask_for_folders
setup_persistent_volumes
build_docker_args
update_claude_config
test_persistence
restart_claude
echo
print_success "✅ Claude has been restarted (if possible)"
print_info "Desktop Commander is available as 'desktop-commander' in Claude"
echo
print_info "Next steps: Install anything you want - it will persist!"
echo "• Global packages: npm install -g typescript"
echo "• User configs: git config, SSH keys, .bashrc"
show_management_info