inboxer/install.sh
cclohmar 54dd30a2d6 Add .env.example as single source of truth for secrets documentation
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.
2026-04-23 19:57:44 +00:00

248 lines
9.7 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 ""