#!/usr/bin/env bash
set -euo pipefail
DEPLOY_DIR="${DEPLOY_DIR:-/opt/phoboswg}"
COMPOSE_FILE="docker-compose.yml"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-phoboswg}"
export COMPOSE_PROJECT_NAME
log() { printf '\e[1;34m==>\e[0m %s\n' "$*"; }
ok() { printf '\e[1;32m ✓\e[0m %s\n' "$*"; }
info() { printf '\e[1;30m ·\e[0m %s\n' "$*"; }
warn() { printf '\e[1;33m !\e[0m %s\n' "$*"; }
fail() { printf '\e[1;31mERROR:\e[0m %s\n' "$*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || fail "Run as root: sudo bash $0"
if ! command -v docker >/dev/null 2>&1; then
warn "docker not found — will only clean host files"
HAS_DOCKER=0
else
HAS_DOCKER=1
fi
CONTAINER_NAMES=(phobos phobos-dev)
VOLUME_KEYS_RE='etc_wireguard|sqlite_data|certs_data'
VOLUME_RE="(^|_)phobos_(${VOLUME_KEYS_RE})$"
NETWORK_RE='^(phobos|phoboswg)_(wg|default)$'
IMAGE_RE='^(ghcr\.io/)?ground-zerro/phobos(:|@)'
# Set when `compose down --volumes --remove-orphans` succeeded — subsequent
# manual cleanup steps then expect to find nothing left, so "no match" is
# informational rather than a warning.
COMPOSE_CLEAN=0
printf '\e[1;31m'
printf '┌─────────────────────────────────────────────┐\n'
printf '│ DESTRUCTIVE OPERATION — NO UNDO POSSIBLE │\n'
printf '│ │\n'
printf '│ Will permanently remove: │\n'
printf '│ • PhobosWG containers │\n'
printf '│ • PhobosWG Docker volumes │\n'
printf '│ • PhobosWG Docker networks │\n'
printf '│ • PhobosWG Docker image │\n'
printf '│ • Deploy directory %-21s │\n' "$DEPLOY_DIR"
printf '│ • PhobosWG logs and env files │\n'
printf '│ │\n'
printf '│ Other Docker workloads remain untouched. │\n'
printf '└─────────────────────────────────────────────┘\n'
printf '\e[0m'
printf '\nType YES to continue: '
read -r CONFIRM
[ "$CONFIRM" = "YES" ] || { warn "Aborted."; exit 0; }
cd /
if [ "$HAS_DOCKER" -eq 1 ]; then
log "Tearing down compose stack"
if [ -d "$DEPLOY_DIR" ] && [ -f "$DEPLOY_DIR/$COMPOSE_FILE" ]; then
if docker compose \
--project-name "$COMPOSE_PROJECT_NAME" \
--project-directory "$DEPLOY_DIR" \
-f "$DEPLOY_DIR/$COMPOSE_FILE" \
down --volumes --remove-orphans --timeout 15 >/dev/null 2>&1; then
ok "Compose stack torn down (containers + volumes + network)"
COMPOSE_CLEAN=1
else
warn "compose down failed — falling back to manual cleanup"
fi
else
info "Compose file not found at $DEPLOY_DIR/$COMPOSE_FILE — skipping compose down"
fi
log "Removing PhobosWG containers by name"
REMOVED_CT=0
for ctr in "${CONTAINER_NAMES[@]}"; do
if docker ps -a --format '{{.Names}}' | grep -qxF "$ctr"; then
if docker rm -f "$ctr" >/dev/null 2>&1; then
ok "Container removed: $ctr"
REMOVED_CT=$((REMOVED_CT + 1))
else
warn "Failed to remove container: $ctr"
fi
fi
done
if [ "$REMOVED_CT" -eq 0 ]; then
if [ "$COMPOSE_CLEAN" -eq 1 ]; then
info "No residual containers (compose already removed them)"
else
info "No matching containers found"
fi
fi
log "Removing PhobosWG volumes"
mapfile -t VOLUMES < <(
docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E "$VOLUME_RE" || true
)
if [ "${#VOLUMES[@]}" -eq 0 ]; then
if [ "$COMPOSE_CLEAN" -eq 1 ]; then
info "No residual volumes (compose already removed them)"
else
warn "No matching volumes found"
fi
else
for vol in "${VOLUMES[@]}"; do
[ -n "$vol" ] || continue
if docker volume rm "$vol" >/dev/null 2>&1; then
ok "Volume removed: $vol"
else
warn "Failed to remove volume: $vol (in use?)"
fi
done
fi
log "Removing PhobosWG networks"
mapfile -t NETS < <(
docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "$NETWORK_RE" || true
)
if [ "${#NETS[@]}" -eq 0 ]; then
if [ "$COMPOSE_CLEAN" -eq 1 ]; then
info "No residual networks (compose already removed them)"
else
warn "No matching networks found"
fi
else
for net in "${NETS[@]}"; do
[ -n "$net" ] || continue
if docker network rm "$net" >/dev/null 2>&1; then
ok "Network removed: $net"
else
warn "Failed to remove network: $net"
fi
done
fi
log "Removing PhobosWG image"
mapfile -t IMGS < <(
docker image ls --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -E "$IMAGE_RE" || true
)
if [ "${#IMGS[@]}" -eq 0 ]; then
info "No PhobosWG image present"
else
for img in "${IMGS[@]}"; do
[ -n "$img" ] || continue
if docker rmi -f "$img" >/dev/null 2>&1; then
ok "Image removed: $img"
else
warn "Failed to remove image: $img"
fi
done
fi
fi
log "Removing deploy directory: $DEPLOY_DIR"
if [ -d "$DEPLOY_DIR" ]; then
rm -rf "$DEPLOY_DIR"
ok "Removed $DEPLOY_DIR"
else
info "Deploy directory not present"
fi
log "Removing logs"
LOG_REMOVED=0
for LOG_PATH in /var/log/phoboswg /var/log/phoboswg.log; do
if [ -e "$LOG_PATH" ]; then
rm -rf "$LOG_PATH"
ok "Removed $LOG_PATH"
LOG_REMOVED=$((LOG_REMOVED + 1))
fi
done
[ "$LOG_REMOVED" -eq 0 ] && info "No PhobosWG logs found"
log "Removing env residuals"
ENV_REMOVED=0
for ENV_FILE in /root/.phoboswg.env /etc/phoboswg.env; do
if [ -f "$ENV_FILE" ]; then
rm -f "$ENV_FILE"
ok "Removed $ENV_FILE"
ENV_REMOVED=$((ENV_REMOVED + 1))
fi
done
[ "$ENV_REMOVED" -eq 0 ] && info "No env residuals found"
printf '\n\e[1;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\e[0m\n'
printf '\e[1;32m PhobosWG fully removed from this server\e[0m\n'
printf '\e[1;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\e[0m\n\n'
if [ "$HAS_DOCKER" -eq 1 ]; then
REMAINING_CT=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -E '^(phobos$|phobos-)' || true)
REMAINING_VOL=$(docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E "$VOLUME_RE" || true)
REMAINING_NET=$(docker network ls --format '{{.Name}}' 2>/dev/null | grep -E "$NETWORK_RE" || true)
if [ -n "$REMAINING_CT" ] || [ -n "$REMAINING_VOL" ] || [ -n "$REMAINING_NET" ]; then
warn "Residual PhobosWG resources still present:"
[ -n "$REMAINING_CT" ] && printf ' containers: %s\n' $REMAINING_CT
[ -n "$REMAINING_VOL" ] && printf ' volumes: %s\n' $REMAINING_VOL
[ -n "$REMAINING_NET" ] && printf ' networks: %s\n' $REMAINING_NET
fi
fi