- Run Apache as the instance user instead of mpm_itk AssignUserID - Add Listen for the staff port; start zebra/indexer/worker/plack explicitly - Tolerate koha-plack --enable returning non-zero under set -e - install.sh: only fail on a crash loop, not a single restart Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
130 lines
4.9 KiB
Bash
Executable File
130 lines
4.9 KiB
Bash
Executable File
#!/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" "$@"; }
|
|
|
|
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
|
|
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)..."
|
|
dc build
|
|
c_info "Starting..."
|
|
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
|