Anyone cloning the repo can now see exactly which environment variables are required by reading .env.example at the repo root. install.sh updated to copy .env.example during deployment (rather than duplicating the template inline), keeping the two in sync.
248 lines
9.7 KiB
Bash
Executable file
248 lines
9.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# inBOXER Install Script
|
||
# ======================
|
||
# Deploys the inBOXER binary and configuration to /opt/inboxer
|
||
# Creates a systemd service for production deployment.
|
||
# Supports Debian (apt) and RHEL (yum/dnf) families.
|
||
#
|
||
# Usage: sudo ./install.sh
|
||
#
|
||
set -euo pipefail
|
||
|
||
# ─── Configuration ───────────────────────────────────────────────────────────
|
||
INSTALL_DIR="/opt/inboxer"
|
||
SERVICE_USER="inboxer"
|
||
SERVICE_GROUP="inboxer"
|
||
SERVICE_NAME="inboxer"
|
||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||
|
||
BIN_DIR="${INSTALL_DIR}/bin"
|
||
DATA_DIR="${INSTALL_DIR}/data"
|
||
LOGS_DIR="${INSTALL_DIR}/logs"
|
||
|
||
# Detect script/repo location
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
if [[ "$(basename "${SCRIPT_DIR}")" == "bin" ]]; then
|
||
REPO_DIR="$(dirname "${SCRIPT_DIR}")"
|
||
else
|
||
REPO_DIR="${SCRIPT_DIR}"
|
||
fi
|
||
|
||
# ─── Terminal colours ────────────────────────────────────────────────────────
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||
|
||
# ─── Pre-flight checks ───────────────────────────────────────────────────────
|
||
if [[ $EUID -ne 0 ]]; then
|
||
error "This script must be run as root (use sudo)."
|
||
exit 1
|
||
fi
|
||
|
||
if ! command -v systemctl &>/dev/null; then
|
||
error "systemd is required but not found on this system."
|
||
exit 1
|
||
fi
|
||
|
||
REQUIRED_FILES=(
|
||
"${REPO_DIR}/bin/inboxer"
|
||
"${REPO_DIR}/bin/config.yaml"
|
||
"${REPO_DIR}/bin/prompt.txt"
|
||
)
|
||
for f in "${REQUIRED_FILES[@]}"; do
|
||
if [[ ! -f "$f" ]]; then
|
||
error "Required file not found: $f"
|
||
error "Run this script from the inBOXER repository root."
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
# ─── OS Detection (informational) ───────────────────────────────────────────
|
||
if [[ -f /etc/os-release ]]; then
|
||
# shellcheck source=/dev/null
|
||
. /etc/os-release
|
||
info "Detected OS: ${NAME} ${VERSION_ID}"
|
||
else
|
||
info "Could not detect OS version (no /etc/os-release)."
|
||
fi
|
||
|
||
# ─── Create system user & group ──────────────────────────────────────────────
|
||
info "Creating system user '${SERVICE_USER}'..."
|
||
|
||
if getent group "${SERVICE_GROUP}" &>/dev/null; then
|
||
info "Group '${SERVICE_GROUP}' already exists."
|
||
else
|
||
groupadd --system "${SERVICE_GROUP}"
|
||
info "Group '${SERVICE_GROUP}' created."
|
||
fi
|
||
|
||
if getent passwd "${SERVICE_USER}" &>/dev/null; then
|
||
info "User '${SERVICE_USER}' already exists."
|
||
else
|
||
useradd --system \
|
||
--no-create-home \
|
||
--gid "${SERVICE_GROUP}" \
|
||
--shell /sbin/nologin \
|
||
--comment "inBOXER Service User" \
|
||
"${SERVICE_USER}"
|
||
info "User '${SERVICE_USER}' created."
|
||
fi
|
||
|
||
# ─── Create directory structure ──────────────────────────────────────────────
|
||
info "Creating directories under ${INSTALL_DIR}..."
|
||
mkdir -p "${BIN_DIR}" "${DATA_DIR}" "${LOGS_DIR}"
|
||
|
||
# ─── Install binary & config files ───────────────────────────────────────────
|
||
info "Installing binary..."
|
||
install -m 755 "${REPO_DIR}/bin/inboxer" "${BIN_DIR}/inboxer"
|
||
|
||
info "Installing configuration..."
|
||
install -m 644 "${REPO_DIR}/bin/config.yaml" "${BIN_DIR}/config.yaml"
|
||
install -m 644 "${REPO_DIR}/bin/prompt.txt" "${BIN_DIR}/prompt.txt"
|
||
|
||
# Adjust config paths for /opt/inboxer deployment
|
||
info "Adjusting configuration paths for deployment..."
|
||
sed -i 's|path: "bin/db.sqlite"|path: "'"${DATA_DIR}"'/db.sqlite"|' "${BIN_DIR}/config.yaml"
|
||
sed -i 's|file: "bin/inboxer.log"|file: "'"${LOGS_DIR}"'/inboxer.log"|' "${BIN_DIR}/config.yaml"
|
||
# prompt_file: "bin/prompt.txt" works as-is relative to the working directory,
|
||
# since WorkingDirectory=/opt/inboxer resolves it to /opt/inboxer/bin/prompt.txt
|
||
|
||
# ─── Create .env from template ──────────────────────────────────────────────
|
||
ENV_FILE="${INSTALL_DIR}/.env"
|
||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||
# Prefer .env.example from repo (single source of truth)
|
||
if [[ -f "${REPO_DIR}/.env.example" ]]; then
|
||
info "Copying .env.example from repository to ${ENV_FILE} ..."
|
||
cp "${REPO_DIR}/.env.example" "${ENV_FILE}"
|
||
else
|
||
info "Creating .env template at ${ENV_FILE} ..."
|
||
cat > "${ENV_FILE}" << 'ENVEOF'
|
||
# inBOXER Environment Configuration
|
||
# ====================================
|
||
# Set your credentials below. The service reads these variables on startup.
|
||
# You can also set them directly in the systemd EnvironmentFile or via
|
||
# the shell environment (godotenv.Load() checks both the .env file and os.Getenv).
|
||
|
||
# DeepSeek API key – used to classify incoming emails
|
||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
||
|
||
# SMTP credentials – used to send one-time passwords (OTP) via email
|
||
SMTP_HOST=your.smtp.host
|
||
SMTP_PORT=587
|
||
SMTP_USER=your-email@example.com
|
||
SMTP_PASS=your-smtp-password
|
||
|
||
# (Optional) Override the session_secret from config.yaml.
|
||
# APP_SECRET=change-me-in-production
|
||
ENVEOF
|
||
fi
|
||
info ".env file created."
|
||
else
|
||
info ".env already exists, keeping existing file."
|
||
fi
|
||
|
||
# ─── Create systemd service unit ─────────────────────────────────────────────
|
||
info "Creating systemd service unit at ${SERVICE_FILE} ..."
|
||
cat > "${SERVICE_FILE}" << UNITEOF
|
||
[Unit]
|
||
Description=inBOXER – AI-Powered Email Classifier
|
||
Documentation=https://github.com/cclohmar/inboxer
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=${SERVICE_USER}
|
||
Group=${SERVICE_GROUP}
|
||
WorkingDirectory=${INSTALL_DIR}
|
||
EnvironmentFile=${ENV_FILE}
|
||
ExecStart=${BIN_DIR}/inboxer
|
||
Restart=on-failure
|
||
RestartSec=10
|
||
|
||
# Logging
|
||
StandardOutput=append:${LOGS_DIR}/stdout.log
|
||
StandardError=append:${LOGS_DIR}/stderr.log
|
||
|
||
# Security hardening
|
||
NoNewPrivileges=true
|
||
ProtectSystem=full
|
||
ProtectHome=true
|
||
PrivateTmp=true
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
UNITEOF
|
||
|
||
# ─── Set file permissions ────────────────────────────────────────────────────
|
||
info "Setting file ownership and permissions..."
|
||
chown -R "${SERVICE_USER}:${SERVICE_GROUP}" "${INSTALL_DIR}"
|
||
|
||
# Directory permissions
|
||
chmod 755 "${BIN_DIR}"
|
||
chmod 750 "${DATA_DIR}" # database file is sensitive
|
||
chmod 750 "${LOGS_DIR}"
|
||
|
||
# .env must be readable by the service user (and owner-only for secrets)
|
||
chmod 640 "${ENV_FILE}"
|
||
|
||
# ─── Register & start service ───────────────────────────────────────────────
|
||
info "Reloading systemd daemon..."
|
||
systemctl daemon-reload
|
||
|
||
info "Enabling ${SERVICE_NAME} service (starts on boot)..."
|
||
systemctl enable "${SERVICE_NAME}"
|
||
|
||
info "Starting ${SERVICE_NAME} service..."
|
||
systemctl start "${SERVICE_NAME}"
|
||
|
||
# Brief pause so the service can initialise
|
||
sleep 2
|
||
|
||
# ─── Verify ──────────────────────────────────────────────────────────────────
|
||
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
||
info "Service '${SERVICE_NAME}' is running."
|
||
systemctl status "${SERVICE_NAME}" --no-pager
|
||
else
|
||
warn "Service '${SERVICE_NAME}' did not start. Check logs:"
|
||
warn " journalctl -u ${SERVICE_NAME} --no-pager"
|
||
systemctl status "${SERVICE_NAME}" --no-pager || true
|
||
fi
|
||
|
||
# ─── Summary ─────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
info "═════════════════════════════════════════════════"
|
||
info " inBOXER Installation Complete"
|
||
info "═════════════════════════════════════════════════"
|
||
echo ""
|
||
info " Install directory: ${INSTALL_DIR}"
|
||
info " Binary: ${BIN_DIR}/inboxer"
|
||
info " Configuration: ${BIN_DIR}/config.yaml"
|
||
info " Prompt file: ${BIN_DIR}/prompt.txt"
|
||
info " Data (SQLite): ${DATA_DIR}/"
|
||
info " Logs: ${LOGS_DIR}/"
|
||
info " Environment: ${ENV_FILE}"
|
||
echo ""
|
||
info " Edit the environment file with your credentials:"
|
||
info " sudo nano ${ENV_FILE}"
|
||
echo ""
|
||
if grep -q "your_deepseek_api_key_here" "${ENV_FILE}" 2>/dev/null; then
|
||
warn " ⚠ The .env file still contains placeholder values!"
|
||
warn " Edit ${ENV_FILE} before the service will function."
|
||
echo ""
|
||
fi
|
||
info " Service management:"
|
||
info " sudo systemctl status ${SERVICE_NAME}"
|
||
info " sudo systemctl restart ${SERVICE_NAME}"
|
||
info " sudo systemctl stop ${SERVICE_NAME}"
|
||
info " sudo journalctl -u ${SERVICE_NAME} -f"
|
||
echo ""
|
||
info " Web interface: http://$(hostname -s 2>/dev/null || echo "localhost"):8080"
|
||
info "═════════════════════════════════════════════════"
|
||
echo ""
|