Skip to content

Instantly share code, notes, and snippets.

@mikestreety
Created February 6, 2026 10:29
Show Gist options
  • Select an option

  • Save mikestreety/07d531b346ab8ce9c62ec655dd4274a4 to your computer and use it in GitHub Desktop.

Select an option

Save mikestreety/07d531b346ab8ce9c62ec655dd4274a4 to your computer and use it in GitHub Desktop.
Completely remove DDEV from your system. Note: Generated with assistance from AI
#!/usr/bin/env bash
################################################################################
# DDEV System Removal Script
#
# Safely removes DDEV and all associated system-level resources from macOS
# while preserving project-level .ddev directories
#
# Version: 1.0.0
# Platform: macOS (Darwin)
# Usage: ./remove-ddev.sh [OPTIONS]
#
# Options:
# --backup Create backup of global config before deletion
# --keep-docker Keep Docker images/volumes (faster reinstall)
# --keep-ssl Keep mkcert SSL certificates
# --yes, -y Skip confirmation prompts
# --dry-run Show what would be removed without doing it
# --help, -h Show help message
################################################################################
set -euo pipefail
# Script metadata
readonly SCRIPT_VERSION="1.0.0"
readonly SCRIPT_NAME="$(basename "$0")"
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly BOLD='\033[1m'
readonly RESET='\033[0m'
# Configuration
BACKUP_ENABLED=false
KEEP_DOCKER=false
KEEP_SSL=false
SKIP_CONFIRMATIONS=false
DRY_RUN=false
SHOW_HELP=false
# State tracking
CONTAINERS_REMOVED=0
IMAGES_REMOVED=0
VOLUMES_REMOVED=0
BYTES_FREED=0
ERRORS_ENCOUNTERED=0
BACKUP_CREATED=false
BACKUP_DIR=""
# Paths
readonly HOME_DIR="$HOME"
readonly DDEV_GLOBAL_CONFIG="$HOME_DIR/.ddev"
readonly MKCERT_DIR="$HOME_DIR/Library/Application Support/mkcert"
readonly BREW_PREFIX="${BREW_PREFIX:-/opt/homebrew}"
################################################################################
# Utility Functions
################################################################################
print_section() {
echo -e "\n${BLUE}${BOLD}$1${RESET}"
}
print_success() {
echo -e " ${GREEN}${RESET} $1"
}
print_warning() {
echo -e " ${YELLOW}${RESET} $1"
}
print_error() {
echo -e " ${RED}${RESET} $1" >&2
}
print_info() {
echo -e " $1"
}
track_error() {
print_error "$1"
((ERRORS_ENCOUNTERED++))
}
confirm() {
local prompt="$1"
local response
if [[ "$SKIP_CONFIRMATIONS" == "true" ]]; then
return 0
fi
while true; do
read -r -p "$(echo -e ${YELLOW}$prompt${RESET}) (y/n) " response
case "$response" in
[yY][eE][sS]|[yY])
return 0
;;
[nN][oO]|[nN])
return 1
;;
*)
echo "Please answer yes or no."
;;
esac
done
}
confirm_typed() {
local prompt="$1"
local required_text="$2"
local response
if [[ "$SKIP_CONFIRMATIONS" == "true" ]]; then
return 0
fi
read -r -p "$(echo -e ${YELLOW}$prompt${RESET})" response
[[ "$response" == "$required_text" ]]
}
execute_cmd() {
local cmd="$1"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[DRY-RUN] $cmd"
return 0
else
eval "$cmd"
fi
}
################################################################################
# Pre-flight Checks
################################################################################
check_prerequisites() {
print_section "Checking Prerequisites"
# Check macOS
if [[ "$(uname -s)" != "Darwin" ]]; then
print_error "This script is designed for macOS only"
return 1
fi
print_success "Running on macOS"
# Check Docker
if ! command -v docker &>/dev/null; then
print_error "Docker is not installed or not in PATH"
return 1
fi
print_success "Docker found: $(docker --version)"
# Check if Docker is running
if ! docker ps &>/dev/null 2>&1; then
print_error "Docker is not running. Please start Docker Desktop"
return 1
fi
print_success "Docker is running"
# Check Homebrew
if ! command -v brew &>/dev/null; then
print_error "Homebrew is not installed"
return 1
fi
print_success "Homebrew found: $(brew --version | head -1)"
# Check if DDEV is installed
if ! command -v ddev &>/dev/null; then
print_warning "DDEV is not installed (nothing to remove)"
return 1
fi
print_success "DDEV found: $(ddev version)"
return 0
}
show_removal_plan() {
print_section "DDEV Removal Plan"
# Count existing resources
local container_count
local image_count
local volume_count
container_count=$(docker ps -a --filter "name=ddev-" --format "{{.Names}}" | wc -l 2>/dev/null || echo 0)
image_count=$(docker images --filter "reference=ddev/*" --format "{{.Repository}}:{{.Tag}}" | wc -l 2>/dev/null || echo 0)
volume_count=$(docker volume ls --filter "name=ddev-" --format "{{.Name}}" | wc -l 2>/dev/null || echo 0)
# Calculate size
local config_size=0
if [[ -d "$DDEV_GLOBAL_CONFIG" ]]; then
config_size=$(du -sk "$DDEV_GLOBAL_CONFIG" 2>/dev/null | awk '{print $1 * 1024}' || echo 0)
fi
echo ""
echo "Will remove:"
echo " • DDEV binary and Homebrew formula"
echo " • Global configuration directory (~/.ddev)"
[[ $container_count -gt 0 ]] && echo "$container_count Docker container(s)"
[[ $image_count -gt 0 ]] && echo "$image_count Docker image(s)"
[[ $volume_count -gt 0 ]] && echo "$volume_count Docker volume(s)"
[[ $KEEP_SSL != "true" ]] && echo " • mkcert SSL certificates"
echo ""
echo "Will preserve:"
echo " • All project-level .ddev directories in ~/Sites"
[[ $KEEP_DOCKER == "true" ]] && echo " • Docker images and volumes"
[[ $KEEP_SSL == "true" ]] && echo " • mkcert SSL certificates"
echo ""
}
################################################################################
# Removal Functions
################################################################################
stop_ddev_services() {
print_section "Stopping DDEV Services"
# Try graceful shutdown
if ddev poweroff 2>/dev/null; then
print_success "DDEV poweroff completed"
else
print_warning "DDEV poweroff failed or already stopped"
fi
# Wait a bit for graceful shutdown
sleep 2
# Check for remaining containers and stop them
local remaining
remaining=$(docker ps --filter "name=ddev-" --format "{{.Names}}" | tr '\n' ' ')
if [[ -n "$remaining" ]]; then
print_info "Stopping remaining containers..."
for container in $remaining; do
print_info "Stopping $container..."
execute_cmd "docker stop -t 10 '$container' 2>/dev/null || docker kill '$container' 2>/dev/null || true"
print_success "Stopped $container"
done
else
print_success "All DDEV services stopped"
fi
}
remove_docker_containers() {
print_section "Removing Docker Containers"
local containers
containers=$(docker ps -a --filter "name=ddev-" --format "{{.Names}}")
if [[ -z "$containers" ]]; then
print_success "No DDEV containers found"
return 0
fi
local container_count=0
while IFS= read -r container; do
[[ -z "$container" ]] && continue
print_info "Removing container: $container"
execute_cmd "docker rm -f '$container' 2>/dev/null" || true
print_success "Removed $container"
((container_count++))
CONTAINERS_REMOVED=$container_count
done <<< "$containers"
print_success "Removed $container_count container(s)"
}
remove_docker_images() {
print_section "Removing Docker Images"
if [[ "$KEEP_DOCKER" == "true" ]]; then
print_warning "Skipped (--keep-docker enabled)"
return 0
fi
local images
images=$(docker images --filter "reference=ddev/*" --format "{{.Repository}}:{{.Tag}}")
if [[ -z "$images" ]]; then
print_success "No DDEV images found"
return 0
fi
local image_count=0
while IFS= read -r image; do
[[ -z "$image" ]] && continue
print_info "Removing image: $image"
execute_cmd "docker rmi -f '$image' 2>/dev/null" || track_error "Failed to remove $image"
print_success "Removed $image"
((image_count++))
IMAGES_REMOVED=$image_count
done <<< "$images"
print_success "Removed $image_count image(s)"
}
remove_docker_volumes() {
print_section "Removing Docker Volumes"
if [[ "$KEEP_DOCKER" == "true" ]]; then
print_warning "Skipped (--keep-docker enabled)"
return 0
fi
local volumes
volumes=$(docker volume ls --filter "name=ddev-" --format "{{.Name}}")
if [[ -z "$volumes" ]]; then
print_success "No DDEV volumes found"
return 0
fi
print_warning "This will permanently delete database snapshots"
if ! confirm "Continue removing volumes?"; then
print_warning "Skipped volume removal"
return 0
fi
local volume_count=0
while IFS= read -r volume; do
[[ -z "$volume" ]] && continue
print_info "Removing volume: $volume"
execute_cmd "docker volume rm '$volume' 2>/dev/null" || track_error "Failed to remove $volume"
print_success "Removed $volume"
((volume_count++))
VOLUMES_REMOVED=$volume_count
done <<< "$volumes"
print_success "Removed $volume_count volume(s)"
}
remove_docker_networks() {
print_section "Removing Docker Networks"
local network
network=$(docker network ls --filter "name=ddev_default" --format "{{.Name}}")
if [[ -z "$network" ]]; then
print_success "No DDEV network found"
return 0
fi
print_info "Removing network: ddev_default"
execute_cmd "docker network rm ddev_default 2>/dev/null" || true
print_success "Removed ddev_default network"
}
create_backup() {
print_section "Creating Backup"
if [[ ! -d "$DDEV_GLOBAL_CONFIG" ]]; then
print_warning "Global config not found, nothing to backup"
return 0
fi
local timestamp
timestamp=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="$HOME_DIR/ddev-backup-$timestamp"
print_info "Creating backup to: $BACKUP_DIR"
if execute_cmd "mkdir -p '$BACKUP_DIR'"; then
if execute_cmd "cp -r '$DDEV_GLOBAL_CONFIG' '$BACKUP_DIR/global'"; then
# Also copy project list if exists
if [[ -f "$DDEV_GLOBAL_CONFIG/project_list.yaml" ]]; then
execute_cmd "cp '$DDEV_GLOBAL_CONFIG/project_list.yaml' '$BACKUP_DIR/project_list.yaml'"
fi
# Create tar archive
print_info "Archiving backup..."
execute_cmd "tar -czf '$HOME_DIR/ddev-backup-$timestamp.tar.gz' -C '$HOME_DIR' 'ddev-backup-$timestamp'" || true
BACKUP_CREATED=true
print_success "Backup created: $BACKUP_DIR"
else
track_error "Failed to copy config to backup"
fi
else
track_error "Failed to create backup directory"
fi
}
remove_global_config() {
print_section "Removing Global Configuration"
if [[ ! -d "$DDEV_GLOBAL_CONFIG" ]]; then
print_success "No global config found"
return 0
fi
if ! confirm "Remove ~/.ddev directory?"; then
print_warning "Skipped global config removal"
return 0
fi
print_info "Removing $DDEV_GLOBAL_CONFIG"
execute_cmd "rm -rf '$DDEV_GLOBAL_CONFIG'"
print_success "Removed global configuration"
}
remove_ssl_certificates() {
print_section "Removing SSL Certificates"
if [[ "$KEEP_SSL" == "true" ]]; then
print_warning "Skipped (--keep-ssl enabled)"
return 0
fi
if [[ ! -d "$MKCERT_DIR" ]]; then
print_success "No mkcert certificates found"
return 0
fi
print_warning "This removes mkcert SSL certificates from your system"
print_warning "If other tools use mkcert, use --keep-ssl instead"
if ! confirm "Continue removing SSL certificates?"; then
print_warning "Skipped SSL certificate removal"
return 0
fi
# Uninstall from system trust store
print_info "Uninstalling mkcert from system trust store..."
execute_cmd "mkcert -uninstall 2>/dev/null || true"
# Remove certificate directory
print_info "Removing certificate directory..."
execute_cmd "rm -rf '$MKCERT_DIR'"
print_success "Removed SSL certificates"
}
remove_homebrew_formula() {
print_section "Uninstalling Homebrew Formula"
if ! command -v ddev &>/dev/null; then
print_success "DDEV binary not found"
return 0
fi
# Find the actual formula name (might be 'ddev', 'ddev/ddev/ddev', or versioned)
local formula_name
formula_name=$(brew list --formula 2>/dev/null | grep -E '^ddev(@|$|/)' | head -1)
if [[ -z "$formula_name" ]]; then
# Try alternative detection
formula_name=$(brew list --formula 2>/dev/null | grep ddev | head -1)
fi
if [[ -z "$formula_name" ]]; then
print_warning "Could not determine DDEV formula name, trying 'ddev'..."
formula_name="ddev"
fi
print_info "Uninstalling DDEV ($formula_name)..."
execute_cmd "brew uninstall -f '$formula_name'" || track_error "Failed to uninstall DDEV"
print_success "Uninstalled DDEV"
# Fallback: directly remove binary if still present
if command -v ddev &>/dev/null; then
local ddev_path
ddev_path=$(command -v ddev)
print_warning "DDEV binary still present, removing directly: $ddev_path"
# Remove both symlinks and the actual file
execute_cmd "rm -f '$ddev_path'" || track_error "Failed to remove binary"
# Also try removing from standard locations
execute_cmd "rm -f /opt/homebrew/bin/ddev /usr/local/bin/ddev" || true
print_success "Removed DDEV binary"
fi
# Clean Homebrew cache
print_info "Cleaning Homebrew cache..."
execute_cmd "brew cleanup -s 2>/dev/null || true"
print_success "Homebrew cleanup completed"
}
################################################################################
# Verification
################################################################################
verify_removal() {
print_section "Verifying Removal"
local issues=0
# Check DDEV binary
# Clear shell's command cache first
hash -r 2>/dev/null || true
# Check if binary exists in standard locations or PATH
local ddev_exists=false
if [[ -f /opt/homebrew/bin/ddev ]] || [[ -f /usr/local/bin/ddev ]]; then
ddev_exists=true
elif command -v ddev &>/dev/null 2>&1; then
# Double-check the path exists
if [[ -f "$(command -v ddev)" ]]; then
ddev_exists=true
fi
fi
if [[ "$ddev_exists" == "true" ]]; then
print_warning "DDEV binary still present"
((issues++))
else
print_success "DDEV binary removed"
fi
# Check global config
if [[ -d "$DDEV_GLOBAL_CONFIG" ]]; then
print_warning "Global config still present: $DDEV_GLOBAL_CONFIG"
((issues++))
else
print_success "Global config removed"
fi
# Check Docker containers
local remaining_containers
remaining_containers=$(docker ps -a --filter "name=ddev-" --format "{{.Names}}" | wc -l)
if [[ $remaining_containers -gt 0 ]]; then
print_warning "$remaining_containers DDEV container(s) still present"
((issues++))
else
print_success "All DDEV containers removed"
fi
# Check Docker images (unless kept)
if [[ "$KEEP_DOCKER" != "true" ]]; then
local remaining_images
remaining_images=$(docker images --filter "reference=ddev/*" --format "{{.Repository}}" | wc -l)
if [[ $remaining_images -gt 0 ]]; then
print_warning "$remaining_images DDEV image(s) still present"
((issues++))
else
print_success "All DDEV images removed"
fi
fi
# Check projects are intact
local projects_found
projects_found=$(find "$HOME_DIR/Sites" -maxdepth 2 -type d -name ".ddev" 2>/dev/null | wc -l)
if [[ $projects_found -gt 0 ]]; then
print_success "Project .ddev directories preserved ($projects_found found)"
else
print_warning "No project .ddev directories found (expected to exist)"
fi
echo ""
if [[ $issues -eq 0 ]]; then
echo -e "${GREEN}${BOLD}✓ DDEV completely removed from system${RESET}"
return 0
else
echo -e "${YELLOW}${BOLD}⚠ Removal verification found $issues issue(s)${RESET}"
return 1
fi
}
print_removal_summary() {
print_section "Removal Summary"
echo ""
echo "Resources Removed:"
echo " • Docker containers: $CONTAINERS_REMOVED"
echo " • Docker images: $IMAGES_REMOVED"
echo " • Docker volumes: $VOLUMES_REMOVED"
echo " • DDEV binary: Yes"
echo " • Global configuration: Yes"
echo " • SSL certificates: $([ "$KEEP_SSL" == "true" ] && echo 'No (kept)' || echo 'Yes')"
echo ""
if [[ "$BACKUP_CREATED" == "true" ]]; then
echo -e "${GREEN}Backup created:${RESET}"
echo " • Directory: $BACKUP_DIR"
echo " • Archive: $HOME_DIR/ddev-backup-$(basename $BACKUP_DIR).tar.gz"
fi
echo ""
echo "Suggested cleanup:"
echo " docker system prune -a --volumes"
echo " brew cleanup"
if [[ $ERRORS_ENCOUNTERED -gt 0 ]]; then
echo ""
echo -e "${YELLOW}⚠ Encountered $ERRORS_ENCOUNTERED error(s) during removal${RESET}"
fi
echo ""
}
################################################################################
# Help and Information
################################################################################
show_help() {
echo -e ""
echo -e "${BOLD}DDEV System Removal Script${RESET}"
echo -e ""
echo -e "${BOLD}Usage:${RESET}"
echo -e " $SCRIPT_NAME [OPTIONS]"
echo -e ""
echo -e "${BOLD}Options:${RESET}"
echo -e " --backup Create backup of global config before deletion"
echo -e " --keep-docker Keep Docker images/volumes (faster reinstall)"
echo -e " --keep-ssl Keep mkcert SSL certificates"
echo -e " --yes, -y Skip confirmation prompts"
echo -e " --dry-run Show what would be removed without doing it"
echo -e " --help, -h Show this help message"
echo -e ""
echo -e "${BOLD}Examples:${RESET}"
echo -e " # Full removal with backup (recommended)"
echo -e " $SCRIPT_NAME --backup"
echo -e ""
echo -e " # Preview what will be removed"
echo -e " $SCRIPT_NAME --dry-run"
echo -e ""
echo -e " # Quick removal keeping Docker resources"
echo -e " $SCRIPT_NAME --keep-docker"
echo -e ""
echo -e " # Automated removal"
echo -e " $SCRIPT_NAME --yes"
echo -e ""
echo -e "${BOLD}Notes:${RESET}"
echo -e " • Project-level .ddev directories are always preserved"
echo -e " • SSL certificates affect system-wide mkcert installation"
echo -e " • Use --backup to create a backup before full removal"
echo -e " • Use --dry-run to preview changes without making them"
echo -e ""
}
################################################################################
# Argument Parsing
################################################################################
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
--backup)
BACKUP_ENABLED=true
shift
;;
--keep-docker)
KEEP_DOCKER=true
shift
;;
--keep-ssl)
KEEP_SSL=true
shift
;;
--yes|-y)
SKIP_CONFIRMATIONS=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--help|-h)
SHOW_HELP=true
shift
;;
*)
print_error "Unknown option: $1"
exit 2
;;
esac
done
}
################################################################################
# Error Handling
################################################################################
cleanup_on_exit() {
if [[ "$DRY_RUN" != "true" ]]; then
# Clean up any temporary backup directory if creation failed
if [[ -n "${BACKUP_DIR:-}" && ! "$BACKUP_CREATED" == "true" && -d "$BACKUP_DIR" ]]; then
rm -rf "$BACKUP_DIR" 2>/dev/null || true
fi
fi
}
trap cleanup_on_exit EXIT
################################################################################
# Main Execution
################################################################################
main() {
# Show help if requested
if [[ "$SHOW_HELP" == "true" ]]; then
show_help
exit 0
fi
# Print header
echo ""
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════${RESET}"
echo -e "${BLUE}${BOLD} DDEV System Removal Script v$SCRIPT_VERSION${RESET}"
echo -e "${BLUE}${BOLD}═══════════════════════════════════════════════════════════${RESET}"
echo ""
# Show mode
if [[ "$DRY_RUN" == "true" ]]; then
echo -e "${YELLOW}${BOLD}⚠ DRY-RUN MODE - No changes will be made${RESET}"
echo ""
fi
# Check prerequisites
if ! check_prerequisites; then
print_error "Prerequisites check failed"
exit 1
fi
# Show removal plan
show_removal_plan
# Final confirmation
if ! confirm "Do you want to proceed with DDEV removal?"; then
echo ""
echo "Removal cancelled."
exit 1
fi
# Execute removal phases
stop_ddev_services
remove_docker_containers
remove_docker_images
remove_docker_volumes
remove_docker_networks
if [[ "$BACKUP_ENABLED" == "true" ]]; then
create_backup
fi
remove_global_config
remove_ssl_certificates
remove_homebrew_formula
# Verify and report
verify_removal
print_removal_summary
echo -e "${GREEN}${BOLD}Done!${RESET}"
echo ""
exit 0
}
# Run main function
parse_arguments "$@"
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment