docker-install.sh 19 KB

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