Skip to content

Booklore Installer #61

@Pinewood538

Description

@Pinewood538

Booklore only provides a docker image for installation, so this was a bit tricky.
The basic steps are (updated as of 02/27/2026):

  1. Install the dependencies: Java, MariaDB, crane (for help extracting the docker image)
  2. Setup MariaDB
  3. Extract the booklore jar from the docker image using crane (to avoid having to build Booklore locally)
  4. Initialize booklore

-Unfortunately MariaDB specifically is required, so the installation directory is bulky (about 1GB, of which MariaDB is ~730MB). I've tried to slim down MariaDB after the download by removing some unused plugins and binaries.

-It is possible to extract the necessary booklore files from their docker image without using crane, but the logic was tricky and the download links seem fragile. crane makes the logic a lot simpler and future-proof if booklore decides to change something on their end.

Thanks for helping in the discord with how to get started. I did my best to follow the advice given (not binding to localhost, etc). I also tried to follow along the basic format of the other scripts here and stole some code from there (like the little function to generate an open port). I looked at the ports other apps were using here and tried to pick a range outside of them.

I'm a novice at this stuff, so please give feedback or adjust it however you like before merging.

booklore.sh (updated 02/27/2026)
#!/bin/bash

mkdir -p "$HOME/.logs/"
export log="$HOME/.logs/booklore.log"
touch "$log"
SUBNET_IP=$(cat "$HOME/.install/subnet.lock")
BASE="$HOME/booklore"

# crane is helpful to extract the necessary files from the Booklore docker image.
# it's possible without it, but the hardcoded links seemed fragile, and crane makes
# the logic MUCH easier anyway.
function crane_install() {
    if [[ -x "$BASE/crane" ]]; then
        echo "crane is already installed. Skipping download."
        return 0
    fi

    echo "Downloading crane (for Booklore docker extraction)..."
    local dl_file=$(mktemp)
    curl -sLf "https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz" -o "$dl_file"
    tar -xzf "$dl_file" -C "$BASE" crane
    rm -f "$dl_file"
}

function java_install() {
    # get the docker config using crane and extract the required java version
    local DOCKER_CONFIG=$("$BASE/crane" config ghcr.io/booklore-app/booklore:latest 2>> "$log")
    local JAVA_VER=$(echo "$DOCKER_CONFIG" | grep -oP '"JAVA_VERSION[^"]*"' | grep -oP '\d+' | head -n 1)

    if [[ -z "$JAVA_VER" ]]; then
        echo "ERROR: Could not detect Java version from the Booklore docker image."
        exit 1
    fi

    if [[ -x "$BASE/java/bin/java" ]] && "$BASE/java/bin/java" -version 2>&1 | grep -q "version \"$JAVA_VER"; then
        echo "Java $JAVA_VER is already installed. Skipping download."
        return 0
    fi

    echo "Detected Booklore requires Java $JAVA_VER. Downloading..."

    rm -rf "$BASE/java"
    mkdir -p "$BASE/java"

    local dl_file=$(mktemp)
    curl -sLf "https://api.adoptium.net/v3/binary/latest/${JAVA_VER}/ga/linux/x64/jre/hotspot/normal/eclipse" -o "$dl_file"
    tar -xzf "$dl_file" --strip-components=1 -C "$BASE/java"
    rm -f "$dl_file"
}

function maria_install() {
    echo "Downloading MariaDB..."
    local dl_file=$(mktemp)
    curl -sLf "https://archive.mariadb.org/mariadb-12.3.1/bintar-linux-systemd-x86_64/mariadb-12.3.1-linux-systemd-x86_64.tar.gz" -o "$dl_file"
    tar -xzf "$dl_file" --strip-components=1 -C "$BASE/mariadb"
    rm -f "$dl_file"

    # remove useless directories (tests and such)
    rm -rf "$BASE/mariadb"/{mariadb-test,sql-bench,include,man,docs}

    # remove massive storage engines not used by Booklore (MyRocks, Mroonga, etc.)
    rm -f "$BASE/mariadb/lib/plugin"/{ha_rocksdb.so,ha_mroonga.so,ha_spider.so,ha_connect.so,ha_oqgraph.so,libgalera_smm.so}

    # remove some unused binaries (mariadb-dump is kept instead of mariadb-backup)
    rm -f "$BASE/mariadb/bin/"{mariadb-backup,garbd,mariadb-ldb,sst_dump,mariadb-slap,mariadb-test,mariadb-client-test,aria_s3_copy,mbstream}
}

function port() {
    LOW_BOUND=$1
    UPPER_BOUND=$2
    comm -23 <(seq ${LOW_BOUND} ${UPPER_BOUND} | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 1
}

function _docker_extract() {
    local tmp=$(mktemp -d)
    trap 'rm -rf "$tmp"' RETURN
    echo "Extracting Booklore via crane..."

    # extract the booklore .jar from the docker image using crane
    "$BASE/crane" export ghcr.io/booklore-app/booklore:latest - | tar -xvf - -C "$tmp" --wildcards "app/*.jar" >> "$log" 2>&1

    # verify and move the file to the install directory. crane gives full paths so the jar is likely at /app/app.jar
    if [[ -f "$tmp/app/app.jar" ]]; then
        echo "Files verified. Installing to $BASE..."
        mkdir -p "$BASE/app"

        # Backup old jar
        [[ -f "$BASE/app/app.jar" ]] && mv "$BASE/app/app.jar" "$BASE/app/app.jar.bak"

        # Move new jar
        mv "$tmp/app/app.jar" "$BASE/app/app.jar"
        return 0
    else
        echo "ERROR: crane export failed or file structure is different than expected. script needs to be updated."
        return 1
    fi
}

function _install() {
    mkdir -p "$BASE/"{app,bookdrop,books,config,java,tmp,mariadb,mariadb/data,mariadb/tmp}

    # dependency installs
    crane_install
    java_install
    maria_install

    DB_PORT=$(port 17000 17400)
    APP_PORT=$(port 17401 17800)
    DB_PASS=$(openssl rand -hex 16)

    # configure MariaDB
    echo "Setting up MariaDB on $SUBNET_IP:$DB_PORT..."
    cat <<EOF > "$BASE/mariadb/my.cnf"
[mysqld]
port = $DB_PORT
bind-address = $SUBNET_IP
socket = $BASE/mariadb/mysql.sock
datadir = $BASE/mariadb/data
tmpdir = $BASE/mariadb/tmp
skip-log-bin
innodb_log_file_size = 16M
innodb_buffer_pool_size = 128M
pid-file = $BASE/mariadb/pid
log-error = $BASE/mariadb/error.log
EOF

    # initialize database
    "$BASE/mariadb/scripts/mariadb-install-db" --defaults-file="$BASE/mariadb/my.cnf" --basedir="$BASE/mariadb" >> "$log" 2>&1

    cat <<EOF > "$HOME/.config/systemd/user/booklore-db.service"
[Unit]
Description=Booklore MariaDB
[Service]
ExecStart=$BASE/mariadb/bin/mariadbd --defaults-file=$BASE/mariadb/my.cnf
Restart=on-failure
[Install]
WantedBy=default.target
EOF

    systemctl --user daemon-reload
    systemctl --user enable --now booklore-db

    # wait for the DB to be ready
    if ! "$BASE/mariadb/bin/mariadb-admin" --socket="$BASE/mariadb/mysql.sock" --wait=30 ping --silent; then
        echo "ERROR: DB failed to start."; tail -n 20 "$BASE/mariadb/error.log"; exit 1
    fi

    # initialize and try root user first, then current user if that fails
    SQL="CREATE DATABASE IF NOT EXISTS booklore; GRANT ALL PRIVILEGES ON booklore.* TO 'booklore'@'%' IDENTIFIED BY '$DB_PASS'; FLUSH PRIVILEGES;"
    "$BASE/mariadb/bin/mariadb" --socket="$BASE/mariadb/mysql.sock" -u root -e "$SQL" 2>/dev/null || \
    "$BASE/mariadb/bin/mariadb" --socket="$BASE/mariadb/mysql.sock" -u "$(whoami)" -e "$SQL"

    # extract jar from Booklore docker via crane
    if ! _docker_extract; then
        exit 1
    fi

    # Booklore configuration
    cat <<EOF > "$BASE/application.yml"
server:
  port: $APP_PORT
spring:
  datasource:
    url: jdbc:mariadb://$SUBNET_IP:$DB_PORT/booklore
    username: booklore
    password: $DB_PASS
app:
  path-config: $BASE/config
  bookdrop-folder: $BASE/bookdrop
EOF

    cat <<EOF > "$HOME/.config/systemd/user/booklore.service"
[Unit]
Description=Booklore
After=booklore-db.service
[Service]
WorkingDirectory=$BASE
ExecStart=$BASE/java/bin/java -Xmx1g -Djava.io.tmpdir=$BASE/tmp -jar $BASE/app/app.jar --spring.config.additional-location=file:$BASE/application.yml
Restart=on-failure
[Install]
WantedBy=default.target
EOF

    systemctl --user enable --now booklore
    touch "$HOME/.install/.booklore.lock"
    echo "Booklore has been installed and should be running at http://$(hostname -f):$APP_PORT (give it a minute or so to initialize)"
}

function _remove() {
    systemctl --user stop booklore booklore-db
    systemctl --user disable booklore booklore-db

    rm -f "$HOME/.config/systemd/user/booklore"*.service
    rm -f "$HOME/.install/.booklore.lock"
    rm -rf "$BASE/"

    systemctl --user daemon-reload
    echo "Uninstall Complete."
}

function _upgrade() {
    if [ ! -f "$HOME/.install/.booklore.lock" ]; then
        echo "Booklore is not installed."
        exit 1
    fi

    echo "Upgrading Booklore..."

    systemctl --user stop booklore
    crane_install
    if _docker_extract; then
        # if extraction works, update java to the required version
        java_install
        systemctl --user start booklore
        echo "Upgrade complete. Services restarted."
    else
        # if extraction fails, restart the current Booklore
        echo "Extraction failed. Restarting the old version..."
        systemctl --user start booklore
        exit 1
    fi
}

echo 'This is unsupported software. You will not get help with this, please answer `yes` if you understand and wish to proceed'
if [[ -z ${eula} ]]; then
    read -r eula
fi

if ! [[ $eula =~ yes ]]; then
  echo "You did not accept the above. Exiting..."
  exit 1
else
  echo "Proceeding with installation"
fi

echo "Welcome to the Booklore Installer"
echo ""
echo "What do you like to do?"
echo "Logs are stored at ${log}"
echo "install = Install Booklore"
echo "upgrade = Upgrade Booklore to the latest version"
echo "uninstall = Completely removes Booklore (including the database! backup yourself if needed)"
echo "exit = Exit"

while true; do
    read -r -p "Enter choice: " choice
    case $choice in
        "install")
            _install
            break
            ;;
        "upgrade")
            _upgrade
            break
            ;;
        "uninstall")
            _remove
            break
            ;;
        "exit")
            break
            ;;
        *)
            echo "Unknown Option."
            ;;
    esac
done
exit

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions