docker-install.sh 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. #!/usr/bin/env bash
  2. #
  3. # BamBuddy Docker Installation Script
  4. # Supports: Linux (all distros), macOS
  5. #
  6. # Usage:
  7. # Interactive: curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/docker-install.sh -o docker-install.sh && chmod +x docker-install.sh && ./docker-install.sh
  8. # Unattended: ./docker-install.sh --path /opt/bambuddy --port 8000 --yes
  9. #
  10. # Options:
  11. # --path PATH Installation directory (default: /opt/bambuddy)
  12. # --port PORT Port to expose (default: 8000)
  13. # --tz TIMEZONE Timezone (default: system timezone or UTC)
  14. # --build Build from source instead of using pre-built image
  15. # --yes, -y Non-interactive mode, accept defaults
  16. # --redirect-990 (Deprecated, no longer needed — FTP binds to port 990 directly)
  17. # --help, -h Show this help message
  18. #
  19. set -e
  20. # Colors for output
  21. RED='\033[0;31m'
  22. GREEN='\033[0;32m'
  23. YELLOW='\033[1;33m'
  24. BLUE='\033[0;34m'
  25. CYAN='\033[0;36m'
  26. NC='\033[0m' # No Color
  27. BOLD='\033[1m'
  28. # Default values
  29. DEFAULT_INSTALL_PATH="/opt/bambuddy"
  30. DEFAULT_PORT="8000"
  31. # Script variables
  32. INSTALL_PATH=""
  33. PORT=""
  34. TIMEZONE=""
  35. BUILD_FROM_SOURCE="false"
  36. NON_INTERACTIVE="false"
  37. OS_TYPE=""
  38. DOCKER_CMD=""
  39. REDIRECT_990="false"
  40. # -----------------------------------------------------------------------------
  41. # Helper Functions
  42. # -----------------------------------------------------------------------------
  43. print_banner() {
  44. echo -e "${CYAN}"
  45. echo "╔════════════════════════════════════════════════════════╗"
  46. echo "║ ║"
  47. echo "║ ____ _ _ _ ║"
  48. echo "║ | __ ) __ _ _ __ ___ | |__ _ _ __| | __| |_ _ ║"
  49. echo "║ | _ \\ / _\` | '_ \` _ \\| '_ \\| | | |/ _\` |/ _\` | | | | ║"
  50. echo "║ | |_) | (_| | | | | | | |_) | |_| | (_| | (_| | |_| | ║"
  51. echo "║ |____/ \\__,_|_| |_| |_|_.__/ \\__,_|\\__,_|\\__,_|\\__, | ║"
  52. echo "║ |___/ ║"
  53. echo "║ ║"
  54. echo "║ Docker Installation Script ║"
  55. echo "║ ║"
  56. echo "╚════════════════════════════════════════════════════════╝"
  57. echo -e "${NC}"
  58. }
  59. log_info() {
  60. echo -e "${BLUE}[INFO]${NC} $1"
  61. }
  62. log_success() {
  63. echo -e "${GREEN}[OK]${NC} $1"
  64. }
  65. log_warn() {
  66. echo -e "${YELLOW}[WARN]${NC} $1"
  67. }
  68. log_error() {
  69. echo -e "${RED}[ERROR]${NC} $1"
  70. }
  71. prompt() {
  72. local prompt_text="$1"
  73. local default_value="$2"
  74. local var_name="$3"
  75. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  76. eval "$var_name=\"$default_value\""
  77. return
  78. fi
  79. if [[ -n "$default_value" ]]; then
  80. echo -en "${BOLD}$prompt_text${NC} [${CYAN}$default_value${NC}]: "
  81. else
  82. echo -en "${BOLD}$prompt_text${NC}: "
  83. fi
  84. read -r input
  85. if [[ -z "$input" ]]; then
  86. eval "$var_name=\"$default_value\""
  87. else
  88. eval "$var_name=\"$input\""
  89. fi
  90. }
  91. prompt_yes_no() {
  92. local prompt_text="$1"
  93. local default="$2" # y or n
  94. if [[ "$NON_INTERACTIVE" == "true" ]]; then
  95. [[ "$default" == "y" ]] && return 0 || return 1
  96. fi
  97. local yn_hint="[y/n]"
  98. [[ "$default" == "y" ]] && yn_hint="[Y/n]"
  99. [[ "$default" == "n" ]] && yn_hint="[y/N]"
  100. while true; do
  101. echo -en "${BOLD}$prompt_text${NC} $yn_hint: "
  102. read -r yn
  103. [[ -z "$yn" ]] && yn="$default"
  104. case "$yn" in
  105. [Yy]* ) return 0;;
  106. [Nn]* ) return 1;;
  107. * ) echo "Please answer yes or no.";;
  108. esac
  109. done
  110. }
  111. show_help() {
  112. echo "BamBuddy Docker Installation Script"
  113. echo ""
  114. echo "Usage: $0 [OPTIONS]"
  115. echo ""
  116. echo "Options:"
  117. echo " --path PATH Installation directory (default: /opt/bambuddy)"
  118. echo " --port PORT Port to expose (default: 8000)"
  119. echo " --tz TIMEZONE Timezone (default: system timezone or UTC)"
  120. echo " --build Build from source instead of using pre-built image"
  121. echo " --yes, -y Non-interactive mode, accept defaults"
  122. echo " --redirect-990 (Deprecated, no longer needed)"
  123. echo " --help, -h Show this help message"
  124. echo ""
  125. echo "Examples:"
  126. echo " Interactive installation:"
  127. echo " ./docker-install.sh"
  128. echo ""
  129. echo " Unattended installation with custom settings:"
  130. echo " ./docker-install.sh --path /srv/bambuddy --port 3000 --tz America/New_York --yes"
  131. echo ""
  132. echo " Build from source:"
  133. echo " ./docker-install.sh --build --yes"
  134. exit 0
  135. }
  136. # -----------------------------------------------------------------------------
  137. # System Detection
  138. # -----------------------------------------------------------------------------
  139. detect_os() {
  140. if [[ "$OSTYPE" == "darwin"* ]]; then
  141. OS_TYPE="macos"
  142. return
  143. fi
  144. if [[ -f /etc/os-release ]]; then
  145. OS_TYPE="linux"
  146. else
  147. log_error "Cannot detect operating system"
  148. exit 1
  149. fi
  150. }
  151. detect_docker() {
  152. # Check for docker compose (v2) or docker-compose (v1)
  153. if docker compose version &>/dev/null 2>&1; then
  154. DOCKER_CMD="docker compose"
  155. log_success "Found Docker Compose v2"
  156. return 0
  157. elif docker-compose --version &>/dev/null 2>&1; then
  158. DOCKER_CMD="docker-compose"
  159. log_success "Found Docker Compose v1"
  160. return 0
  161. fi
  162. return 1
  163. }
  164. detect_timezone() {
  165. if [[ -n "$TIMEZONE" ]]; then
  166. return 0
  167. fi
  168. # Try to get system timezone (with error handling for set -e)
  169. TIMEZONE=""
  170. if [[ -f /etc/timezone ]]; then
  171. TIMEZONE=$(cat /etc/timezone 2>/dev/null) || true
  172. fi
  173. if [[ -z "$TIMEZONE" ]] && [[ -L /etc/localtime ]]; then
  174. TIMEZONE=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||') || true
  175. fi
  176. if [[ -z "$TIMEZONE" ]] && command -v timedatectl &>/dev/null; then
  177. TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null) || true
  178. fi
  179. # Default to UTC if not found (use if/then to avoid set -e issue with &&)
  180. if [[ -z "$TIMEZONE" ]]; then
  181. TIMEZONE="UTC"
  182. fi
  183. return 0
  184. }
  185. # -----------------------------------------------------------------------------
  186. # Installation Functions
  187. # -----------------------------------------------------------------------------
  188. install_docker() {
  189. log_info "Docker not found, installing..."
  190. case "$OS_TYPE" in
  191. linux)
  192. # Use Docker's convenience script
  193. curl -fsSL https://get.docker.com | sh
  194. # Add current user to docker group
  195. if [[ -n "$SUDO_USER" ]]; then
  196. sudo usermod -aG docker "$SUDO_USER"
  197. log_warn "Added $SUDO_USER to docker group. You may need to log out and back in."
  198. else
  199. sudo usermod -aG docker "$USER"
  200. log_warn "Added $USER to docker group. You may need to log out and back in."
  201. fi
  202. # Start Docker service
  203. sudo systemctl enable docker
  204. sudo systemctl start docker
  205. ;;
  206. macos)
  207. log_error "Docker Desktop not found."
  208. log_error "Please install Docker Desktop for Mac from: https://www.docker.com/products/docker-desktop"
  209. exit 1
  210. ;;
  211. esac
  212. log_success "Docker installed"
  213. }
  214. create_install_dir() {
  215. log_info "Creating installation directory..."
  216. mkdir -p "$INSTALL_PATH"
  217. cd "$INSTALL_PATH"
  218. log_success "Directory created: $INSTALL_PATH"
  219. }
  220. download_compose_file() {
  221. log_info "Downloading docker-compose.yml..."
  222. if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
  223. # Clone the full repo for building
  224. if [[ -d ".git" ]]; then
  225. log_info "Existing repository found, updating..."
  226. git fetch origin
  227. git reset --hard origin/main
  228. else
  229. git clone https://github.com/maziggy/bambuddy.git .
  230. fi
  231. else
  232. # Just download the compose file
  233. curl -fsSL -o docker-compose.yml \
  234. https://raw.githubusercontent.com/maziggy/bambuddy/main/docker-compose.yml
  235. fi
  236. log_success "docker-compose.yml ready"
  237. }
  238. create_env_file() {
  239. log_info "Creating environment configuration..."
  240. cat > .env << EOF
  241. # BamBuddy Docker Configuration
  242. # Generated by docker-install.sh on $(date)
  243. # Port BamBuddy runs on
  244. PORT=$PORT
  245. # Timezone
  246. TZ=$TIMEZONE
  247. EOF
  248. log_success "Environment file created"
  249. }
  250. customize_compose() {
  251. # Detect if we need to disable host networking (macOS/Windows in Docker Desktop)
  252. if [[ "$OS_TYPE" == "macos" ]]; then
  253. log_warn "Docker Desktop detected. Host networking is not supported."
  254. log_info "Modifying docker-compose.yml for port mapping..."
  255. # Create a modified compose file for macOS
  256. if [[ -f docker-compose.yml ]]; then
  257. # Comment out network_mode: host and uncomment ports section
  258. sed -i.bak \
  259. -e 's/^[[:space:]]*network_mode: host/# network_mode: host/' \
  260. -e 's/^[[:space:]]*#ports:/ ports:/' \
  261. -e 's/^[[:space:]]*#[[:space:]]*- "\${PORT:-8000}:8000"/ - "\${PORT:-8000}:8000"/' \
  262. docker-compose.yml
  263. log_warn "Printer discovery may not work. Add printers manually by IP address."
  264. fi
  265. fi
  266. }
  267. start_container() {
  268. log_info "Starting BamBuddy..."
  269. if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
  270. $DOCKER_CMD up -d --build
  271. else
  272. $DOCKER_CMD up -d
  273. fi
  274. # Wait for container to start
  275. log_info "Waiting for container to start..."
  276. local max_attempts=15
  277. local attempt=0
  278. while [[ $attempt -lt $max_attempts ]]; do
  279. # Check if container is running (Up)
  280. if $DOCKER_CMD ps | grep -q "Up"; then
  281. log_success "BamBuddy container is running"
  282. return 0
  283. fi
  284. # Check if container failed
  285. if $DOCKER_CMD ps -a | grep -q "Exited"; then
  286. log_error "Container failed to start"
  287. log_info "Check logs with: $DOCKER_CMD logs bambuddy"
  288. return 1
  289. fi
  290. sleep 2
  291. ((attempt++))
  292. done
  293. log_warn "Container may still be starting. Check with: $DOCKER_CMD ps"
  294. }
  295. # -----------------------------------------------------------------------------
  296. # Main Installation Flow
  297. # -----------------------------------------------------------------------------
  298. parse_args() {
  299. while [[ $# -gt 0 ]]; do
  300. case "$1" in
  301. --path)
  302. INSTALL_PATH="$2"
  303. shift 2
  304. ;;
  305. --port)
  306. PORT="$2"
  307. shift 2
  308. ;;
  309. --tz)
  310. TIMEZONE="$2"
  311. shift 2
  312. ;;
  313. --build)
  314. BUILD_FROM_SOURCE="true"
  315. shift
  316. ;;
  317. --yes|-y)
  318. NON_INTERACTIVE="true"
  319. shift
  320. ;;
  321. --redirect-990)
  322. REDIRECT_990="true"
  323. shift
  324. ;;
  325. --help|-h)
  326. show_help
  327. ;;
  328. *)
  329. log_error "Unknown option: $1"
  330. show_help
  331. ;;
  332. esac
  333. done
  334. }
  335. check_sudo() {
  336. if ! command -v sudo &>/dev/null; then
  337. log_error "sudo is required for iptables redirect but is not installed. Skipping iptables redirect."
  338. return 1
  339. fi
  340. if ! command -v iptables &>/dev/null; then
  341. log_error "iptables is required for iptables redirect but is not installed. Skipping iptables redirect."
  342. return 1
  343. fi
  344. return 0
  345. }
  346. configure_iptables_redirect() {
  347. # Deprecated: FTP now binds directly to port 990 (requires CAP_NET_BIND_SERVICE).
  348. # The iptables 990→9990 redirect is no longer needed and caused issues with
  349. # multi-VP setups (REDIRECT rewrites dest IP to the interface's primary address).
  350. if [[ "$REDIRECT_990" == "true" ]]; then
  351. log_warn "The --redirect-990 flag is deprecated. FTP now binds directly to port 990."
  352. log_warn "No iptables redirect is needed. Skipping."
  353. fi
  354. }
  355. gather_config() {
  356. echo ""
  357. echo -e "${BOLD}Installation Configuration${NC}"
  358. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  359. echo ""
  360. # Installation path
  361. [[ -z "$INSTALL_PATH" ]] && prompt "Installation directory" "$DEFAULT_INSTALL_PATH" INSTALL_PATH
  362. # Port
  363. [[ -z "$PORT" ]] && prompt "Port to expose" "$DEFAULT_PORT" PORT
  364. # Timezone
  365. detect_timezone
  366. prompt "Timezone" "$TIMEZONE" TIMEZONE
  367. # Build from source?
  368. if [[ "$BUILD_FROM_SOURCE" != "true" ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
  369. if prompt_yes_no "Build from source? (No = use pre-built image)" "n"; then
  370. BUILD_FROM_SOURCE="true"
  371. fi
  372. fi
  373. # Confirm
  374. echo ""
  375. echo -e "${BOLD}Installation Summary${NC}"
  376. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  377. echo -e " Install path: ${GREEN}$INSTALL_PATH${NC}"
  378. echo -e " Port: ${GREEN}$PORT${NC}"
  379. echo -e " Timezone: ${GREEN}$TIMEZONE${NC}"
  380. echo -e " Build source: ${GREEN}$BUILD_FROM_SOURCE${NC}"
  381. echo -e " Redirect 990: ${GREEN}$REDIRECT_990${NC}"
  382. echo ""
  383. if ! prompt_yes_no "Proceed with installation?" "y"; then
  384. echo "Installation cancelled."
  385. exit 0
  386. fi
  387. }
  388. main() {
  389. parse_args "$@"
  390. print_banner
  391. # Check if running via pipe (curl | bash) - interactive mode won't work
  392. if [[ ! -t 0 ]] && [[ "$NON_INTERACTIVE" != "true" ]]; then
  393. log_error "Interactive mode requires a terminal."
  394. log_info "When using 'curl | bash', you must use non-interactive mode:"
  395. echo ""
  396. echo " curl -fsSL URL | bash -s -- --yes"
  397. echo ""
  398. log_info "Or download and run directly:"
  399. echo ""
  400. echo " curl -fsSL URL -o docker-install.sh && chmod +x docker-install.sh && ./docker-install.sh"
  401. echo ""
  402. exit 1
  403. fi
  404. # Detect system
  405. log_info "Detecting system..."
  406. detect_os
  407. log_success "Detected: $OS_TYPE"
  408. # Check for Docker
  409. if ! command -v docker &>/dev/null; then
  410. install_docker
  411. fi
  412. if ! detect_docker; then
  413. log_error "Docker Compose not found. Please install Docker Compose."
  414. exit 1
  415. fi
  416. # Check if Docker daemon is running
  417. if ! docker info &>/dev/null; then
  418. log_error "Docker daemon is not running. Please start Docker and try again."
  419. exit 1
  420. fi
  421. # Gather configuration
  422. gather_config
  423. # Install steps
  424. echo ""
  425. echo -e "${BOLD}Starting Installation${NC}"
  426. echo -e "${CYAN}─────────────────────────────────────────${NC}"
  427. echo ""
  428. create_install_dir
  429. download_compose_file
  430. create_env_file
  431. customize_compose
  432. start_container
  433. configure_iptables_redirect
  434. # Done!
  435. echo ""
  436. echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
  437. echo -e "${GREEN}║ ║${NC}"
  438. echo -e "${GREEN}║ Installation Complete! ║${NC}"
  439. echo -e "${GREEN}║ ║${NC}"
  440. echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
  441. echo ""
  442. local ip_addr
  443. ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}') || ip_addr="<your-ip>"
  444. echo -e " ${BOLD}Access BamBuddy:${NC} ${CYAN}http://localhost:$PORT${NC}"
  445. echo -e " ${CYAN}http://$ip_addr:$PORT${NC} (from other devices)"
  446. echo ""
  447. echo -e " ${BOLD}Manage container:${NC}"
  448. echo -e " Status: cd $INSTALL_PATH && $DOCKER_CMD ps"
  449. echo -e " Logs: cd $INSTALL_PATH && $DOCKER_CMD logs -f bambuddy"
  450. echo -e " Stop: cd $INSTALL_PATH && $DOCKER_CMD down"
  451. echo -e " Start: cd $INSTALL_PATH && $DOCKER_CMD up -d"
  452. echo -e " Restart: cd $INSTALL_PATH && $DOCKER_CMD restart"
  453. echo ""
  454. echo -e " ${BOLD}Update BamBuddy:${NC}"
  455. if [[ "$BUILD_FROM_SOURCE" == "true" ]]; then
  456. echo -e " cd $INSTALL_PATH && git pull && $DOCKER_CMD up -d --build"
  457. else
  458. echo -e " cd $INSTALL_PATH && $DOCKER_CMD pull && $DOCKER_CMD up -d"
  459. fi
  460. echo ""
  461. echo -e " ${BOLD}Data location:${NC} Docker volumes (bambuddy_data, bambuddy_logs)"
  462. echo ""
  463. echo -e " ${BOLD}Documentation:${NC} ${CYAN}https://wiki.bambuddy.cool${NC}"
  464. echo ""
  465. # Warn about iptables persistence
  466. if [[ "$REDIRECT_990" == "true" ]] && [[ "$OS_TYPE" == "linux" ]]; then
  467. echo -e " ${YELLOW}Note:${NC} iptables redirect rules do NOT survive reboot."
  468. echo -e " Install 'iptables-persistent' if persistence is required."
  469. echo ""
  470. fi
  471. if [[ "$OS_TYPE" == "macos" ]]; then
  472. echo -e " ${YELLOW}Note:${NC} Printer discovery may not work with Docker Desktop."
  473. echo -e " Add printers manually using their IP address."
  474. echo ""
  475. fi
  476. }
  477. main "$@"