#!/bin/bash # One-command setup for a self-hosted home library (Koha) on Ubuntu/Debian. # # sudo ./install.sh # install Docker if needed, build, start # sudo ./install.sh --status # show URLs and container health # sudo ./install.sh --update # rebuild with the latest Koha and restart # sudo ./install.sh --stop | --start # # Optional environment variables: # LIBRARY_NAME="Smith Family Library" name shown on the catalogue # OPAC_PORT=8080 STAFF_PORT=8081 preferred host ports (next free is used) # EXTRA_CA=/path/to/proxy-ca.crt trust an extra CA while building set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$HERE" c_info() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; } c_ok() { printf '\033[0;32m[ OK ]\033[0m %s\n' "$*"; } c_warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; } c_err() { printf '\033[0;31m[FAIL]\033[0m %s\n' "$*" >&2; } [ "$(id -u)" = 0 ] || { c_err "Run with sudo: sudo $0 $*"; exit 1; } RUN_USER="${SUDO_USER:-root}" # --- Docker ------------------------------------------------------------------- ensure_docker() { if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then c_ok "Docker $(docker --version | awk '{print $3}' | tr -d ,) with Compose is installed" else c_info "Installing Docker (official get.docker.com script)..." command -v curl >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq curl ca-certificates; } curl -fsSL https://get.docker.com | sh docker compose version >/dev/null 2>&1 || apt-get install -y -qq docker-compose-plugin c_ok "Docker installed" fi systemctl enable --now docker >/dev/null 2>&1 || true if [ "$RUN_USER" != root ] && ! id -nG "$RUN_USER" | tr ' ' '\n' | grep -qx docker; then usermod -aG docker "$RUN_USER" && c_info "Added $RUN_USER to the docker group (log out/in to use docker without sudo)" fi docker info >/dev/null 2>&1 || { c_err "The Docker daemon is not running."; exit 1; } } port_in_use() { ss -tlnH "sport = :$1" 2>/dev/null | grep -q .; } free_port() { local p="$1"; while port_in_use "$p"; do p=$((p + 1)); done; echo "$p"; } rand() { head -c 64 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c "$1"; } lan_ip() { hostname -I 2>/dev/null | awk '{print $1}'; } dc() { docker compose --project-directory "$HERE" "$@"; } # Docker Hub rate-limits anonymous pulls (HTTP 429), which is common on shared # IPs; retry with a growing pause instead of failing the whole install. with_retry() { local n=1 max=5 wait=15 until "$@"; do [ "$n" -ge "$max" ] && { c_err "Failed after $max attempts: $*"; return 1; } c_warn "Attempt $n failed (Docker Hub rate limit or network?) - retrying in ${wait}s..." sleep "$wait"; n=$((n + 1)); wait=$((wait * 2)) done } show_status() { [ -f .env ] || { c_err "Not installed yet - run: sudo ./install.sh"; exit 1; } # shellcheck disable=SC1091 . ./.env local ip; ip="$(lan_ip)"; ip="${ip:-localhost}" dc ps echo echo " Catalogue (public): http://$ip:$OPAC_PORT" echo " Staff interface: http://$ip:$STAFF_PORT" echo " Staff login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)" echo } case "${1:-}" in --status) show_status; exit 0 ;; --stop) dc stop; exit 0 ;; --start) dc start; exit 0 ;; --update) ensure_docker with_retry dc build --pull --no-cache koha dc up -d show_status; exit 0 ;; "" ) ;; *) c_err "Unknown option: $1"; exit 1 ;; esac ensure_docker # --- Settings (.env is created once and never overwritten) ------------------------ if [ ! -f .env ]; then c_info "Generating settings and passwords..." TZ_NOW="$(cat /etc/timezone 2>/dev/null || timedatectl show -p Timezone --value 2>/dev/null || echo UTC)" umask 077 cat > .env << ENVEOF # Generated by install.sh - keep this file private. LIBRARY_NAME="${LIBRARY_NAME:-My Library}" TZ=${TZ_NOW} OPAC_PORT=$(free_port "${OPAC_PORT:-8080}") STAFF_PORT=$(free_port "${STAFF_PORT:-8081}") ADMIN_USER=admin ADMIN_PASSWORD=$(rand 16) DB_ROOT_PASSWORD=$(rand 32) DB_PASSWORD=$(rand 32) KOHA_SUITE=stable ENVEOF umask 022 [ "$RUN_USER" = root ] || chown "$RUN_USER" .env else c_info "Re-using existing .env" fi if [ -n "${EXTRA_CA:-}" ]; then cp "$EXTRA_CA" koha/certs/extra-ca.crt c_info "Trusting extra CA during the build: $EXTRA_CA" fi # --- Build and start --------------------------------------------------------------- c_info "Building the Koha image (first time: 5-10 minutes, downloads ~1 GB)..." with_retry dc build c_info "Starting..." with_retry dc up -d # --- Wait until the site answers --------------------------------------------------- # shellcheck disable=SC1091 . ./.env c_info "First start installs the database - waiting for the staff interface..." for i in $(seq 1 120); do if curl -fsS -o /dev/null "http://127.0.0.1:$STAFF_PORT/" 2>/dev/null; then READY=1; break; fi # One restart is tolerated (Docker's restart policy); a crash loop is not. if [ "$(docker inspect -f '{{.RestartCount}}' library-koha 2>/dev/null || echo 99)" -ge 3 ]; then c_err "library-koha keeps crashing - see: docker logs library-koha"; exit 1 fi sleep 5 done [ "${READY:-0}" = 1 ] || { c_err "Timed out - see: docker logs -f library-koha"; exit 1; } c_ok "Your library is running." echo show_status