diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..885715c --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Windows Docker VM Configuration +# Copy this file to .env and customize + +# =========================================== +# MODERN SYSTEMS (Windows 10/11, Server 2016+) +# =========================================== +MODERN_RAM_SIZE=8G +MODERN_CPU_CORES=4 +MODERN_DISK_SIZE=128G + +# =========================================== +# LEGACY SYSTEMS (Windows 7/8, Vista, XP, 2000, Server 2003-2012) +# =========================================== +LEGACY_RAM_SIZE=2G +LEGACY_CPU_CORES=2 +LEGACY_DISK_SIZE=32G + +# =========================================== +# COMMON SETTINGS (applies to all) +# =========================================== + +# User Credentials +USERNAME=Docker +PASSWORD=admin + +# Language & Region +LANGUAGE=en +REGION=en-US +KEYBOARD=en-US + +# Display +WIDTH=1280 +HEIGHT=720 + +# Network +DHCP=N +SAMBA=Y + +# Debug +DEBUG=N diff --git a/.gitignore b/.gitignore index ce65657..fd89282 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Environment file (contains local settings) +.env + # Ignore data folder contents but keep structure data/* !data/*/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..acb1456 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.0.0] - 2026-01-28 + +### Added +- **winctl.sh**: Management script for Windows Docker containers + - 12 commands: start, stop, restart, status, logs, shell, stats, build, rebuild, list, inspect, monitor, check + - Interactive menus for version selection + - Prerequisites checking (Docker, Compose, KVM, TUN, memory, disk) + - Color-coded output with professional table formatting + - Safety confirmations for destructive operations + - Support for all 22 Windows versions across 4 categories +- Multi-version compose structure with organized folders (`compose/`) +- Environment file configuration (`.env` / `.env.example`) +- Two resource profiles: modern (8G RAM, 4 CPU) and legacy (2G RAM, 2 CPU) +- Per-version data folders under `data/` +- Pre-configured compose files for all Windows versions: + - Desktop: Win 11, 10, 8.1, 7 (with Enterprise variants) + - Legacy: Vista, XP, 2000 + - Server: 2003, 2008, 2012, 2016, 2019, 2022, 2025 + - Tiny: Tiny11, Tiny10 +- Unique port mappings for each version (no conflicts) +- CLAUDE.md for Claude Code guidance + +### Changed +- Default storage location changed from `./windows` to `./data/` +- Compose files now use `env_file` for centralized configuration +- Restart policy changed from `always` to `unless-stopped` + +### Resource Profiles + +| Profile | RAM | CPU | Disk | Used By | +|---------|-----|-----|------|---------| +| Modern | 8G | 4 | 128G | Win 10/11, Server 2016+ | +| Legacy | 2G | 2 | 32G | Win 7/8, Vista, XP, 2000, Server 2003-2012, Tiny | diff --git a/compose.yml b/compose.yml index 593f605..c7ab26e 100644 --- a/compose.yml +++ b/compose.yml @@ -1,25 +1,27 @@ # Default compose file - Windows 11 Pro # For more versions, see compose/ folder - -x-common: &common - image: dockurr/windows - devices: - - /dev/kvm - - /dev/net/tun - cap_add: - - NET_ADMIN - restart: unless-stopped - stop_grace_period: 2m +# Configure settings in .env file services: windows: - <<: *common + image: dockurr/windows container_name: windows + env_file: .env environment: VERSION: "11" + RAM_SIZE: ${MODERN_RAM_SIZE:-8G} + CPU_CORES: ${MODERN_CPU_CORES:-4} + DISK_SIZE: ${MODERN_DISK_SIZE:-128G} + devices: + - /dev/kvm + - /dev/net/tun + cap_add: + - NET_ADMIN ports: - 8006:8006 - 3389:3389/tcp - 3389:3389/udp volumes: - ./data/win11:/storage + restart: unless-stopped + stop_grace_period: 2m diff --git a/compose/base-legacy.yml b/compose/base-legacy.yml new file mode 100644 index 0000000..3dc8cc0 --- /dev/null +++ b/compose/base-legacy.yml @@ -0,0 +1,23 @@ +x-legacy: &legacy + image: dockurr/windows + devices: + - /dev/kvm + - /dev/net/tun + cap_add: + - NET_ADMIN + restart: unless-stopped + stop_grace_period: 2m + environment: + RAM_SIZE: ${LEGACY_RAM_SIZE:-2G} + CPU_CORES: ${LEGACY_CPU_CORES:-2} + DISK_SIZE: ${LEGACY_DISK_SIZE:-32G} + USERNAME: ${USERNAME:-Docker} + PASSWORD: ${PASSWORD:-admin} + LANGUAGE: ${LANGUAGE:-en} + REGION: ${REGION:-en-US} + KEYBOARD: ${KEYBOARD:-en-US} + WIDTH: ${WIDTH:-1280} + HEIGHT: ${HEIGHT:-720} + DHCP: ${DHCP:-N} + SAMBA: ${SAMBA:-Y} + DEBUG: ${DEBUG:-N} diff --git a/compose/base-modern.yml b/compose/base-modern.yml new file mode 100644 index 0000000..178fa19 --- /dev/null +++ b/compose/base-modern.yml @@ -0,0 +1,23 @@ +x-modern: &modern + image: dockurr/windows + devices: + - /dev/kvm + - /dev/net/tun + cap_add: + - NET_ADMIN + restart: unless-stopped + stop_grace_period: 2m + environment: + RAM_SIZE: ${MODERN_RAM_SIZE:-8G} + CPU_CORES: ${MODERN_CPU_CORES:-4} + DISK_SIZE: ${MODERN_DISK_SIZE:-128G} + USERNAME: ${USERNAME:-Docker} + PASSWORD: ${PASSWORD:-admin} + LANGUAGE: ${LANGUAGE:-en} + REGION: ${REGION:-en-US} + KEYBOARD: ${KEYBOARD:-en-US} + WIDTH: ${WIDTH:-1280} + HEIGHT: ${HEIGHT:-720} + DHCP: ${DHCP:-N} + SAMBA: ${SAMBA:-Y} + DEBUG: ${DEBUG:-N} diff --git a/compose/base.yml b/compose/base.yml deleted file mode 100644 index 4f9096e..0000000 --- a/compose/base.yml +++ /dev/null @@ -1,9 +0,0 @@ -x-common: &common - image: dockurr/windows - devices: - - /dev/kvm - - /dev/net/tun - cap_add: - - NET_ADMIN - restart: unless-stopped - stop_grace_period: 2m diff --git a/compose/desktop/win10.yml b/compose/desktop/win10.yml index a48e255..1aa4039 100644 --- a/compose/desktop/win10.yml +++ b/compose/desktop/win10.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win10: - <<: *common + <<: *modern container_name: win10 + env_file: ../../.env environment: VERSION: "10" ports: @@ -15,8 +16,9 @@ services: - ../../data/win10:/storage win10e: - <<: *common + <<: *modern container_name: win10e + env_file: ../../.env environment: VERSION: "10e" ports: @@ -27,8 +29,9 @@ services: - ../../data/win10e:/storage win10l: - <<: *common + <<: *modern container_name: win10l + env_file: ../../.env environment: VERSION: "10l" ports: diff --git a/compose/desktop/win11.yml b/compose/desktop/win11.yml index 49e3f6d..9ed4043 100644 --- a/compose/desktop/win11.yml +++ b/compose/desktop/win11.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win11: - <<: *common + <<: *modern container_name: win11 + env_file: ../../.env environment: VERSION: "11" ports: @@ -15,8 +16,9 @@ services: - ../../data/win11:/storage win11e: - <<: *common + <<: *modern container_name: win11e + env_file: ../../.env environment: VERSION: "11e" ports: @@ -27,8 +29,9 @@ services: - ../../data/win11e:/storage win11l: - <<: *common + <<: *modern container_name: win11l + env_file: ../../.env environment: VERSION: "11l" ports: diff --git a/compose/desktop/win7.yml b/compose/desktop/win7.yml index 468ea66..7c0a0d5 100644 --- a/compose/desktop/win7.yml +++ b/compose/desktop/win7.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win7: - <<: *common + <<: *legacy container_name: win7 + env_file: ../../.env environment: VERSION: "7u" ports: @@ -15,8 +16,9 @@ services: - ../../data/win7:/storage win7e: - <<: *common + <<: *legacy container_name: win7e + env_file: ../../.env environment: VERSION: "7e" ports: diff --git a/compose/desktop/win8.yml b/compose/desktop/win8.yml index a92ef1f..408ec52 100644 --- a/compose/desktop/win8.yml +++ b/compose/desktop/win8.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win81: - <<: *common + <<: *legacy container_name: win81 + env_file: ../../.env environment: VERSION: "8" ports: @@ -15,8 +16,9 @@ services: - ../../data/win81:/storage win81e: - <<: *common + <<: *legacy container_name: win81e + env_file: ../../.env environment: VERSION: "8e" ports: diff --git a/compose/legacy/vista.yml b/compose/legacy/vista.yml index 0589ca8..cf96b10 100644 --- a/compose/legacy/vista.yml +++ b/compose/legacy/vista.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: vista: - <<: *common + <<: *legacy container_name: vista + env_file: ../../.env environment: VERSION: "vu" ports: diff --git a/compose/legacy/win2k.yml b/compose/legacy/win2k.yml index 376aa52..e40e3e6 100644 --- a/compose/legacy/win2k.yml +++ b/compose/legacy/win2k.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win2k: - <<: *common + <<: *legacy container_name: win2k + env_file: ../../.env environment: VERSION: "2k" ports: diff --git a/compose/legacy/winxp.yml b/compose/legacy/winxp.yml index d6c9901..cb35412 100644 --- a/compose/legacy/winxp.yml +++ b/compose/legacy/winxp.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: winxp: - <<: *common + <<: *legacy container_name: winxp + env_file: ../../.env environment: VERSION: "xp" ports: diff --git a/compose/server/win2003.yml b/compose/server/win2003.yml index b837aab..dcf96a9 100644 --- a/compose/server/win2003.yml +++ b/compose/server/win2003.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win2003: - <<: *common + <<: *legacy container_name: win2003 + env_file: ../../.env environment: VERSION: "2003" ports: diff --git a/compose/server/win2008.yml b/compose/server/win2008.yml index a36b80b..c2c6c1b 100644 --- a/compose/server/win2008.yml +++ b/compose/server/win2008.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win2008: - <<: *common + <<: *legacy container_name: win2008 + env_file: ../../.env environment: VERSION: "2008" ports: diff --git a/compose/server/win2012.yml b/compose/server/win2012.yml index b67a29d..56f0514 100644 --- a/compose/server/win2012.yml +++ b/compose/server/win2012.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: win2012: - <<: *common + <<: *legacy container_name: win2012 + env_file: ../../.env environment: VERSION: "2012" ports: diff --git a/compose/server/win2016.yml b/compose/server/win2016.yml index 5df94f0..204388f 100644 --- a/compose/server/win2016.yml +++ b/compose/server/win2016.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win2016: - <<: *common + <<: *modern container_name: win2016 + env_file: ../../.env environment: VERSION: "2016" ports: diff --git a/compose/server/win2019.yml b/compose/server/win2019.yml index f4bdb91..73a0821 100644 --- a/compose/server/win2019.yml +++ b/compose/server/win2019.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win2019: - <<: *common + <<: *modern container_name: win2019 + env_file: ../../.env environment: VERSION: "2019" ports: diff --git a/compose/server/win2022.yml b/compose/server/win2022.yml index 657b93d..5ee9c35 100644 --- a/compose/server/win2022.yml +++ b/compose/server/win2022.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win2022: - <<: *common + <<: *modern container_name: win2022 + env_file: ../../.env environment: VERSION: "2022" ports: diff --git a/compose/server/win2025.yml b/compose/server/win2025.yml index 51cb7d4..c01b6ed 100644 --- a/compose/server/win2025.yml +++ b/compose/server/win2025.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-modern.yml services: win2025: - <<: *common + <<: *modern container_name: win2025 + env_file: ../../.env environment: VERSION: "2025" ports: diff --git a/compose/tiny/tiny10.yml b/compose/tiny/tiny10.yml index 21a6882..b5a730f 100644 --- a/compose/tiny/tiny10.yml +++ b/compose/tiny/tiny10.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: tiny10: - <<: *common + <<: *legacy container_name: tiny10 + env_file: ../../.env environment: VERSION: "tiny10" ports: diff --git a/compose/tiny/tiny11.yml b/compose/tiny/tiny11.yml index a7e6cd4..416ff54 100644 --- a/compose/tiny/tiny11.yml +++ b/compose/tiny/tiny11.yml @@ -1,10 +1,11 @@ include: - - ../base.yml + - ../base-legacy.yml services: tiny11: - <<: *common + <<: *legacy container_name: tiny11 + env_file: ../../.env environment: VERSION: "tiny11" ports: diff --git a/readme.md b/readme.md index adbb6bf..8818e1f 100644 --- a/readme.md +++ b/readme.md @@ -70,6 +70,123 @@ kubectl apply -f https://raw.githubusercontent.com/dockur/windows/refs/heads/mas [![Download WinBoat](https://github.com/dockur/windows/raw/master/.github/winboat.png)](https://winboat.app) +## Multi-Version Setup 🗂️ + +This repository includes pre-configured compose files for all Windows versions with optimized resource profiles. + +### Management Script (winctl.sh) + +Use `winctl.sh` for easy container management: + +```bash +# Check prerequisites +./winctl.sh check + +# Start a container (interactive menu if no version specified) +./winctl.sh start win11 +./winctl.sh start # Shows interactive menu + +# View status of all containers +./winctl.sh status + +# Stop containers (with confirmation) +./winctl.sh stop win11 + +# View logs +./winctl.sh logs win11 -f + +# List all available versions +./winctl.sh list +./winctl.sh list desktop # Filter by category + +# Real-time monitoring dashboard +./winctl.sh monitor + +# Rebuild container (preserves data) +./winctl.sh rebuild win11 + +# Full help +./winctl.sh help +``` + +### Quick Start (Manual) + +```bash +# Copy environment template +cp .env.example .env + +# Run default (Windows 11) +docker compose up + +# Run specific version +docker compose -f compose/desktop/win11.yml up win11 +docker compose -f compose/legacy/winxp.yml up winxp + +# Run all desktop versions +docker compose -f compose/desktop.yml up + +# Run everything +docker compose -f compose/all.yml up +``` + +### Configuration + +Edit `.env` to customize resources for all VMs: + +```bash +# Modern systems (Win 10/11, Server 2016+) +MODERN_RAM_SIZE=8G +MODERN_CPU_CORES=4 +MODERN_DISK_SIZE=128G + +# Legacy systems (Win 7/8, Vista, XP, 2000, Server 2003-2012, Tiny) +LEGACY_RAM_SIZE=2G +LEGACY_CPU_CORES=2 +LEGACY_DISK_SIZE=32G + +# Common settings +USERNAME=Docker +PASSWORD=admin +LANGUAGE=en +``` + +### Folder Structure + +``` +compose/ +├── base-modern.yml # High-resource profile +├── base-legacy.yml # Low-resource profile +├── all.yml # All versions +├── desktop.yml # Desktop versions +├── legacy.yml # Vista, XP, 2000 +├── server.yml # Server versions +├── tiny.yml # Tiny versions +├── desktop/ # Individual desktop configs +├── legacy/ # Individual legacy configs +├── server/ # Individual server configs +└── tiny/ # Individual tiny configs + +data/ # VM storage (per-version folders) +├── win11/ +├── winxp/ +└── ... +``` + +### Port Reference + +| Version | Web UI | RDP | +|---------|--------|-----| +| win11 | 8011 | 3311 | +| win10 | 8010 | 3310 | +| win81 | 8008 | 3308 | +| win7 | 8007 | 3307 | +| vista | 8006 | 3306 | +| winxp | 8005 | 3305 | +| win2k | 8000 | 3300 | +| win2025 | 8025 | 3325 | +| win2022 | 8022 | 3322 | +| tiny11 | 8111 | 3111 | + ## FAQ 💬 ### How do I use it? diff --git a/winctl.sh b/winctl.sh new file mode 100755 index 0000000..8c86dad --- /dev/null +++ b/winctl.sh @@ -0,0 +1,1016 @@ +#!/usr/bin/env bash +# +# winctl.sh - Windows Docker Container Management Script +# Manage Windows Docker containers with ease +# +# Usage: ./winctl.sh [options] +# +set -Eeuo pipefail + +# ============================================================================== +# METADATA +# ============================================================================== + +readonly SCRIPT_VERSION="1.0.0" +readonly SCRIPT_NAME="winctl" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR + +# ============================================================================== +# COLORS & TERMINAL DETECTION +# ============================================================================== + +if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then + readonly RED='\033[0;31m' + readonly GREEN='\033[0;32m' + readonly YELLOW='\033[0;33m' + readonly BLUE='\033[0;34m' + readonly MAGENTA='\033[0;35m' + readonly CYAN='\033[0;36m' + readonly WHITE='\033[0;37m' + readonly BOLD='\033[1m' + readonly DIM='\033[2m' + readonly RESET='\033[0m' +else + readonly RED='' + readonly GREEN='' + readonly YELLOW='' + readonly BLUE='' + readonly MAGENTA='' + readonly CYAN='' + readonly WHITE='' + readonly BOLD='' + readonly DIM='' + readonly RESET='' +fi + +# ============================================================================== +# VERSION DATA +# ============================================================================== + +# All supported versions +readonly ALL_VERSIONS=( + win11 win11e win11l win10 win10e win10l + win81 win81e win7 win7e + vista winxp win2k + win2025 win2022 win2019 win2016 win2012 win2008 win2003 + tiny11 tiny10 +) + +# Port mappings (web) +declare -A VERSION_PORTS_WEB=( + ["win11"]=8011 ["win11e"]=8012 ["win11l"]=8013 + ["win10"]=8010 ["win10e"]=8014 ["win10l"]=8015 + ["win81"]=8008 ["win81e"]=8081 + ["win7"]=8007 ["win7e"]=8071 + ["vista"]=8006 ["winxp"]=8005 ["win2k"]=8000 + ["win2025"]=8025 ["win2022"]=8022 ["win2019"]=8019 ["win2016"]=8016 + ["win2012"]=8112 ["win2008"]=8108 ["win2003"]=8003 + ["tiny11"]=8111 ["tiny10"]=8110 +) + +# Port mappings (RDP) +declare -A VERSION_PORTS_RDP=( + ["win11"]=3311 ["win11e"]=3312 ["win11l"]=3313 + ["win10"]=3310 ["win10e"]=3314 ["win10l"]=3315 + ["win81"]=3308 ["win81e"]=3381 + ["win7"]=3307 ["win7e"]=3371 + ["vista"]=3306 ["winxp"]=3305 ["win2k"]=3300 + ["win2025"]=3325 ["win2022"]=3322 ["win2019"]=3319 ["win2016"]=3316 + ["win2012"]=3212 ["win2008"]=3208 ["win2003"]=3303 + ["tiny11"]=3111 ["tiny10"]=3110 +) + +# Categories +declare -A VERSION_CATEGORIES=( + ["win11"]="desktop" ["win11e"]="desktop" ["win11l"]="desktop" + ["win10"]="desktop" ["win10e"]="desktop" ["win10l"]="desktop" + ["win81"]="desktop" ["win81e"]="desktop" + ["win7"]="desktop" ["win7e"]="desktop" + ["vista"]="legacy" ["winxp"]="legacy" ["win2k"]="legacy" + ["win2025"]="server" ["win2022"]="server" ["win2019"]="server" ["win2016"]="server" + ["win2012"]="server" ["win2008"]="server" ["win2003"]="server" + ["tiny11"]="tiny" ["tiny10"]="tiny" +) + +# Compose files +declare -A VERSION_COMPOSE_FILES=( + ["win11"]="compose/desktop/win11.yml" ["win11e"]="compose/desktop/win11.yml" ["win11l"]="compose/desktop/win11.yml" + ["win10"]="compose/desktop/win10.yml" ["win10e"]="compose/desktop/win10.yml" ["win10l"]="compose/desktop/win10.yml" + ["win81"]="compose/desktop/win8.yml" ["win81e"]="compose/desktop/win8.yml" + ["win7"]="compose/desktop/win7.yml" ["win7e"]="compose/desktop/win7.yml" + ["vista"]="compose/legacy/vista.yml" ["winxp"]="compose/legacy/winxp.yml" ["win2k"]="compose/legacy/win2k.yml" + ["win2025"]="compose/server/win2025.yml" ["win2022"]="compose/server/win2022.yml" + ["win2019"]="compose/server/win2019.yml" ["win2016"]="compose/server/win2016.yml" + ["win2012"]="compose/server/win2012.yml" ["win2008"]="compose/server/win2008.yml" ["win2003"]="compose/server/win2003.yml" + ["tiny11"]="compose/tiny/tiny11.yml" ["tiny10"]="compose/tiny/tiny10.yml" +) + +# Display names +declare -A VERSION_DISPLAY_NAMES=( + ["win11"]="Windows 11 Pro" ["win11e"]="Windows 11 Enterprise" ["win11l"]="Windows 11 LTSC" + ["win10"]="Windows 10 Pro" ["win10e"]="Windows 10 Enterprise" ["win10l"]="Windows 10 LTSC" + ["win81"]="Windows 8.1 Pro" ["win81e"]="Windows 8.1 Enterprise" + ["win7"]="Windows 7 Ultimate" ["win7e"]="Windows 7 Enterprise" + ["vista"]="Windows Vista Ultimate" ["winxp"]="Windows XP Professional" ["win2k"]="Windows 2000 Professional" + ["win2025"]="Windows Server 2025" ["win2022"]="Windows Server 2022" + ["win2019"]="Windows Server 2019" ["win2016"]="Windows Server 2016" + ["win2012"]="Windows Server 2012 R2" ["win2008"]="Windows Server 2008 R2" ["win2003"]="Windows Server 2003" + ["tiny11"]="Tiny11" ["tiny10"]="Tiny10" +) + +# Resource type (modern = high resources, legacy = low resources) +declare -A VERSION_RESOURCE_TYPE=( + ["win11"]="modern" ["win11e"]="modern" ["win11l"]="modern" + ["win10"]="modern" ["win10e"]="modern" ["win10l"]="modern" + ["win81"]="legacy" ["win81e"]="legacy" + ["win7"]="legacy" ["win7e"]="legacy" + ["vista"]="legacy" ["winxp"]="legacy" ["win2k"]="legacy" + ["win2025"]="modern" ["win2022"]="modern" ["win2019"]="modern" ["win2016"]="modern" + ["win2012"]="legacy" ["win2008"]="legacy" ["win2003"]="legacy" + ["tiny11"]="legacy" ["tiny10"]="legacy" +) + +# Resource requirements +readonly MODERN_RAM_GB=8 +readonly MODERN_DISK_GB=128 +readonly LEGACY_RAM_GB=2 +readonly LEGACY_DISK_GB=32 + +# ============================================================================== +# OUTPUT HELPERS +# ============================================================================== + +info() { + echo -e "${BLUE}[INFO]${RESET} $*" +} + +success() { + echo -e "${GREEN}[OK]${RESET} $*" +} + +warn() { + echo -e "${YELLOW}[WARN]${RESET} $*" +} + +error() { + echo -e "${RED}[ERROR]${RESET} $*" >&2 +} + +die() { + error "$@" + exit 1 +} + +header() { + echo "" + echo -e "${BOLD}${CYAN}$*${RESET}" + echo -e "${DIM}$(printf '─%.0s' {1..60})${RESET}" +} + +# Print a formatted table row +table_row() { + local version="$1" + local name="$2" + local status="$3" + local web="$4" + local rdp="$5" + + local status_color + case "$status" in + running) status_color="${GREEN}" ;; + stopped|exited) status_color="${RED}" ;; + *) status_color="${YELLOW}" ;; + esac + + printf " ${BOLD}%-12s${RESET} %-26s ${status_color}%-10s${RESET} %-8s %-8s\n" \ + "$version" "$name" "$status" "$web" "$rdp" +} + +table_header() { + echo "" + printf " ${BOLD}${DIM}%-12s %-26s %-10s %-8s %-8s${RESET}\n" \ + "VERSION" "NAME" "STATUS" "WEB" "RDP" + echo -e " ${DIM}$(printf '─%.0s' {1..66})${RESET}" +} + +# ============================================================================== +# PREREQUISITES CHECKS +# ============================================================================== + +check_docker() { + if ! command -v docker &>/dev/null; then + error "Docker is not installed" + echo " Install Docker: https://docs.docker.com/get-docker/" + return 1 + fi + + if ! docker info &>/dev/null; then + error "Docker daemon is not running" + echo " Start Docker: sudo systemctl start docker" + return 1 + fi + + success "Docker is available" + return 0 +} + +check_compose() { + if docker compose version &>/dev/null; then + success "Docker Compose plugin is available" + return 0 + elif command -v docker-compose &>/dev/null; then + success "Docker Compose standalone is available" + return 0 + else + error "Docker Compose is not installed" + echo " Install: https://docs.docker.com/compose/install/" + return 1 + fi +} + +check_kvm() { + if [[ ! -e /dev/kvm ]]; then + error "KVM device not found (/dev/kvm)" + echo " Enable virtualization in BIOS or check nested virtualization" + return 1 + fi + + if [[ ! -r /dev/kvm ]] || [[ ! -w /dev/kvm ]]; then + error "KVM device not accessible" + echo " Fix: sudo usermod -aG kvm \$USER && newgrp kvm" + return 1 + fi + + success "KVM is available" + return 0 +} + +check_tun() { + if [[ ! -e /dev/net/tun ]]; then + warn "TUN device not found (/dev/net/tun) - networking may be limited" + return 1 + fi + + success "TUN device is available" + return 0 +} + +check_memory() { + local required_gb="${1:-$MODERN_RAM_GB}" + local available_kb + available_kb=$(grep MemAvailable /proc/meminfo | awk '{print $2}') + local available_gb=$((available_kb / 1024 / 1024)) + + if ((available_gb < required_gb)); then + warn "Low memory: ${available_gb}GB available (${required_gb}GB recommended)" + return 1 + fi + + success "Memory OK: ${available_gb}GB available (${required_gb}GB needed)" + return 0 +} + +check_disk() { + local required_gb="${1:-$MODERN_DISK_GB}" + local available_kb + available_kb=$(df "$SCRIPT_DIR" | tail -1 | awk '{print $4}') + local available_gb=$((available_kb / 1024 / 1024)) + + if ((available_gb < required_gb)); then + warn "Low disk space: ${available_gb}GB available (${required_gb}GB recommended)" + return 1 + fi + + success "Disk space OK: ${available_gb}GB available (${required_gb}GB needed)" + return 0 +} + +run_all_checks() { + header "Prerequisites Check" + + local failed=0 + + check_docker || ((failed++)) + check_compose || ((failed++)) + check_kvm || ((failed++)) + check_tun || true # Warning only + check_memory || true # Warning only + check_disk || true # Warning only + + echo "" + if ((failed > 0)); then + error "Some critical checks failed. Please fix the issues above." + return 1 + else + success "All critical prerequisites passed!" + return 0 + fi +} + +# ============================================================================== +# DOCKER HELPERS +# ============================================================================== + +# Get the compose command (plugin vs standalone) +compose_cmd() { + if docker compose version &>/dev/null; then + echo "docker compose" + else + echo "docker-compose" + fi +} + +# Check if a container is running +is_running() { + local version="$1" + docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${version}$" +} + +# Check if a container exists (running or stopped) +container_exists() { + local version="$1" + docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${version}$" +} + +# Get container status +get_status() { + local version="$1" + local status + status=$(docker ps -a --filter "name=^${version}$" --format '{{.State}}' 2>/dev/null) + echo "${status:-not created}" +} + +# Get compose file path for version +get_compose_file() { + local version="$1" + local file="${VERSION_COMPOSE_FILES[$version]:-}" + if [[ -z "$file" ]]; then + die "Unknown version: $version" + fi + echo "$SCRIPT_DIR/$file" +} + +# Validate version +validate_version() { + local version="$1" + if [[ -z "${VERSION_COMPOSE_FILES[$version]:-}" ]]; then + error "Unknown version: $version" + echo " Run '${SCRIPT_NAME} list' to see available versions" + return 1 + fi + return 0 +} + +# Run compose command for a version +run_compose() { + local version="$1" + shift + local compose_file + compose_file=$(get_compose_file "$version") + + cd "$SCRIPT_DIR" + $(compose_cmd) -f "$compose_file" "$@" +} + +# ============================================================================== +# INTERACTIVE MENU +# ============================================================================== + +# Get versions by category +get_versions_by_category() { + local category="$1" + local versions=() + for v in "${ALL_VERSIONS[@]}"; do + if [[ "${VERSION_CATEGORIES[$v]}" == "$category" ]]; then + versions+=("$v") + fi + done + echo "${versions[*]}" +} + +# Show category menu +select_category() { + header "Select Category" + echo "" + echo " ${BOLD}1${RESET}) Desktop (Win 11, 10, 8.1, 7)" + echo " ${BOLD}2${RESET}) Legacy (Vista, XP, 2000)" + echo " ${BOLD}3${RESET}) Server (2025, 2022, 2019, 2016, 2012, 2008, 2003)" + echo " ${BOLD}4${RESET}) Tiny (Tiny11, Tiny10)" + echo " ${BOLD}5${RESET}) All versions" + echo " ${BOLD}6${RESET}) Select individual versions" + echo "" + echo -n " Select [1-6]: " + + local choice + read -r choice + + case "$choice" in + 1) echo "desktop" ;; + 2) echo "legacy" ;; + 3) echo "server" ;; + 4) echo "tiny" ;; + 5) echo "all" ;; + 6) echo "individual" ;; + *) echo "" ;; + esac +} + +# Show version selection menu +select_versions() { + local category="$1" + local versions=() + + if [[ "$category" == "all" ]]; then + versions=("${ALL_VERSIONS[@]}") + elif [[ "$category" == "individual" ]]; then + versions=("${ALL_VERSIONS[@]}") + else + IFS=' ' read -ra versions <<< "$(get_versions_by_category "$category")" + fi + + if [[ ${#versions[@]} -eq 0 ]]; then + die "No versions found for category: $category" + fi + + header "Select Version(s)" + echo "" + + local i=1 + for v in "${versions[@]}"; do + local status="" + if is_running "$v"; then + status="${GREEN}[running]${RESET}" + elif container_exists "$v"; then + status="${YELLOW}[stopped]${RESET}" + fi + printf " ${BOLD}%2d${RESET}) %-10s %-28s %s\n" "$i" "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$status" + ((i++)) + done + + echo "" + echo " ${BOLD} a${RESET}) Select all" + echo " ${BOLD} q${RESET}) Cancel" + echo "" + echo -n " Select (numbers separated by spaces, or 'a' for all): " + + local input + read -r input + + if [[ "$input" == "q" ]] || [[ -z "$input" ]]; then + return 1 + fi + + if [[ "$input" == "a" ]]; then + echo "${versions[*]}" + return 0 + fi + + local selected=() + for num in $input; do + if [[ "$num" =~ ^[0-9]+$ ]] && ((num >= 1 && num <= ${#versions[@]})); then + selected+=("${versions[$((num-1))]}") + fi + done + + if [[ ${#selected[@]} -eq 0 ]]; then + return 1 + fi + + echo "${selected[*]}" +} + +# Interactive version selection +interactive_select() { + local category + category=$(select_category) + + if [[ -z "$category" ]]; then + die "Invalid selection" + fi + + local selected + if ! selected=$(select_versions "$category"); then + die "No versions selected" + fi + + echo "$selected" +} + +# ============================================================================== +# COMMANDS +# ============================================================================== + +cmd_start() { + local versions=("$@") + + # Interactive selection if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + IFS=' ' read -ra versions <<< "$(interactive_select)" + fi + + # Validate all versions first + for v in "${versions[@]}"; do + validate_version "$v" || exit 1 + done + + # Run prerequisite checks + check_docker || exit 1 + check_kvm || exit 1 + + for v in "${versions[@]}"; do + header "Starting ${VERSION_DISPLAY_NAMES[$v]} ($v)" + + # Check resources + local resource_type="${VERSION_RESOURCE_TYPE[$v]}" + if [[ "$resource_type" == "modern" ]]; then + check_memory "$MODERN_RAM_GB" || true + check_disk "$MODERN_DISK_GB" || true + else + check_memory "$LEGACY_RAM_GB" || true + check_disk "$LEGACY_DISK_GB" || true + fi + + if is_running "$v"; then + info "$v is already running" + else + info "Starting $v..." + if run_compose "$v" up -d "$v"; then + success "$v started successfully" + else + error "Failed to start $v" + continue + fi + fi + + # Show connection info + echo "" + echo -e " ${BOLD}Connection Details:${RESET}" + echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" + echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" + echo "" + done +} + +cmd_stop() { + local versions=("$@") + + # Interactive selection if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + IFS=' ' read -ra versions <<< "$(interactive_select)" + fi + + # Validate all versions first + for v in "${versions[@]}"; do + validate_version "$v" || exit 1 + done + + # Show confirmation + header "Stopping Containers" + echo "" + echo " The following containers will be stopped:" + for v in "${versions[@]}"; do + local status + if is_running "$v"; then + status="${GREEN}running${RESET}" + else + status="${YELLOW}not running${RESET}" + fi + echo -e " • $v (${VERSION_DISPLAY_NAMES[$v]}) - $status" + done + echo "" + echo -n " Continue? [y/N]: " + + local confirm + read -r confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + info "Cancelled" + return 0 + fi + + for v in "${versions[@]}"; do + if ! is_running "$v" && ! container_exists "$v"; then + info "$v is not running" + continue + fi + + info "Stopping $v (grace period: 2 minutes)..." + if run_compose "$v" stop "$v"; then + success "$v stopped" + else + error "Failed to stop $v" + fi + done +} + +cmd_restart() { + local versions=("$@") + + # Interactive selection if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + IFS=' ' read -ra versions <<< "$(interactive_select)" + fi + + # Validate all versions first + for v in "${versions[@]}"; do + validate_version "$v" || exit 1 + done + + for v in "${versions[@]}"; do + header "Restarting ${VERSION_DISPLAY_NAMES[$v]} ($v)" + + info "Restarting $v..." + if run_compose "$v" restart "$v"; then + success "$v restarted" + echo "" + echo -e " ${BOLD}Connection Details:${RESET}" + echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" + echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" + echo "" + else + error "Failed to restart $v" + fi + done +} + +cmd_status() { + local versions=("$@") + + # Show all if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + versions=("${ALL_VERSIONS[@]}") + fi + + table_header + + for v in "${versions[@]}"; do + if ! validate_version "$v" 2>/dev/null; then + continue + fi + + local status + status=$(get_status "$v") + table_row "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$status" "${VERSION_PORTS_WEB[$v]}" "${VERSION_PORTS_RDP[$v]}" + done + echo "" +} + +cmd_logs() { + local version="${1:-}" + local follow="${2:-}" + + if [[ -z "$version" ]]; then + die "Usage: ${SCRIPT_NAME} logs [-f]" + fi + + validate_version "$version" || exit 1 + + local args=() + if [[ "$follow" == "-f" ]]; then + args+=("--follow") + fi + + info "Showing logs for $version..." + run_compose "$version" logs "${args[@]}" "$version" +} + +cmd_shell() { + local version="${1:-}" + + if [[ -z "$version" ]]; then + die "Usage: ${SCRIPT_NAME} shell " + fi + + validate_version "$version" || exit 1 + + if ! is_running "$version"; then + die "$version is not running" + fi + + info "Opening shell in $version..." + docker exec -it "$version" /bin/bash +} + +cmd_stats() { + local versions=("$@") + + # Get running containers if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + local running=() + for v in "${ALL_VERSIONS[@]}"; do + if is_running "$v"; then + running+=("$v") + fi + done + if [[ ${#running[@]} -eq 0 ]]; then + die "No containers are running" + fi + versions=("${running[@]}") + fi + + # Validate versions + local valid_running=() + for v in "${versions[@]}"; do + if validate_version "$v" 2>/dev/null && is_running "$v"; then + valid_running+=("$v") + fi + done + + if [[ ${#valid_running[@]} -eq 0 ]]; then + die "None of the specified containers are running" + fi + + info "Showing stats for: ${valid_running[*]}" + docker stats "${valid_running[@]}" +} + +cmd_build() { + header "Building Docker Image" + + check_docker || exit 1 + + info "Building dockurr/windows image locally..." + cd "$SCRIPT_DIR" + + if docker build -t dockurr/windows .; then + success "Image built successfully" + else + die "Build failed" + fi +} + +cmd_rebuild() { + local versions=("$@") + + # Interactive selection if no versions specified + if [[ ${#versions[@]} -eq 0 ]]; then + IFS=' ' read -ra versions <<< "$(interactive_select)" + fi + + # Validate all versions first + for v in "${versions[@]}"; do + validate_version "$v" || exit 1 + done + + # Show warning + header "⚠️ Rebuild Containers" + echo "" + echo -e " ${RED}${BOLD}WARNING: This will destroy and recreate the following containers.${RESET}" + echo -e " ${RED}Data in /storage volumes will be preserved.${RESET}" + echo "" + for v in "${versions[@]}"; do + echo " • $v (${VERSION_DISPLAY_NAMES[$v]})" + done + echo "" + echo -n " Type 'yes' to confirm: " + + local confirm + read -r confirm + if [[ "$confirm" != "yes" ]]; then + info "Cancelled" + return 0 + fi + + for v in "${versions[@]}"; do + header "Rebuilding $v" + + info "Stopping and removing $v..." + run_compose "$v" down "$v" 2>/dev/null || true + + info "Recreating $v..." + if run_compose "$v" up -d "$v"; then + success "$v rebuilt successfully" + echo "" + echo -e " ${BOLD}Connection Details:${RESET}" + echo -e " → Web Viewer: ${CYAN}http://localhost:${VERSION_PORTS_WEB[$v]}${RESET}" + echo -e " → RDP: ${CYAN}localhost:${VERSION_PORTS_RDP[$v]}${RESET}" + echo "" + else + error "Failed to rebuild $v" + fi + done +} + +cmd_list() { + local category="${1:-all}" + + header "Available Windows Versions" + + local categories=() + case "$category" in + desktop) categories=("desktop") ;; + legacy) categories=("legacy") ;; + server) categories=("server") ;; + tiny) categories=("tiny") ;; + all) categories=("desktop" "legacy" "server" "tiny") ;; + *) + die "Unknown category: $category. Use: desktop, legacy, server, tiny, or all" + ;; + esac + + for cat in "${categories[@]}"; do + echo "" + local cat_upper + cat_upper=$(echo "$cat" | tr '[:lower:]' '[:upper:]') + echo -e " ${BOLD}${cat_upper}${RESET}" + echo -e " ${DIM}$(printf '─%.0s' {1..50})${RESET}" + + for v in "${ALL_VERSIONS[@]}"; do + if [[ "${VERSION_CATEGORIES[$v]}" == "$cat" ]]; then + local status="" + if is_running "$v"; then + status="${GREEN}[running]${RESET}" + elif container_exists "$v"; then + status="${YELLOW}[stopped]${RESET}" + fi + local resource_tag + if [[ "${VERSION_RESOURCE_TYPE[$v]}" == "modern" ]]; then + resource_tag="${CYAN}(8G RAM)${RESET}" + else + resource_tag="${DIM}(2G RAM)${RESET}" + fi + printf " %-10s %-28s %s %s\n" "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$resource_tag" "$status" + fi + done + done + echo "" +} + +cmd_inspect() { + local version="${1:-}" + + if [[ -z "$version" ]]; then + die "Usage: ${SCRIPT_NAME} inspect " + fi + + validate_version "$version" || exit 1 + + header "Container Details: $version" + echo "" + echo -e " ${BOLD}Version:${RESET} $version" + echo -e " ${BOLD}Name:${RESET} ${VERSION_DISPLAY_NAMES[$version]}" + echo -e " ${BOLD}Category:${RESET} ${VERSION_CATEGORIES[$version]}" + echo -e " ${BOLD}Status:${RESET} $(get_status "$version")" + echo -e " ${BOLD}Web Port:${RESET} ${VERSION_PORTS_WEB[$version]}" + echo -e " ${BOLD}RDP Port:${RESET} ${VERSION_PORTS_RDP[$version]}" + echo -e " ${BOLD}Resources:${RESET} ${VERSION_RESOURCE_TYPE[$version]}" + echo -e " ${BOLD}Compose:${RESET} ${VERSION_COMPOSE_FILES[$version]}" + echo "" + + if container_exists "$version"; then + echo -e " ${BOLD}Docker Info:${RESET}" + docker inspect "$version" --format ' + Image: {{.Config.Image}} + Created: {{.Created}} + IP Address: {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} + Mounts: {{range .Mounts}}{{.Source}} -> {{.Destination}} + {{end}}' 2>/dev/null || true + fi + echo "" +} + +cmd_monitor() { + local interval="${1:-5}" + + if ! [[ "$interval" =~ ^[0-9]+$ ]]; then + die "Interval must be a number (seconds)" + fi + + header "Real-time Monitor (refresh: ${interval}s)" + echo " Press Ctrl+C to exit" + echo "" + + while true; do + clear + echo -e "${BOLD}${CYAN}Windows Container Monitor${RESET} - $(date '+%Y-%m-%d %H:%M:%S')" + echo -e "${DIM}$(printf '─%.0s' {1..70})${RESET}" + + local running_count=0 + local stopped_count=0 + local total_count=0 + + table_header + + for v in "${ALL_VERSIONS[@]}"; do + local status + status=$(get_status "$v") + if [[ "$status" != "not created" ]]; then + ((total_count++)) + if [[ "$status" == "running" ]]; then + ((running_count++)) + else + ((stopped_count++)) + fi + table_row "$v" "${VERSION_DISPLAY_NAMES[$v]}" "$status" "${VERSION_PORTS_WEB[$v]}" "${VERSION_PORTS_RDP[$v]}" + fi + done + + if [[ $total_count -eq 0 ]]; then + echo -e " ${DIM}No containers found${RESET}" + fi + + echo "" + echo -e " ${BOLD}Summary:${RESET} ${GREEN}$running_count running${RESET}, ${RED}$stopped_count stopped${RESET}, $total_count total" + echo "" + echo -e " ${DIM}Refreshing in ${interval}s... (Ctrl+C to exit)${RESET}" + + sleep "$interval" + done +} + +cmd_check() { + run_all_checks +} + +# ============================================================================== +# HELP +# ============================================================================== + +show_usage() { + cat << EOF +${BOLD}${SCRIPT_NAME}${RESET} v${SCRIPT_VERSION} - Windows Docker Container Management + +${BOLD}USAGE${RESET} + ${SCRIPT_NAME} [options] + +${BOLD}COMMANDS${RESET} + ${BOLD}start${RESET} [version...] Start container(s), interactive if no version + ${BOLD}stop${RESET} [version...] Stop container(s) with 2-min grace period + ${BOLD}restart${RESET} [version...] Restart container(s) + ${BOLD}status${RESET} [version...] Show status of container(s) + ${BOLD}logs${RESET} [-f] View container logs (-f to follow) + ${BOLD}shell${RESET} Open bash shell in container + ${BOLD}stats${RESET} [version...] Show real-time resource usage + ${BOLD}build${RESET} Build Docker image locally + ${BOLD}rebuild${RESET} [version...] Destroy and recreate container(s) + ${BOLD}list${RESET} [category] List versions (desktop/legacy/server/tiny/all) + ${BOLD}inspect${RESET} Show detailed container info + ${BOLD}monitor${RESET} [interval] Real-time dashboard (default: 5s refresh) + ${BOLD}check${RESET} Run prerequisites check + ${BOLD}help${RESET} Show this help message + +${BOLD}CATEGORIES${RESET} + desktop Win 11/10/8.1/7 (Pro, Enterprise, LTSC variants) + legacy Vista, XP, 2000 + server Server 2025/2022/2019/2016/2012/2008/2003 + tiny Tiny11, Tiny10 + +${BOLD}EXAMPLES${RESET} + ${SCRIPT_NAME} start # Interactive menu + ${SCRIPT_NAME} start win11 # Start Windows 11 + ${SCRIPT_NAME} start win11 win10 # Start multiple + ${SCRIPT_NAME} stop win11 # Stop with confirmation + ${SCRIPT_NAME} status # Show all containers + ${SCRIPT_NAME} logs win11 -f # Follow logs + ${SCRIPT_NAME} list desktop # List desktop versions + ${SCRIPT_NAME} monitor 10 # Dashboard with 10s refresh + ${SCRIPT_NAME} rebuild win11 # Recreate container + +${BOLD}PORTS${RESET} + Each version has unique ports for Web UI and RDP access. + Run '${SCRIPT_NAME} list' to see port mappings. + +EOF +} + +# ============================================================================== +# MAIN +# ============================================================================== + +main() { + # Change to script directory + cd "$SCRIPT_DIR" + + local command="${1:-}" + shift || true + + case "$command" in + start) cmd_start "$@" ;; + stop) cmd_stop "$@" ;; + restart) cmd_restart "$@" ;; + status) cmd_status "$@" ;; + logs) cmd_logs "$@" ;; + shell) cmd_shell "$@" ;; + stats) cmd_stats "$@" ;; + build) cmd_build "$@" ;; + rebuild) cmd_rebuild "$@" ;; + list) cmd_list "$@" ;; + inspect) cmd_inspect "$@" ;; + monitor) cmd_monitor "$@" ;; + check) cmd_check "$@" ;; + help|--help|-h) + show_usage + ;; + "") + show_usage + exit 1 + ;; + *) + error "Unknown command: $command" + echo "Run '${SCRIPT_NAME} help' for usage information" + exit 1 + ;; + esac +} + +main "$@"