#!/bin/sh # Abhako installer for macOS and Linux. # # curl -fsSL https://abhako.com/install.sh | sh # # Installs (or updates) Abhako from GitHub and starts it with Docker Compose. # Run it again to update: your .env and data/ are kept. # # Options (environment variables): # ABHAKO_DIR install folder (default: $HOME/abhako) # ABHAKO_PORT port on 127.0.0.1 (default: 3040) # ABHAKO_VERSION git tag or branch to install (default: main = latest version) # # The script never uses sudo. Source: https://github.com/Gegenschuss/abhako (MIT) # Everything is inside main(), which is only called on the last line: a download cut off # halfway through cannot run a partial script. set -eu REPO="Gegenschuss/abhako" say() { printf '%s\n' "$*"; } step() { printf '\n\033[1;36m==>\033[0m \033[1m%s\033[0m\n' "$*"; } warn() { printf '\033[33mNote:\033[0m %s\n' "$*" >&2; } die() { printf '\n\033[31mError:\033[0m %s\n' "$*" >&2; exit 1; } # docker compose (v2 plugin) or docker-compose (v1), chosen in check_docker dc() { if [ "$COMPOSE" = plugin ]; then docker compose "$@"; else docker-compose "$@"; fi } check_docker() { os=$(uname -s) if ! command -v docker >/dev/null 2>&1; then case "$os" in Darwin) die "Docker is not installed. Install Docker Desktop for Mac: https://docs.docker.com/desktop/setup/install/mac-install/ Start it once, then run this installer again." ;; *) die "Docker is not installed. Install Docker Engine and the Compose plugin from your distribution's packages (for example: Debian/Ubuntu 'docker.io docker-compose-v2', Fedora 'docker docker-compose', Arch 'docker docker-compose') or follow https://docs.docker.com/engine/install/ then run this installer again." ;; esac fi if ! err=$(docker info 2>&1 >/dev/null); then case "$err" in *"permission denied"*) die "Docker is installed, but your user may not use it. Add yourself to the docker group, log out and in again, then rerun this installer: sudo usermod -aG docker \"\$USER\"" ;; *) if [ "$os" = Darwin ]; then die "Docker is installed but not running. Start Docker Desktop and run this installer again." fi die "Docker is installed but not running. Start it (for example: sudo systemctl enable --now docker) and run this installer again." ;; esac fi if docker compose version >/dev/null 2>&1; then COMPOSE=plugin elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then COMPOSE=standalone else die "Docker Compose is missing. Docker Desktop includes it; on Linux install the Compose plugin (package 'docker-compose-plugin' or 'docker-compose-v2')." fi say "Docker $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'), compose: $COMPOSE" } detect_tz() { tz="${TZ:-}" if [ -z "$tz" ] && [ -r /etc/timezone ]; then tz=$(head -n 1 /etc/timezone); fi if [ -z "$tz" ] && [ -L /etc/localtime ]; then tz=$(readlink /etc/localtime | sed -n 's|.*zoneinfo/||p') fi if [ -z "$tz" ] && command -v timedatectl >/dev/null 2>&1; then tz=$(timedatectl show -p Timezone --value 2>/dev/null || true) fi case "$tz" in ""|*[!A-Za-z0-9_/+-]*) tz="UTC" ;; esac printf '%s' "$tz" } fetch_code() { if command -v git >/dev/null 2>&1 && { [ -d "$DIR/.git" ] || [ ! -e "$DIR" ]; }; then if [ -d "$DIR/.git" ]; then step "Updating $DIR to $VERSION (git)" git -C "$DIR" fetch --quiet --depth 1 origin "$VERSION" \ || die "Could not fetch '$VERSION' from GitHub. Is it an existing tag or branch?" git -C "$DIR" checkout --quiet --force FETCH_HEAD 2>/dev/null \ || die "Could not update the files in $DIR (local changes?). Check 'git -C $DIR status'." else step "Downloading Abhako $VERSION (git clone)" git clone --quiet --depth 1 --branch "$VERSION" "https://github.com/$REPO.git" "$DIR" \ || die "Could not clone '$VERSION' from GitHub. Is it an existing tag or branch?" fi REV=$(git -C "$DIR" rev-parse --short HEAD) else step "Downloading Abhako $VERSION (tarball)" command -v tar >/dev/null 2>&1 || die "Neither git nor tar is available." tmp=$(mktemp -d 2>/dev/null || mktemp -d -t abhako) # shellcheck disable=SC2064 trap "rm -rf '$tmp'" EXIT INT TERM curl -fsSL "https://github.com/$REPO/archive/$VERSION.tar.gz" -o "$tmp/src.tar.gz" \ || die "Could not download '$VERSION' from GitHub. Is it an existing tag or branch?" mkdir -p "$tmp/src" tar -xzf "$tmp/src.tar.gz" -C "$tmp/src" --strip-components 1 [ -f "$tmp/src/docker-compose.yml" ] || die "The download does not look like Abhako." mkdir -p "$DIR" # overwrite the program files, keep .env, data/ and the override file cp -R "$tmp/src/." "$DIR/" REV="$VERSION (tarball)" fi printf '%s\n' "$VERSION" > "$DIR/.abhako-version" } write_env() { if [ -f "$DIR/.env" ]; then say "Keeping your existing .env" return fi tz=$(detect_tz) sed -e "s|^TZ=.*|TZ=$tz|" -e "s|^PUBLIC_URL=.*|PUBLIC_URL=http://localhost:$PORT|" \ "$DIR/.env.example" > "$DIR/.env" chmod 600 "$DIR/.env" say "Created .env (TZ=$tz, PUBLIC_URL=http://localhost:$PORT)" } # The tracked docker-compose.yml publishes 127.0.0.1:3040. Anything else goes into a generated # docker-compose.override.yml, which Compose reads automatically. OVR_MARK="# generated by the Abhako installer" write_override() { ovr="$DIR/docker-compose.override.yml" netmode="${1:-}" if [ -f "$ovr" ] && ! head -n 1 "$ovr" | grep -q "^$OVR_MARK"; then warn "Keeping your own docker-compose.override.yml (not written by this installer)." [ "$PORT" = 3040 ] || warn "ABHAKO_PORT=$PORT is ignored; set the port in that file yourself." return fi if [ -z "$netmode" ] && [ -f "$ovr" ] && grep -q '^ network_mode: bridge' "$ovr"; then netmode=bridge fi uidline="" if [ "$(uname -s)" = Linux ] && [ "$(id -u)" != 1000 ]; then # the image runs as 1000:1000 by default; use your own user so it can write ./data uidline=" user: \"$(id -u):$(id -g)\"" fi if [ "$PORT" = 3040 ] && [ -z "$netmode" ] && [ -z "$uidline" ]; then rm -f "$ovr" return fi { printf '%s (safe to edit: remove the first line and it is left alone)\n' "$OVR_MARK" printf 'services:\n abhako:\n' if [ "$PORT" != 3040 ]; then printf ' ports: !override\n - "127.0.0.1:%s:3040"\n' "$PORT" fi if [ -n "$uidline" ]; then printf '%s\n' "$uidline"; fi if [ -n "$netmode" ]; then printf ' network_mode: bridge\n'; fi } > "$ovr" if [ "$PORT" != 3040 ] && [ "$COMPOSE" = standalone ]; then warn "docker-compose v1 may not understand '!override'. If the port is wrong, install the Compose v2 plugin." fi } compose_up() { log="$DIR/.install.log" rcf="$DIR/.install.rc" (cd "$DIR" && { dc up -d --build 2>&1 && echo 0 > "$rcf" || echo $? > "$rcf"; } | tee "$log") rc=$(cat "$rcf"); rm -f "$rcf" if [ "$rc" != 0 ] && grep -qi "address pool" "$log"; then warn "Docker has no free address range for a new network. Retrying on Docker's default bridge network (written to docker-compose.override.yml)." write_override bridge (cd "$DIR" && { dc up -d --build 2>&1 && echo 0 > "$rcf" || echo $? > "$rcf"; } | tee "$log") rc=$(cat "$rcf"); rm -f "$rcf" fi if [ "$rc" != 0 ] && grep -qi "address already in use\|port is already allocated" "$log"; then die "Port $PORT is already in use. Pick another one, for example: ABHAKO_PORT=3050" fi [ "$rc" = 0 ] || die "docker compose up failed (see above, log in $log)." rm -f "$log" } wait_health() { step "Waiting for Abhako to start" i=0 while [ "$i" -lt 60 ]; do if curl -fsS -o /dev/null "http://127.0.0.1:$PORT/api/health" 2>/dev/null; then say "Healthy." return 0 fi i=$((i + 1)) sleep 2 done die "Abhako did not answer on http://127.0.0.1:$PORT within 2 minutes. Check the logs: cd \"$DIR\" && docker compose logs --tail 50" } detect_port() { # keep a port chosen on an earlier run unless ABHAKO_PORT is given if [ -n "${ABHAKO_PORT:-}" ]; then PORT="$ABHAKO_PORT"; return; fi PORT=3040 ovr="$DIR/docker-compose.override.yml" if [ -f "$ovr" ] && head -n 1 "$ovr" | grep -q "^$OVR_MARK"; then p=$(sed -n 's/.*"127\.0\.0\.1:\([0-9][0-9]*\):3040".*/\1/p' "$ovr" | head -n 1) if [ -n "$p" ]; then PORT="$p"; fi fi return 0 } main() { command -v curl >/dev/null 2>&1 || die "curl is required." [ -n "${HOME:-}" ] || die "HOME is not set." DIR="${ABHAKO_DIR:-$HOME/abhako}" VERSION="${ABHAKO_VERSION:-main}" case "$VERSION" in ""|-*|*[!A-Za-z0-9._/-]*) die "ABHAKO_VERSION must be a tag or branch name." ;; esac detect_port case "$PORT" in ""|*[!0-9]*) die "ABHAKO_PORT must be a number." ;; esac printf '\n\033[1mAbhako installer\033[0m\n' if [ "$VERSION" = main ]; then say "Installs the latest version from GitHub (branch main) into $DIR" else say "Installs version $VERSION from GitHub into $DIR" fi if [ -f "$DIR/docker-compose.yml" ]; then say "Existing installation found: updating (your .env and data/ are kept)." elif [ -e "$DIR" ] && [ -n "$(ls -A "$DIR" 2>/dev/null)" ]; then die "$DIR exists and is not an Abhako installation. Choose another folder with ABHAKO_DIR." fi step "Checking Docker" check_docker fetch_code mkdir -p "$DIR/data" write_env write_override step "Building and starting (this takes a minute the first time)" compose_up wait_health url="http://localhost:$PORT" printf '\n\033[1;32mAbhako is running:\033[0m \033[1m%s\033[0m (version %s)\n\n' "$url" "$REV" say "Open it now: the first account you create becomes the admin." say "" say "Folder: $DIR (settings in .env, your data in data/)" say "Update: run the install command again" say "Stop: cd \"$DIR\" && docker compose down" say "Uninstall: cd \"$DIR\" && docker compose down && docker image rm abhako:local" say " then delete the folder (this deletes your tasks)" say "Use it from other devices: put it behind a reverse proxy with HTTPS, see" say " https://github.com/$REPO#reverse-proxy" } main "$@"