#!/usr/bin/env bash set -Eeuo pipefail RED='\033[0;31m' GREEN='\033[0;32m' BLUE='\033[0;34m' YELLOW='\033[1;33m' NC='\033[0m' APP_NAME="home-server" INSTALL_DIR="${HOMEIO_INSTALL_DIR:-/opt/home-server}" ENV_FILE="${INSTALL_DIR}/.env" SERVICE_NAME="${HOMEIO_SERVICE_NAME:-home-server}" DBUS_SERVICE_NAME="${HOMEIO_DBUS_SERVICE_NAME:-home-server-dbus}" APP_PORT="${HOMEIO_APP_PORT:-${HOMEIO_PORT:-12026}}" PUBLIC_PORT="${HOMEIO_PUBLIC_PORT:-80}" NGINX_SITE_NAME="${HOMEIO_NGINX_SITE_NAME:-home-server}" REPO_URL="${HOMEIO_REPO_URL:-https://github.com/doctor-io/homeio.git}" REPO_BRANCH="${HOMEIO_REPO_BRANCH:-main}" # The tarball install path is gone; a pinned version is now a git tag. if [[ -n "${HOMEIO_RELEASE_TAG:-}" || -n "${HOMEIO_RELEASE_TARBALL_URL:-}" ]]; then echo "[!] HOMEIO_RELEASE_TAG and HOMEIO_RELEASE_TARBALL_URL are no longer supported." >&2 echo "[!] Homeio installs with git. To pin a version, set HOMEIO_REPO_BRANCH to its tag, for example HOMEIO_REPO_BRANCH=v1.10.0." >&2 exit 1 fi NODE_VERSION="${NODE_VERSION:-22.14.0}" GO_VERSION="${GO_VERSION:-1.23.4}" YQ_VERSION="${YQ_VERSION:-4.45.4}" DOCKER_VERSION="${DOCKER_VERSION:-}" # empty = always install latest DOCKER_INSTALL_SCRIPT_COMMIT="${DOCKER_INSTALL_SCRIPT_COMMIT:-master}" UPLOAD_SERVICE_NAME="${HOMEIO_UPLOAD_SERVICE_NAME:-home-server-upload}" HOMEIO_INSTALL_YQ="${HOMEIO_INSTALL_YQ:-false}" HOMEIO_HOSTNAME="${HOMEIO_HOSTNAME:-}" HOMEIO_ALLOW_OTHER_NODE="${HOMEIO_ALLOW_OTHER_NODE:-false}" HOMEIO_VERBOSE="${HOMEIO_VERBOSE:-false}" HOMEIO_DRY_RUN="${HOMEIO_DRY_RUN:-false}" EFFECTIVE_DB_USER="" print_status() { echo -e "${GREEN}[+]${NC} $1"; } print_error() { echo -e "${RED}[!]${NC} $1" >&2; } print_warn() { echo -e "${YELLOW}[*]${NC} $1"; } print_dry() { echo -e "${BLUE}[DRY]${NC} Would: $1"; } run_step() { local label="${1}" shift if [[ "${HOMEIO_DRY_RUN}" == "true" ]]; then print_dry "${label}" return 0 fi print_status "${label}" "$@" } command_exists() { command -v "$1" >/dev/null 2>&1 } # The tag of the release GitHub marks as Latest, or nothing when it cannot be # read (offline, rate-limited, no release yet). Never fails. latest_release_tag() { local repo_path="${REPO_URL#https://github.com/}" repo_path="${repo_path%.git}" local json tag json="$(curl -fsSL --max-time 20 -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/${repo_path}/releases/latest" 2>/dev/null)" || return 0 tag="$(printf '%s\n' "${json}" | sed -n 's/^[[:space:]]*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p')" if [[ "${tag}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then printf '%s\n' "${tag}" fi } # Without HOMEIO_REPO_BRANCH, install the latest published release rather than # whatever main holds between a merge and its release. git fetches a tag the # same way as a branch. resolve_repo_ref() { [[ -n "${HOMEIO_REPO_BRANCH:-}" ]] && return 0 local tag tag="$(latest_release_tag)" if [[ -n "${tag}" ]]; then REPO_BRANCH="${tag}" print_status "Latest release: ${tag}" else print_warn "Could not read the latest release from GitHub; using the ${REPO_BRANCH} branch." fi } require_root() { [[ "${EUID}" -eq 0 ]] || { print_error "Run this installer as root (for example: sudo bash install.sh)."; exit 1; } } run_cmd() { local cmd="${1}" bash -c "${cmd}" } validate_identifiers() { local db_user="${HOMEIO_DB_USER:-home_server}" local db_name="${HOMEIO_DB_NAME:-home_server}" local admin_username="${HOMEIO_ADMIN_USERNAME:-auto}" [[ "${db_user}" =~ ^[a-z_][a-z0-9_]{0,62}$ ]] || { print_error "Invalid HOMEIO_DB_USER: ${db_user}"; exit 1; } [[ "${db_name}" =~ ^[a-z_][a-z0-9_]{0,62}$ ]] || { print_error "Invalid HOMEIO_DB_NAME: ${db_name}"; exit 1; } [[ "${HOMEIO_VERBOSE}" == "true" || "${HOMEIO_VERBOSE}" == "false" ]] || { print_error "Invalid HOMEIO_VERBOSE: ${HOMEIO_VERBOSE} (expected true|false)"; exit 1; } [[ "${HOMEIO_DRY_RUN}" == "true" || "${HOMEIO_DRY_RUN}" == "false" ]] || { print_error "Invalid HOMEIO_DRY_RUN: ${HOMEIO_DRY_RUN} (expected true|false)"; exit 1; } if [[ "${admin_username}" != "auto" ]]; then [[ "${admin_username}" =~ ^[a-zA-Z0-9._-]{3,64}$ ]] || { print_error "Invalid HOMEIO_ADMIN_USERNAME: ${admin_username}"; exit 1; } fi } detect_arch() { case "$(uname -m)" in x86_64) echo "x64" ;; aarch64) echo "arm64" ;; *) print_error "Unsupported architecture: $(uname -m). Supported: x86_64, aarch64." exit 1 ;; esac } ensure_apt() { command_exists apt-get || { print_error "This installer supports Debian/Ubuntu/Raspberry Pi OS only (apt-get required)."; exit 1; } } normalize_hostname() { local input="${1:-}" # Remove whitespace and lowercase input="$(echo "${input}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')" # Strip .local if provided if [[ "${input}" == *.local ]]; then input="${input%.local}" fi # Default if empty if [[ -z "${input}" ]]; then input="homeio" fi # Basic validation (allow dots for FQDN) if ! [[ "${input}" =~ ^[a-z0-9][a-z0-9.-]{0,62}$ ]]; then print_error "Invalid hostname: ${input}. Use letters, numbers, hyphens, and dots." exit 1 fi echo "${input}" } configure_hostname() { local desired="${HOMEIO_HOSTNAME}" if [[ -z "${desired}" && -t 0 ]]; then echo "Enter the hostname for Home Server (used as .local)." read -r -p "Hostname [homeio]: " desired fi desired="$(normalize_hostname "${desired}")" if command_exists hostnamectl; then hostnamectl set-hostname "${desired}" else echo "${desired}" >/etc/hostname hostname "${desired}" fi local hosts_entry="127.0.1.1 ${desired}" if [[ "${desired}" != *.* ]]; then hosts_entry="${hosts_entry} ${desired}.local" fi if grep -qE '^127\.0\.1\.1' /etc/hosts; then sed -i "s|^127\.0\.1\.1.*|${hosts_entry}|" /etc/hosts else echo "${hosts_entry}" >>/etc/hosts fi if [[ "${desired}" == *.* ]]; then print_status "Hostname set to ${desired}. Configure DNS to point this name to the server." else print_status "Hostname set to ${desired}. Access via http://${desired}.local" fi } # Install postgresql-common on its own, before the postgresql metapackage. # apt preconfigures every package in a batch before unpacking any of them, and # the postgresql metapackage's debconf script calls pg_lsclusters — which ships # in postgresql-common. In a single batch it isn't unpacked yet, so the install # prints "pg_lsclusters: not found". Harmless, but it reads like a failure. install_postgresql_common() { if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then apt-get install -y postgresql-common else apt-get install -y -qq postgresql-common >/dev/null fi } install_packages() { print_status "Installing system dependencies..." if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then apt-get update -y install_postgresql_common apt-get install -y \ ca-certificates \ curl \ gnupg \ git \ jq \ rsync \ python3 \ build-essential \ libudev-dev \ tar \ xz-utils \ unzip \ procps \ postgresql \ postgresql-contrib \ openssl else apt-get update -qq >/dev/null install_postgresql_common apt-get install -y -qq \ ca-certificates \ curl \ gnupg \ git \ jq \ rsync \ python3 \ build-essential \ libudev-dev \ tar \ xz-utils \ unzip \ procps \ postgresql \ postgresql-contrib \ openssl >/dev/null fi } install_extras() { print_status "Installing full extras..." if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then apt-get install -y \ network-manager \ nginx \ systemd-timesyncd \ openssh-server \ avahi-daemon \ avahi-discover \ avahi-utils \ libnss-mdns \ bluez \ sudo \ nano \ vim \ less \ man \ iproute2 \ iputils-ping \ wget \ usbutils \ whois \ fswatch \ gettext-base \ dmidecode \ unar \ imagemagick \ ffmpeg \ ufw \ fail2ban \ python3-systemd \ samba \ wsdd2 \ cifs-utils \ smbclient \ gdisk \ parted \ e2fsprogs \ exfatprogs else apt-get install -y -qq \ network-manager \ nginx \ systemd-timesyncd \ openssh-server \ avahi-daemon \ avahi-discover \ avahi-utils \ libnss-mdns \ bluez \ sudo \ nano \ vim \ less \ man \ iproute2 \ iputils-ping \ wget \ usbutils \ whois \ fswatch \ gettext-base \ dmidecode \ unar \ imagemagick \ ffmpeg \ ufw \ fail2ban \ python3-systemd \ samba \ wsdd2 \ cifs-utils \ smbclient \ gdisk \ parted \ e2fsprogs \ exfatprogs >/dev/null fi # ntfs-3g only on amd64 if [[ "$(detect_arch)" == "x64" ]]; then if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then apt-get install -y ntfs-3g else apt-get install -y -qq ntfs-3g >/dev/null fi fi # Let HomeIO manage these when needed systemctl disable smbd wsdd2 >/dev/null 2>&1 || true systemctl enable --now avahi-daemon >/dev/null 2>&1 || true configure_fail2ban_backend systemctl enable --now fail2ban >/dev/null 2>&1 || true systemctl restart fail2ban >/dev/null 2>&1 || true } # Debian 12+ no longer ships rsyslog, so /var/log/auth.log does not exist and # fail2ban's default backend aborts on the sshd jail. Read the journal instead. # Kept apart from jail.d/homeio.local, which the app rewrites from Settings. configure_fail2ban_backend() { mkdir -p /etc/fail2ban/jail.d cat > /etc/fail2ban/jail.d/00-homeio-backend.local <<'CONF' [DEFAULT] backend = systemd [sshd] backend = systemd CONF } ensure_security_dependencies() { print_status "Ensuring security dependencies (ufw, fail2ban)..." local packages=() command_exists ufw || packages+=("ufw") command_exists fail2ban-client || packages+=("fail2ban") dpkg-query -W -f='${Status}' python3-systemd 2>/dev/null | grep -q "install ok installed" || packages+=("python3-systemd") if (( ${#packages[@]} > 0 )); then if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then apt-get update -y apt-get install -y "${packages[@]}" else apt-get update -qq >/dev/null apt-get install -y -qq "${packages[@]}" >/dev/null fi fi configure_fail2ban_backend systemctl enable --now fail2ban >/dev/null 2>&1 || true systemctl restart fail2ban >/dev/null 2>&1 || true } deploy_os_configs() { print_status "Deploying OS configuration overlays..." local overlay_src="${INSTALL_DIR}/packages/os/overlay-common" if [[ ! -d "${overlay_src}" ]]; then print_status "No OS overlays found at ${overlay_src}, skipping." return fi # Deploy netplan config to use NetworkManager if [[ -f "${overlay_src}/etc/netplan/01-netcfg.yaml" ]]; then mkdir -p /etc/netplan cp "${overlay_src}/etc/netplan/01-netcfg.yaml" /etc/netplan/01-netcfg.yaml chmod 600 /etc/netplan/01-netcfg.yaml print_status "Deployed netplan config to use NetworkManager renderer" # Apply netplan configuration netplan apply >/dev/null 2>&1 || true sleep 2 fi # Deploy NetworkManager config if [[ -f "${overlay_src}/etc/NetworkManager/NetworkManager.conf" ]]; then mkdir -p /etc/NetworkManager cp "${overlay_src}/etc/NetworkManager/NetworkManager.conf" /etc/NetworkManager/NetworkManager.conf # Restart NetworkManager to apply the new configuration systemctl restart NetworkManager >/dev/null 2>&1 || true sleep 2 # Give NetworkManager time to reinitialize devices print_status "Deployed NetworkManager.conf and restarted NetworkManager" fi # Deploy systemd logind configs if [[ -d "${overlay_src}/etc/systemd/logind.conf.d" ]]; then mkdir -p /etc/systemd/logind.conf.d cp "${overlay_src}/etc/systemd/logind.conf.d/"*.conf /etc/systemd/logind.conf.d/ 2>/dev/null || true systemctl restart systemd-logind >/dev/null 2>&1 || true print_status "Deployed logind configurations" fi # Deploy avahi-daemon config if [[ -f "${overlay_src}/etc/avahi/avahi-daemon.conf" ]]; then mkdir -p /etc/avahi cp "${overlay_src}/etc/avahi/avahi-daemon.conf" /etc/avahi/avahi-daemon.conf systemctl restart avahi-daemon >/dev/null 2>&1 || true print_status "Deployed avahi-daemon.conf" fi } install_docker() { if command_exists docker; then print_status "Docker already installed." return fi local version_args=() if [[ -n "${DOCKER_VERSION}" && "${DOCKER_VERSION}" != "latest" ]]; then print_status "Installing Docker ${DOCKER_VERSION}..." version_args=(--version "v${DOCKER_VERSION}") else print_status "Installing Docker (latest)..." fi if ! curl -fsSL "https://raw.githubusercontent.com/docker/docker-install/${DOCKER_INSTALL_SCRIPT_COMMIT}/install.sh" -o /tmp/install-docker.sh; then print_error "Failed to download Docker install script. Check your internet connection." exit 1 fi local docker_log docker_log=$(mktemp) if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then sh /tmp/install-docker.sh "${version_args[@]}" 2>&1 | tee "${docker_log}" else sh /tmp/install-docker.sh "${version_args[@]}" >"${docker_log}" 2>&1 fi local docker_exit=$? rm -f /tmp/install-docker.sh if [[ ${docker_exit} -ne 0 ]]; then print_error "Docker installation failed (exit ${docker_exit})." print_error "Last output:" tail -20 "${docker_log}" >&2 rm -f "${docker_log}" exit 1 fi rm -f "${docker_log}" if ! systemctl enable --now docker >/dev/null 2>&1; then print_error "Docker installed but failed to start. Run: systemctl status docker" exit 1 fi } install_node() { local arch arch="$(detect_arch)" if command_exists node; then local current current="$(node -v | sed 's/^v//')" if [[ "${current}" == "${NODE_VERSION}" ]]; then print_status "Node ${NODE_VERSION} already installed." return fi if [[ "${HOMEIO_ALLOW_OTHER_NODE}" == "true" ]]; then print_status "Using existing Node ${current} (HOMEIO_ALLOW_OTHER_NODE=true)." return fi print_status "Existing Node ${current} detected; installing Node ${NODE_VERSION}." fi local node_tar="node-v${NODE_VERSION}-linux-${arch}.tar.gz" local node_url="https://nodejs.org/dist/v${NODE_VERSION}/${node_tar}" local checksums_url="https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" local sha_expected="" print_status "Installing Node ${NODE_VERSION}..." curl -fsSL "${checksums_url}" -o /tmp/SHASUMS256.txt sha_expected="$(grep " ${node_tar}\$" /tmp/SHASUMS256.txt | awk '{print $1}')" [[ -n "${sha_expected}" ]] || { print_error "Could not find checksum for ${node_tar}"; exit 1; } curl -fsSL "${node_url}" -o "/tmp/${node_tar}" if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then echo "${sha_expected} /tmp/${node_tar}" | sha256sum -c - else echo "${sha_expected} /tmp/${node_tar}" | sha256sum -c - >/dev/null fi tar -xzf "/tmp/${node_tar}" -C /usr/local --strip-components=1 rm -f "/tmp/${node_tar}" /tmp/SHASUMS256.txt } install_go() { local arch go_arch arch="$(detect_arch)" go_arch="amd64" [[ "${arch}" == "arm64" ]] && go_arch="arm64" if command_exists go; then local current current="$(go version 2>/dev/null | awk '{print $3}' | sed 's/^go//')" if [[ "${current}" == "${GO_VERSION}" ]]; then print_status "Go ${GO_VERSION} already installed." export PATH="/usr/local/go/bin:${PATH}" return fi print_status "Existing Go ${current} detected; installing Go ${GO_VERSION}." fi print_status "Installing Go ${GO_VERSION}..." local tarball="go${GO_VERSION}.linux-${go_arch}.tar.gz" curl -fsSL "https://go.dev/dl/${tarball}" -o "/tmp/${tarball}" rm -rf /usr/local/go tar -C /usr/local -xzf "/tmp/${tarball}" rm -f "/tmp/${tarball}" if [[ ! -f /etc/profile.d/go.sh ]]; then echo 'export PATH="/usr/local/go/bin:$PATH"' > /etc/profile.d/go.sh fi export PATH="/usr/local/go/bin:${PATH}" print_status "Go ${GO_VERSION} installed." } build_upload_server() { print_status "Building upload server (Go)..." export PATH="/usr/local/go/bin:${PATH}" # Go requires a build cache. Some install/update entrypoints run with a # minimal environment, so provide explicit cache defaults for the compiler. export HOME="${HOME:-/root}" export GOCACHE="${GOCACHE:-${HOME}/.cache/go-build}" export GOPATH="${GOPATH:-${HOME}/go}" local src="${INSTALL_DIR}/services/upload-server" [[ -d "${src}" ]] || { print_error "Upload server source not found at ${src}"; exit 1; } mkdir -p "${INSTALL_DIR}/bin" mkdir -p "${GOCACHE}" "${GOPATH}" if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then (cd "${src}" && env HOME="${HOME}" GOCACHE="${GOCACHE}" GOPATH="${GOPATH}" go build -o "${INSTALL_DIR}/bin/upload-server" .) else if ! (cd "${src}" && env HOME="${HOME}" GOCACHE="${GOCACHE}" GOPATH="${GOPATH}" go build -o "${INSTALL_DIR}/bin/upload-server" .) >/dev/null 2>&1; then print_error "Failed to build upload server. Re-run with HOMEIO_VERBOSE=true for details." exit 1 fi fi print_status "Upload server built: ${INSTALL_DIR}/bin/upload-server" } install_upload_server_service() { command_exists systemctl || { print_error "systemd is required but systemctl is not available."; exit 1; } local unit_file="/etc/systemd/system/${UPLOAD_SERVICE_NAME}.service" print_status "Installing systemd unit ${UPLOAD_SERVICE_NAME}.service..." cat >"${unit_file}" </dev/null fi chmod +x /usr/local/bin/yq rm -f /tmp/yq-checksums.txt } ensure_directories() { print_status "Ensuring installation directories..." mkdir -p "${INSTALL_DIR}" # App data directories print_status "Creating /DATA directories..." mkdir -p /DATA/{AppData,Documents,Media,Download} chmod 755 /DATA # Stack storage (STORE_STACKS_ROOT) and logs mkdir -p /var/lib/home-server/stacks mkdir -p "${INSTALL_DIR}/logs" } clone_or_update_repo() { print_status "Syncing repository..." # Remove existing installation if present if [[ -d "${INSTALL_DIR}" ]]; then cd /tmp || cd / print_status "Removing existing installation..." rm -rf "${INSTALL_DIR}" fi resolve_repo_ref print_status "Cloning repository (${REPO_BRANCH})..." git clone --depth=1 --branch "${REPO_BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" --quiet } unset_env_key() { local key="${1}" local file="${2}" local tmp_file tmp_file="$(mktemp)" awk -v key="${key}" ' $0 ~ ("^" key "=") { next } { print } ' "${file}" >"${tmp_file}" cat "${tmp_file}" >"${file}" rm -f "${tmp_file}" } get_env_value() { local key="${1}" local file="${2}" [[ -f "${file}" ]] || return 1 grep -E "^${key}=" "${file}" | head -n 1 | cut -d '=' -f 2- } set_env_key() { local key="${1}" local value="${2}" local file="${3}" local tmp_file tmp_file="$(mktemp)" awk -v key="${key}" -v value="${value}" ' BEGIN { replaced = 0 } $0 ~ ("^" key "=") { if (!replaced) { print key "=" value replaced = 1 } next } { print } END { if (!replaced) { print key "=" value } } ' "${file}" >"${tmp_file}" cat "${tmp_file}" >"${file}" rm -f "${tmp_file}" } create_env_file() { print_status "Creating .env file..." # Generate auth session secret if not provided local auth_session_secret="${HOMEIO_AUTH_SESSION_SECRET:-}" if [[ -z "${auth_session_secret}" ]]; then auth_session_secret="$(openssl rand -hex 32)" print_status "Generated AUTH_SESSION_SECRET" fi # Generate database password local db_password db_password="$(openssl rand -hex 24)" local db_user="${HOMEIO_DB_USER:-home_server}" local db_name="${HOMEIO_DB_NAME:-home_server}" local database_url="postgresql://${db_user}:${db_password}@127.0.0.1:5432/${db_name}" cat > "${ENV_FILE}" </dev/null 2>&1; do attempts=$((attempts + 1)) if [[ "${attempts}" -ge "${max_attempts}" ]]; then print_error "PostgreSQL did not become ready after ${max_attempts} attempts" exit 1 fi sleep 2 done print_status "PostgreSQL is ready" } configure_local_postgres() { print_status "Configuring local PostgreSQL..." systemctl enable --now postgresql wait_for_postgres local db_user="${EFFECTIVE_DB_USER}" local db_pass="${EFFECTIVE_DB_PASSWORD}" local db_name="${EFFECTIVE_DB_NAME}" print_status "Ensuring PostgreSQL role '${db_user}'..." runuser -u postgres -- psql -v ON_ERROR_STOP=1 --set=db_user="${db_user}" --set=db_pass="${db_pass}" 2>&1 <<'SQL' || true SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'db_user', :'db_pass') WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'db_user') \gexec SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'db_user', :'db_pass') WHERE EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'db_user') \gexec SQL print_status "Ensuring PostgreSQL database '${db_name}'..." if ! runuser -u postgres -- psql -tAc "SELECT 1 FROM pg_database WHERE datname='${db_name}'" | grep -q 1; then runuser -u postgres -- createdb --owner="${db_user}" "${db_name}" fi } install_homeio() { print_status "Installing ${APP_NAME}..." if [[ ! -d "${INSTALL_DIR}" ]]; then print_error "Installation directory not found: ${INSTALL_DIR}" exit 1 fi cd "${INSTALL_DIR}" || { print_error "Failed to enter installation directory"; exit 1; } local step_log step_log="$(mktemp)" # Install dependencies print_status "Installing npm dependencies (this may take a few minutes)..." if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then npm ci || { print_error "npm ci failed"; exit 1; } else if ! npm ci --loglevel=error --no-audit --no-fund >"${step_log}" 2>&1; then print_error "npm ci failed." print_error "Last output:" tail -20 "${step_log}" >&2 rm -f "${step_log}" exit 1 fi fi # Initialize database schema print_status "Initializing database schema..." if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then (set -a && source "${ENV_FILE}" && set +a && npm run db:init) || { print_error "Database initialization failed"; exit 1; } else if ! (set -a && source "${ENV_FILE}" && set +a && npm run db:init) >"${step_log}" 2>&1; then print_error "Database initialization failed." print_error "Last output:" tail -20 "${step_log}" >&2 rm -f "${step_log}" exit 1 fi fi # Build application print_status "Building Next.js application..." if [[ "${HOMEIO_VERBOSE}" == "true" ]]; then npm run build || { print_error "Build failed"; exit 1; } else if ! npm run build >"${step_log}" 2>&1; then print_error "Build failed." print_error "Last output:" tail -20 "${step_log}" >&2 rm -f "${step_log}" exit 1 fi fi rm -f "${step_log}" print_status "Installation completed successfully!" } install_systemd_service() { command_exists systemctl || { print_error "systemd is required but systemctl is not available."; exit 1; } local unit_file="/etc/systemd/system/${SERVICE_NAME}.service" print_status "Installing systemd unit ${SERVICE_NAME}.service..." cat >"${unit_file}" </dev/null 2>&1; then print_warn "nginx rejected the raised connection limits; keeping its previous settings." mv "${main_conf}.homeio-bak" "${main_conf}" return 0 fi rm -f "${main_conf}.homeio-bak" print_status "nginx now accepts ${worker_connections} connections per worker." } install_reverse_proxy() { command_exists systemctl || { print_error "systemd is required but systemctl is not available."; exit 1; } command_exists nginx || { print_error "nginx is required but was not found."; exit 1; } local nginx_conf="/etc/nginx/sites-available/${NGINX_SITE_NAME}.conf" local nginx_enabled="/etc/nginx/sites-enabled/${NGINX_SITE_NAME}.conf" local maintenance_root="/var/lib/homeio-maintenance" local maintenance_file="${maintenance_root}/__homeio_unavailable.html" print_status "Configuring nginx reverse proxy on :${PUBLIC_PORT} -> 127.0.0.1:${APP_PORT}..." mkdir -p "${maintenance_root}" cat >"${maintenance_file}" <<'EOF' Homeio is restarting

Homeio is restarting

Homeio is temporarily unavailable. This page will automatically reload when /api/health is available again.

Waiting for Homeio...
EOF cat >"${nginx_conf}" </dev/null 2>&1 || true tune_nginx_limits nginx -t systemctl enable --now nginx systemctl restart nginx } install_dbus_helper_service() { command_exists systemctl || { print_error "systemd is required but systemctl is not available."; exit 1; } local unit_file="/etc/systemd/system/${DBUS_SERVICE_NAME}.service" print_status "Installing systemd unit ${DBUS_SERVICE_NAME}.service..." cat >"${unit_file}" </dev/null 2>&1 && has_service=true if [[ "${has_install_dir}" == "false" && "${has_service}" == "false" ]]; then return fi echo "" print_warn "Existing HomeIO installation detected." print_warn "Running update.sh instead to preserve your data and configuration." print_warn "To force a full reinstall, set HOMEIO_FORCE_REINSTALL=true." echo "" resolve_repo_ref local update_url="https://raw.githubusercontent.com/doctor-io/homeio/${REPO_BRANCH}/scripts/update.sh" local tmp_update tmp_update="$(mktemp /tmp/homeio-update-XXXXXX.sh)" if ! curl -fsSL "${update_url}" -o "${tmp_update}"; then print_error "Failed to download update script from ${update_url}" print_error "Run the update manually: curl -fsSL ${update_url} | sudo bash" rm -f "${tmp_update}" exit 1 fi chmod +x "${tmp_update}" # Pass REPO_BRANCH so update.sh installs the same release or branch as this installer. HOMEIO_REPO_BRANCH="${REPO_BRANCH}" exec bash "${tmp_update}" } main() { run_step "Checking root privileges..." require_root redirect_to_update_if_installed run_step "Checking apt availability..." ensure_apt run_step "Validating installer configuration..." validate_identifiers run_step "Configuring hostname..." configure_hostname run_step "Installing base packages..." install_packages run_step "Installing extras packages..." install_extras run_step "Ensuring security dependencies..." ensure_security_dependencies run_step "Installing Docker..." install_docker run_step "Installing Node.js..." install_node run_step "Installing Go..." install_go run_step "Installing yq (optional)..." install_yq run_step "Ensuring directories..." ensure_directories run_step "Syncing repository..." clone_or_update_repo run_step "Deploying OS configurations..." deploy_os_configs run_step "Creating environment file..." create_env_file run_step "Configuring PostgreSQL..." configure_local_postgres run_step "Installing application..." install_homeio run_step "Building upload server..." build_upload_server run_step "Installing app systemd service..." install_systemd_service run_step "Installing upload server service..." install_upload_server_service run_step "Configuring reverse proxy..." install_reverse_proxy run_step "Installing DBus helper service..." install_dbus_helper_service print_summary } main "$@"