Files
Mako/deploy/install.sh
T
2026-11-01 00:06:51 -06:00

230 lines
8.2 KiB
Bash

#!/bin/sh
# Installs (or upgrades) Nurse as a systemd service.
#
# curl -fsSL https://git.veillax.com/Veillax/nurse/raw/branch/main/deploy/install.sh | sudo sh
# sudo sh install.sh [--start] [--version v0.1.0] [--listen 127.0.0.1:8490] [--source]
# sudo sh install.sh --uninstall [--purge]
#
# What it does:
# 1. downloads the release archive for this machine and checks its SHA-256 (or, with --source,
# clones the repository and builds it with Go),
# 2. installs the binary to /usr/local/bin/nurse,
# 3. creates a "nurse" user, /etc/nurse (settings) and /var/lib/nurse (data),
# 4. writes /etc/nurse/nurse.env and the systemd service,
# 5. tells you what to do next: edit the env file, start the service, watch the log.
#
# Running it again upgrades the binary and keeps your settings; a service that is already running
# is restarted. Add --start to start it straight away instead of editing the env file first.
set -eu
REPO_URL=${NURSE_REPO_URL:-https://git.veillax.com/Veillax/nurse}
API_URL=${NURSE_API_URL:-https://git.veillax.com/api/v1/repos/Veillax/nurse}
BIN=/usr/local/bin/nurse
CONF_DIR=/etc/nurse
DATA_DIR=/var/lib/nurse
ENV_FILE=$CONF_DIR/nurse.env
CONF_FILE=$CONF_DIR/nurse.json
UNIT=/etc/systemd/system/nurse.service
USER_NAME=nurse
VERSION=""
LISTEN="127.0.0.1:8490"
SOURCE=0
START=0
UNINSTALL=0
PURGE=0
say() { printf '%s\n' "$*"; }
die() { printf 'install.sh: %s\n' "$*" >&2; exit 1; }
while [ $# -gt 0 ]; do
case $1 in
--version) VERSION=${2:?--version needs a value}; shift ;;
--listen) LISTEN=${2:?--listen needs a value}; shift ;;
--source) SOURCE=1 ;;
--start) START=1 ;;
--uninstall) UNINSTALL=1 ;;
--purge) PURGE=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) die "unknown option $1" ;;
esac
shift
done
[ "$(id -u)" -eq 0 ] || die "run as root (sudo sh install.sh)"
[ "$(uname -s)" = Linux ] || die "this installer is for Linux; on Windows, download the .zip from the releases page"
command -v systemctl >/dev/null 2>&1 || HAVE_SYSTEMD=0
HAVE_SYSTEMD=${HAVE_SYSTEMD:-1}
if [ "$UNINSTALL" -eq 1 ]; then
if [ "$HAVE_SYSTEMD" -eq 1 ]; then
systemctl disable --now nurse 2>/dev/null || true
rm -f "$UNIT"
systemctl daemon-reload
fi
rm -f "$BIN"
if [ "$PURGE" -eq 1 ]; then
rm -rf "$CONF_DIR" "$DATA_DIR"
userdel "$USER_NAME" 2>/dev/null || true
say "Nurse removed, including its settings and data."
else
say "Nurse removed. Settings ($CONF_DIR) and data ($DATA_DIR) were kept; add --purge to delete them."
fi
exit 0
fi
fetch() { # fetch URL DEST
if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1"
else die "need curl or wget"; fi
}
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# ---- 1. get the binary ----
if [ "$SOURCE" -eq 1 ]; then
command -v git >/dev/null 2>&1 || die "--source needs git"
command -v go >/dev/null 2>&1 || die "--source needs Go (https://go.dev/dl/)"
say "Cloning $REPO_URL ..."
git clone --quiet ${VERSION:+--branch "$VERSION"} --depth 1 "$REPO_URL" "$TMP/src"
(cd "$TMP/src" && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION:-source}" -o "$TMP/nurse" ./cmd/nurse)
else
case $(uname -m) in
x86_64|amd64) ARCH=amd64 ;;
aarch64|arm64) ARCH=arm64 ;;
*) die "no release build for $(uname -m); try --source" ;;
esac
if [ -z "$VERSION" ]; then
fetch "$API_URL/releases/latest" "$TMP/latest.json"
VERSION=$(sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP/latest.json" | head -n 1)
[ -n "$VERSION" ] || die "could not find the latest release; use --version or --source"
fi
ARCHIVE="nurse_${VERSION}_linux_${ARCH}.tar.gz"
say "Downloading Nurse $VERSION ($ARCH) ..."
fetch "$REPO_URL/releases/download/$VERSION/$ARCHIVE" "$TMP/$ARCHIVE"
fetch "$REPO_URL/releases/download/$VERSION/SHA256SUMS" "$TMP/SHA256SUMS"
want=$(grep " $ARCHIVE\$" "$TMP/SHA256SUMS" | cut -d' ' -f1)
[ -n "$want" ] || die "$ARCHIVE is not listed in SHA256SUMS"
got=$(sha256sum "$TMP/$ARCHIVE" | cut -d' ' -f1)
[ "$want" = "$got" ] || die "checksum mismatch for $ARCHIVE; refusing to install"
tar -xzf "$TMP/$ARCHIVE" -C "$TMP"
cp "$TMP/nurse_${VERSION}_linux_${ARCH}/nurse" "$TMP/nurse"
fi
# ---- 2. install it ----
UPGRADE=0
[ -x "$BIN" ] && UPGRADE=1
if [ "$UPGRADE" -eq 1 ] && [ "$HAVE_SYSTEMD" -eq 1 ]; then systemctl stop nurse 2>/dev/null || true; fi
install -m 0755 "$TMP/nurse" "$BIN"
say "Installed $("$BIN" version) to $BIN"
# ---- 3. user and folders ----
id "$USER_NAME" >/dev/null 2>&1 || useradd --system --home-dir "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \
|| adduser --system --home "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME" 2>/dev/null \
|| die "could not create the $USER_NAME user"
# The dashboard rewrites its settings file, so the service must own the folder that holds it.
mkdir -p "$CONF_DIR" "$DATA_DIR"
chown "$USER_NAME" "$CONF_DIR" "$DATA_DIR"
chmod 0750 "$CONF_DIR" "$DATA_DIR"
# ---- 4. settings and environment file ----
if [ ! -f "$CONF_FILE" ]; then
printf '{\n "listen": "%s",\n "data_dir": "%s"\n}\n' "$LISTEN" "$DATA_DIR" > "$CONF_FILE"
chown "$USER_NAME" "$CONF_FILE"
chmod 0600 "$CONF_FILE"
fi
if [ ! -f "$ENV_FILE" ]; then
cat > "$ENV_FILE" <<EOF
# Environment for the nurse service. Restart it after editing: systemctl restart nurse
#
# Nurse has no passwords. You sign in with Authentik, so fill in the four lines below.
# In Authentik: Applications > Providers > Create > OAuth2/OpenID Provider (client type
# Confidential, scopes openid, profile and email), allow the redirect URI
# <NURSE_PUBLIC_URL>/auth/callback
# then create an Application that uses it. Check the result with: nurse check
# Where you open Nurse in a browser, such as https://notes.example.com (no path).
NURSE_PUBLIC_URL=
# The provider's issuer, such as https://auth.example.com/application/o/nurse/
NURSE_OIDC_ISSUER=
NURSE_OIDC_CLIENT_ID=
NURSE_OIDC_CLIENT_SECRET=
# Optional. Only these people may sign in (emails or usernames, separated by commas). Left empty,
# everyone Authentik lets into the application gets a private notebook of their own.
#NURSE_ALLOWED=you@example.com
# Where the settings file is (it holds the signing secret).
NURSE_CONFIG=$CONF_FILE
# Optional. These override what the settings file says.
#NURSE_LISTEN=$LISTEN
#NURSE_DATA=$DATA_DIR
EOF
chmod 0600 "$ENV_FILE" # systemd reads it as root, so the service user does not need access
say "Created $ENV_FILE."
fi
# ---- 5. service ----
if [ "$HAVE_SYSTEMD" -eq 0 ]; then
say "systemd was not found, so no service was installed. Run it yourself with:"
say " sudo -u $USER_NAME env \$(grep -v '^#' $ENV_FILE | xargs) $BIN serve -config $CONF_FILE"
exit 0
fi
cat > "$UNIT" <<EOF
[Unit]
Description=Nurse, a private notebook
After=network-online.target
Wants=network-online.target
[Service]
User=$USER_NAME
EnvironmentFile=$ENV_FILE
WorkingDirectory=$DATA_DIR
ExecStart=$BIN serve
Restart=on-failure
RestartSec=5
# Settings that need fixing by hand exit with status 2; do not loop on those.
RestartPreventExitStatus=2
# Nurse only needs the network and its own folders.
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=$CONF_DIR $DATA_DIR
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable nurse >/dev/null 2>&1
if [ "$START" -eq 1 ] || systemctl is-active --quiet nurse; then
systemctl restart nurse
sleep 3
if systemctl is-active --quiet nurse; then
say "Nurse is running."
else
say "Nurse did not start. If you have not filled in the Authentik settings in $ENV_FILE yet, that is why."
say "Look at: journalctl -u nurse -n 50"
exit 1
fi
fi
say "Installed $("$BIN" version)."
say "Next:"
say " 1. sudoedit $ENV_FILE (fill in the Authentik settings at the top)"
say " 2. systemctl start nurse (or run this again with --start)"
say " 3. journalctl -u nurse -f to watch it; it says at once if a setting is missing or wrong"
say ""
say "Nurse listens on $LISTEN. Put an HTTPS reverse proxy (or Hammerhead) in front of it and open the"
say "address you set as NURSE_PUBLIC_URL; Authentik will not send people back to plain http."