karakeep-linux.sh•17.4 kB
#!/usr/bin/env bash
# v3.0.0
# Copyright 2024-2025
# Author: vhsdream
# Adapted from: The Karakeep installation script from https://github.com/community-scripts/ProxmoxVE
# License: MIT
set -Eeuo pipefail
trap 'catch $LINENO "$BASH_COMMAND"' SIGINT SIGTERM ERR
verbose=0
SPINNER_PID=""
usage() {
header
cat <<EOF
This script has three functions:
'install' Installs $(app) on a clean Debian 12/Ubuntu 24.04 system.
'update' Checks for, and installs updates for $(app) on a system that previously installed $(app) by running this script.
'migrate' Performs a full Hoarder ==> $(app) migration on a previous install. Also checks for updates afterwards.
Available options:
-h, --help Print this help and exit
-v, --verbose Print script StandardOutput
--no-color Disable colours
This script WILL NOT update or migrate a $(app)/Hoarder install that was installed in any other way.
Please back up your existing installation before running this script!
If you encounter any errors please create a GitHub issue (https://github.com/karakeep-app/karakeep/issues) and tag vhsdream
Usage: bash $(basename "${BASH_SOURCE[0]}") [-h] [-v] [install|update|migrate]
EOF
exit
}
header() {
t_width=$(tput cols 2>/dev/null)
if [[ "$t_width" -gt 115 ]]; then
echo -e "$(
cat <<EOF
${YELLOW}
oMMWWWWWMMMMMMMMMMMo .... ...
oMM 'MMMMMMMMMMo xMMX ;MMM.
oMM 'MMOxkXMMMMo kMMX ;MMM.
oMM 'MM' .OMMo kMMX ,;;;. .;lll:. ';;. :l ,cloc, ;MMM. .;;;. .:loc, .:llc' ;;; .clc,
oMM 'MM' .MMo kMMX .0MMX, dMMXKWMMO 0MM0NMM'WMNKXMMW' ;MMM. dMMWl 'KMW0OXMWl ;NMW0ONMN: .MMMXMWMMMWl
oMM .MM' MMo kMMNlWMWl ...',NMM: 0MMWc. '.',xMMK ;MMMlNMMk. .WMW:'''KMMc;MMW,'''XMM,.MMMk. ;WMM:
oMM .MM' MMo kMMWKMMX. .xWMN0ONMMo 0MMO cXMWKOKMMW ;MMM0MMW: cMMMXXXXXXXddMMWXXXXXXXc.MMM. KMMx
oMM .MM'.ol. MMo kMMX xMMWl dMM0 .XMMo 0MMO .MMM. dMMW ;MMM.:WMMO .NMMo...lc. .WMWc...o: .MMMX:',xMMM'
oMMXKKKKXMMWMMMMNMMo kMMX :WMM0,0MMMWX0MMo 0MMO dWMMWWxMMW ;MMM. .KMMN,.dNMMMMMNo .kWMMMMMXc .MMM0WMMMM0'
.:cccccccccccccccc:. ... ... ..'.. .''. .MMM, ...
.MMM,
.OOO.${CLR}
The ${PURPLE}Bookmark${CLR} ${RED}Everything${CLR} ${YELLOW}App${CLR}
script version ${GREEN}3.0.0${CLR}
EOF
)"
fi
}
# Handling output suppression
set_verbosity() {
if [ "$verbose" -eq 1 ]; then
shh=""
else
shh="silent_running"
fi
}
silent_running() {
"$@" >/dev/null 2>&1
}
set_verbosity
# Colour handling
setup_colours() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
CLR='\033[0m' GREEN='\033[0;32m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m' RED='\033[0;31m'
else
CLR='' GREEN='' PURPLE='' CYAN='' YELLOW='' RED=''
fi
}
setup_colours
# Flash and bling
app() {
echo -e "${CLR}${PURPLE}Karakeep${CLR}"
}
spinner() {
local frames=('▹▹▹▹▹' '▸▹▹▹▹' '▹▸▹▹▹' '▹▹▸▹▹' '▹▹▹▸▹' '▹▹▹▹▸')
local spin_i=0
local interval=0.1
printf "\e[?25l"
while true; do
printf "\r${PURPLE}%s${CLR}" "${frames[spin_i]}"
spin_i=$(((spin_i + 1) % ${#frames[@]}))
sleep "$interval"
done
}
msg_start() {
printf " "
echo >&1 -ne "${CYAN}${1-}${CLR}"
spinner &
SPINNER_PID=$!
}
msg_done() {
if [[ -n "$SPINNER_PID" ]] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi
printf "\e[?25h"
local msg="${1-}"
echo -e "\r"
echo >&1 -e "${GREEN}Done ✔ ${msg}${CLR}"
}
msg_info() {
if [[ -n "$SPINNER_PID" ]] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi
printf "\e[?25h"
echo >&1 -e "${1-}\r"
}
# Exception and error handling
msg_err() {
if [[ -n "$SPINNER_PID" ]] && ps -p "$SPINNER_PID" >/dev/null; then kill "$SPINNER_PID" >/dev/null; fi
echo >&2 -e "${RED}${1-}${CLR}"
}
die() {
local err=$1
local code=${2-1}
msg_err "$err"
exit "$code"
}
catch() {
local code=$?
local line=$1
local command=$2
msg_err "Caught error in line $line: exit code $code: while executing $command"
}
parse_params() {
while :; do
case "${1-}" in
-h | --help) usage ;;
-v | --verbose) verbose=1 && set_verbosity ;;
--no-color) NO_COLOR=1 ;;
-?*) die "Unknown flag: $1. Use -h|--help for help" ;;
*) break ;;
esac
shift
done
args=("$@")
[[ ${#args[@]} -eq 0 ]] && die "Missing script arguments. Use -h|--help for help"
return 0
}
parse_params "$@"
OS="$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)"
INSTALL_DIR=/opt/karakeep
APP_DIR="$INSTALL_DIR"/apps
export DATA_DIR=/var/lib/karakeep
CONFIG_DIR=/etc/karakeep
LOG_DIR=/var/log/karakeep
ENV_FILE=${CONFIG_DIR}/karakeep.env
install_karakeep() {
header
msg_info "$(app) installation for Debian 12/Ubuntu 24.04" && sleep 3
echo -e "\n"
msg_start "Installing dependencies..."
$shh apt-get install --no-install-recommends -y \
g++ \
curl \
build-essential \
sudo \
unzip \
gnupg \
graphicsmagick \
ghostscript \
ca-certificates
if [[ "$OS" == "noble" ]]; then
$shh apt-get install -y software-properties-common
$shh add-apt-repository ppa:xtradeb/apps -y
$shh apt-get install --no-install-recommends -y ungoogled-chromium yt-dlp
ln -s /usr/bin/ungoogled-chromium /usr/bin/chromium
else
$shh apt-get install --no-install-recommends -y chromium
curl -fsSL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/bin/yt-dlp && chmod +x /usr/bin/yt-dlp
fi
curl -fsSL https://github.com/Y2Z/monolith/releases/latest/download/monolith-gnu-linux-x86_64 -o /usr/bin/monolith && chmod +x /usr/bin/monolith
curl -fsSLO https://github.com/meilisearch/meilisearch/releases/latest/download/meilisearch.deb
DEBIAN_FRONTEND=noninteractive $shh apt-get install -y ./meilisearch.deb && rm ./meilisearch.deb
msg_done "Installed dependencies"
msg_start "Installing Node.js..."
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >/etc/apt/sources.list.d/nodesource.list
$shh apt-get update
$shh apt-get install -y nodejs
# https://github.com/karakeep-app/karakeep/issues/967
$shh npm install -g corepack@0.31.0
msg_done "Installed Node.js"
msg_start "Installing $(app)${CYAN}, please wait...${CLR}"
mkdir -p {"$DATA_DIR","$CONFIG_DIR","$LOG_DIR"}
M_DATA_DIR=/var/lib/meilisearch
M_CONFIG_FILE=/etc/meilisearch.toml
RELEASE="$(curl -s https://api.github.com/repos/karakeep-app/karakeep/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')"
cd /tmp
curl -fsSLO "https://github.com/karakeep-app/karakeep/archive/refs/tags/v${RELEASE}.zip"
unzip -q v"$RELEASE".zip
mv karakeep-"$RELEASE" "$INSTALL_DIR" && cd "$APP_DIR"/web
corepack enable
export NEXT_TELEMETRY_DISABLED=1
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD="true"
export CI="true"
$shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$APP_DIR"/workers
$shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$APP_DIR"/cli
$shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$INSTALL_DIR"/packages/db
$shh pnpm migrate
msg_done "Installed $(app)"
msg_start "Creating configuration files..."
cd "$INSTALL_DIR"
MASTER_KEY="$(openssl rand -base64 12)"
cat <<EOF >"$M_CONFIG_FILE"
env = "production"
master_key = "$MASTER_KEY"
db_path = "${M_DATA_DIR}/data"
dump_dir = "${M_DATA_DIR}/dumps"
snapshot_dir = "${M_DATA_DIR}/snapshots"
no_analytics = true
EOF
chmod 600 "$M_CONFIG_FILE"
karakeep_SECRET="$(openssl rand -base64 36 | cut -c1-24)"
cat <<EOF >"$ENV_FILE"
NODE_ENV=production
SERVER_VERSION=${RELEASE}
NEXTAUTH_SECRET="${karakeep_SECRET}"
NEXTAUTH_URL="http://localhost:3000"
DATA_DIR=${DATA_DIR}
MEILI_ADDR="http://127.0.0.1:7700"
MEILI_MASTER_KEY="${MASTER_KEY}"
BROWSER_WEB_URL="http://127.0.0.1:9222"
# CRAWLER_VIDEO_DOWNLOAD=true
# CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE=
# OPENAI_API_KEY=
# OLLAMA_BASE_URL=
# INFERENCE_TEXT_MODEL=
# INFERENCE_IMAGE_MODEL=
EOF
chmod 600 "$ENV_FILE"
echo "$RELEASE" >"$INSTALL_DIR"/version.txt
msg_done "Configuration complete"
msg_start "Creating users and modifying permissions..."
useradd -U -s /usr/sbin/nologin -r -m -d "$M_DATA_DIR" meilisearch
useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" karakeep
chown meilisearch:meilisearch "$M_CONFIG_FILE"
touch "$LOG_DIR"/{karakeep-workers.log,karakeep-web.log}
chown -R karakeep:karakeep "$INSTALL_DIR" "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR"
msg_done "Users created, permissions modified"
msg_start "Creating service files and configuring log rotation..."
cat <<EOF >/etc/systemd/system/meilisearch.service
[Unit]
Description=MeiliSearch is a RESTful search API
Documentation=https://docs.meilisearch.com/
After=network.target
[Service]
User=meilisearch
Group=meilisearch
Restart=on-failure
WorkingDirectory=${M_DATA_DIR}
ExecStart=/usr/bin/meilisearch --config-file-path ${M_CONFIG_FILE}
NoNewPrivileges=true
ProtectHome=true
ReadWritePaths=${M_DATA_DIR}
ProtectSystem=full
ProtectHostname=true
ProtectControlGroups=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectKernelLogs=true
ProtectClock=true
LockPersonality=true
RestrictRealtime=yes
RestrictNamespaces=yes
MemoryDenyWriteExecute=yes
PrivateDevices=yes
PrivateTmp=true
CapabilityBoundingSet=
RemoveIPC=true
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/karakeep-browser.service
[Unit]
Description=Karakeep headless browser
After=network.target
[Service]
User=root
Restart=on-failure
ExecStart=/usr/bin/chromium --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --hide-scrollbars
TimeoutStopSec=5
SyslogIdentifier=karakeep-browser
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/karakeep-workers.service
[Unit]
Description=Karakeep workers
Wants=network.target karakeep-browser.service meilisearch.service
After=network.target karakeep-browser.service meilisearch.service
[Service]
User=karakeep
Group=karakeep
Restart=always
EnvironmentFile=${ENV_FILE}
WorkingDirectory=${APP_DIR}/workers
ExecStart=/usr/bin/node dist/index.js
StandardOutput=append:${LOG_DIR}/karakeep-workers.log
StandardError=append:${LOG_DIR}/karakeep-workers.log
TimeoutStopSec=5
SyslogIdentifier=karakeep-workers
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/karakeep-web.service
[Unit]
Description=Karakeep web
Wants=network.target karakeep-workers.service
After=network.target karakeep-workers.service
[Service]
User=karakeep
Group=karakeep
Restart=on-failure
EnvironmentFile=${ENV_FILE}
WorkingDirectory=${APP_DIR}/web
ExecStart=/usr/bin/pnpm start
StandardOutput=append:${LOG_DIR}/karakeep-web.log
StandardError=append:${LOG_DIR}/karakeep-web.log
TimeoutStopSec=5
SyslogIdentifier=karakeep-web
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/karakeep.target
[Unit]
Description=Karakeep Services
After=network-online.target
Wants=meilisearch.service karakeep-browser.service karakeep-workers.service karakeep-web.service
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/logrotate.d/karakeep
/var/log/karakeep/*.log
{
su karakeep karakeep
weekly
missingok
rotate 4
compress
notifempty
}
EOF
msg_done "Service files created, log rotation configured"
msg_start "Enabling and starting services, please wait..." && sleep 3
systemctl enable -q --now meilisearch.service karakeep.target
service_check install
exit 0
}
update_karakeep() {
msg_info "${YELLOW}Checking for an update...${CLR}" && sleep 1
if [[ ! -d ${INSTALL_DIR} ]]; then
die "Is Karakeep even installed?"
fi
RELEASE="$(curl -s https://api.github.com/repos/karakeep-app/karakeep/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')"
PREV_RELEASE="$(cat "$INSTALL_DIR"/version.txt)"
if [[ "$RELEASE" != "$PREV_RELEASE" ]]; then
if [[ "$(systemctl is-active karakeep-web)" == "active" ]]; then
msg_start "Stopping affected services..."
systemctl stop karakeep-web karakeep-workers
msg_done "Stopped services"
fi
if [[ "$OS" == "bookworm" ]]; then
$shh yt-dlp -U
fi
msg_start "Updating $(app) ${CYAN}to v${RELEASE}...${CLR}"
sed -i "s|SERVER_VERSION=${PREV_RELEASE}|SERVER_VERSION=${RELEASE}|" "$ENV_FILE"
rm -R "$INSTALL_DIR"
cd /tmp
curl -fsSLO "https://github.com/karakeep-app/karakeep/archive/refs/tags/v${RELEASE}.zip"
unzip -q v"$RELEASE".zip
mv karakeep-"$RELEASE" "$INSTALL_DIR"
# https://github.com/karakeep-app/karakeep/issues/967
if [[ "$(corepack -v)" < "0.31.0" ]]; then
$shh npm install -g corepack@0.31.0
fi
corepack enable
export NEXT_TELEMETRY_DISABLED=1
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD="true"
export CI="true"
cd "$APP_DIR"/web && $shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$APP_DIR"/workers && $shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$APP_DIR"/cli && $shh pnpm i --frozen-lockfile
$shh pnpm build
cd "$INSTALL_DIR"/packages/db && $shh pnpm migrate
echo "$RELEASE" >"$INSTALL_DIR"/version.txt
chown -R karakeep:karakeep "$INSTALL_DIR" "$DATA_DIR"
msg_done "Updated $(app) ${CYAN}to v${RELEASE}${CLR}"
msg_start "Restarting services and cleaning up..."
rm /tmp/v"$RELEASE".zip
# Migrations
# 0.27 changed the worker compiled file from .mjs to .js
if grep -q '^ExecStart=/usr/bin/node\s\+dist/index\.mjs$' /etc/systemd/system/karakeep-workers.service; then
sed -i -E 's#^(ExecStart=/usr/bin/node\s+dist/)index\.mjs$#\1index.js#' /etc/systemd/system/karakeep-workers.service
systemctl daemon-reload
fi
# End migrations
systemctl restart karakeep.target
service_check update
else
msg_info "${YELLOW}No update required.${CLR}"
fi
exit 0
}
migrate_karakeep() {
if [[ ! -d /opt/karakeep ]]; then
msg_start "Migrating your Hoarder installation to $(app), ${CYAN}then checking for an update...${CLR}"
systemctl stop hoarder-browser hoarder-workers hoarder-web
sed -i -e "s|hoarder|karakeep|g" /etc/hoarder/hoarder.env /etc/systemd/system/hoarder-{browser,web,workers}.service /etc/systemd/system/hoarder.target \
-e "s|Hoarder|Karakeep|g" /etc/systemd/system/hoarder-{browser,web,workers}.service /etc/systemd/system/hoarder.target
for path in /etc/systemd/system/hoarder*.service; do
new_path="${path//hoarder/karakeep}"
mv "$path" "$new_path"
done
mv /etc/systemd/system/hoarder.target /etc/systemd/system/karakeep.target
mv /opt/hoarder "$INSTALL_DIR"
mv /var/lib/hoarder "$DATA_DIR"
mv /etc/hoarder "$CONFIG_DIR"
mv /var/log/hoarder "$LOG_DIR"
mv "$CONFIG_DIR"/hoarder.env "$ENV_FILE"
mv "$LOG_DIR"/hoarder-web.log "$LOG_DIR"/karakeep-web.log
mv "$LOG_DIR"/hoarder-workers.log "$LOG_DIR"/karakeep-workers.log
usermod -l karakeep hoarder -d "$INSTALL_DIR"
groupmod -n karakeep hoarder
chown -R karakeep:karakeep "$INSTALL_DIR" "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR"
systemctl daemon-reload
systemctl -q enable --now karakeep.target
service_check migrate
else
msg_info "${YELLOW}There is no need for a migration: $(app) ${YELLOW}is already installed.${CLR}"
fi
}
service_check() {
local services=("karakeep-browser" "karakeep-workers" "karakeep-web" "meilisearch")
readarray -t status < <(for service in "${services[@]}"; do
systemctl is-active "$service" | grep "^active" -
done)
if [[ "${#status[@]}" -eq 4 ]]; then
if [[ "$1" == "install" ]]; then
msg_done "$(app) ${CYAN}is running!${CLR}"
sleep 1
LOCAL_IP="$(hostname -I | awk '{print $1}')"
msg_info "Go to ${YELLOW}http://$LOCAL_IP:3000 ${CLR}to create your account"
msg_info "Change settings at ${YELLOW}'/etc/karakeep/karakeep.env'${CLR}"
exit 0
elif [[ "$1" == "update" ]]; then
msg_done "$(app) ${CYAN}is updated and running!${CLR}"
sleep 1
exit 0
elif [[ "$1" == "migrate" ]]; then
msg_done "$(app) ${CYAN}migration complete!${CLR}"
sleep 1
exit 0
fi
else
die "Some services have failed. Check 'journalctl -xeu <service-name>' to see what is going on"
fi
}
[ "$(id -u)" -ne 0 ] && die "This script requires root privileges. Please run with sudo or as the root user."
case "${args[0]}" in
install)
install_karakeep
;;
update)
update_karakeep
;;
migrate)
migrate_karakeep && update_karakeep
;;
*)
die "Unknown command. Choose 'install', 'update' or 'migrate.'"
;;
esac