#!/usr/bin/env bash # MyOffice Proxmox LXC installer (community-scripts style). # One-liner (env optional — defaults match public repo): # bash -c "$(curl -fsSL https://mygit.ase.com.ua/alexandr/myoffice_public/raw/branch/master/proxmox/myoffice.sh)" # # Creates one LXC with PostgreSQL + ASP.NET runtime + app from Gitea Release. # No Docker, no nginx, no git/SDK/Node in the CT (use your existing proxy LXC in front). set -euo pipefail GITEA_URL="${GITEA_URL:-https://mygit.ase.com.ua}" GITEA_OWNER="${GITEA_OWNER:-alexandr}" GITEA_REPO="${GITEA_REPO:-myoffice_public}" GITEA_BRANCH="${GITEA_BRANCH:-master}" RELEASE_TAG="${RELEASE_TAG:-latest}" RELEASE_ASSET_URL="${RELEASE_ASSET_URL:-}" GITEA_TOKEN="${GITEA_TOKEN:-}" if [[ -z "${REPO_RAW_BASE:-}" ]]; then REPO_RAW_BASE="${GITEA_URL%/}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/${GITEA_BRANCH}" fi # Derive GITEA_* from REPO_RAW_BASE when only raw base is set: # https://host/owner/repo/raw/branch/master if [[ "$REPO_RAW_BASE" =~ ^(https?://[^/]+)/([^/]+)/([^/]+)/raw/branch/([^/]+) ]]; then GITEA_URL="${BASH_REMATCH[1]}" GITEA_OWNER="${BASH_REMATCH[2]}" GITEA_REPO="${BASH_REMATCH[3]}" GITEA_BRANCH="${BASH_REMATCH[4]}" elif [[ "$REPO_RAW_BASE" =~ ^(https?://[^/]+)/([^/]+)/([^/]+)/raw/ ]]; then GITEA_URL="${BASH_REMATCH[1]}" GITEA_OWNER="${BASH_REMATCH[2]}" GITEA_REPO="${BASH_REMATCH[3]}" fi APP="MyOffice" APP_DIR="/opt/myoffice" YW=$'\033[33m' BL=$'\033[36m' RD=$'\033[01;31m' BGN=$'\033[4;92m' GN=$'\033[1;92m' DGN=$'\033[32m' CL=$'\033[m' BOLD=$'\033[1m' BFR=$'\r\033[K' TAB=$' ' CM="${TAB}✔️${TAB}${CL}" CROSS="${TAB}✖️${TAB}${CL}" INFO="${TAB}💡${TAB}${CL}" OS="${TAB}🖥️${TAB}${CL}" CONTAINERTYPE="${TAB}📦${TAB}${CL}" DISKSIZE="${TAB}💾${TAB}${CL}" CPUCORE="${TAB}🧠${TAB}${CL}" RAMSIZE="${TAB}🛠️${TAB}${CL}" CONTAINERID="${TAB}🆔${TAB}${CL}" HOSTNAME="${TAB}🏠${TAB}${CL}" BRIDGE="${TAB}🌉${TAB}${CL}" GATEWAY="${TAB}🌐${TAB}${CL}" DEFAULT="${TAB}⚙️${TAB}${CL}" CREATING="${TAB}🚀${TAB}${CL}" ADVANCED="${TAB}🧩${TAB}${CL}" header_info() { clear cat <<"EOF" __ ___ ____ ____________ / |/ /_ __/ __ \/ __/ __/ _/______ / /|_/ / / / / / / / /_/ /_ / // ___/ _ \ / / / / /_/ / /_/ / __/ __// // /__/ __/ /_/ /_/\__, /\____/_/ /_/ /___/\___/\___/ /____/ Proxmox LXC (Gitea release) EOF } msg_info() { echo -ne "${TAB}${YW}${1}${CL}"; } msg_ok() { echo -e "${BFR}${CM}${GN}${1}${CL}"; } msg_error() { echo -e "${BFR}${CROSS}${RD}${1}${CL}"; } error_handler() { local exit_code=$? local line_number=$1 local command=$2 echo -e "\n${RD}[ERROR]${CL} line ${RD}${line_number}${CL}: exit ${RD}${exit_code}${CL}: ${YW}${command}${CL}\n" if [[ -n "${CTID:-}" ]] && pct status "$CTID" &>/dev/null; then echo -e "${INFO}CT ${CTID} exists; destroy with: pct stop ${CTID}; pct destroy ${CTID}" fi exit "$exit_code" } trap 'error_handler $LINENO "$BASH_COMMAND"' ERR exit_script() { clear echo -e "\n${CROSS}${RD}User exited script${CL}\n" exit 0 } check_root() { if [[ "$(id -u)" -ne 0 ]]; then msg_error "Please run this script as root on the Proxmox host." exit 1 fi } pve_check() { if ! command -v pveversion >/dev/null 2>&1; then msg_error "This script must run on a Proxmox VE host (pveversion not found)." exit 1 fi if ! command -v whiptail >/dev/null 2>&1; then msg_error "whiptail is required (apt install whiptail)." exit 1 fi } arch_check() { if [[ "$(dpkg --print-architecture)" != "amd64" ]]; then msg_error "Only amd64 is supported." exit 1 fi } get_valid_nextid() { local try_id try_id=$(pvesh get /cluster/nextid) while true; do if [[ -f "/etc/pve/qemu-server/${try_id}.conf" ]] || [[ -f "/etc/pve/lxc/${try_id}.conf" ]]; then try_id=$((try_id + 1)) continue fi break done echo "$try_id" } select_storage() { local STORAGE_MENU=() local MSG_MAX_LENGTH=0 local line TAG TYPE FREE ITEM OFFSET VALID while read -r line; do TAG=$(echo "$line" | awk '{print $1}') TYPE=$(echo "$line" | awk '{printf "%-10s", $2}') FREE=$(echo "$line" | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f 2>/dev/null | awk '{printf("%9sB", $6)}' || echo "?") ITEM=" Type: $TYPE Free: $FREE " OFFSET=2 if [[ $((${#ITEM} + OFFSET)) -gt ${MSG_MAX_LENGTH:-0} ]]; then MSG_MAX_LENGTH=$((${#ITEM} + OFFSET)) fi STORAGE_MENU+=("$TAG" "$ITEM" "OFF") done < <(pvesm status -content rootdir | awk 'NR>1') VALID=$(pvesm status -content rootdir | awk 'NR>1') if [[ -z "$VALID" ]]; then msg_error "Unable to detect a valid storage location (rootdir)." exit 1 elif [[ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]]; then STORAGE=${STORAGE_MENU[0]} else while [[ -z "${STORAGE:+x}" ]]; do STORAGE=$(whiptail --backtitle "MyOffice Proxmox" --title "Storage Pools" --radiolist \ "Which storage pool for ${HN}?\n(Spacebar to select)\n" \ 16 $((MSG_MAX_LENGTH + 23)) 6 \ "${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || exit_script done fi msg_ok "Using ${CL}${BL}${STORAGE}${CL}${GN} for storage." } ask_ip_cpu_ram_disk() { # Always prompt: IP, CPU (default 2), RAM MiB (default 2048), disk GiB (default 10). while true; do if CORE_COUNT=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "CPU cores" 8 58 "${CORE_COUNT:-2}" --title "CPU" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$CORE_COUNT" ]] && CORE_COUNT="2" if [[ "$CORE_COUNT" =~ ^[1-9][0-9]*$ ]]; then echo -e "${CPUCORE}${BOLD}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" break fi whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "CPU cores must be a positive integer." 8 58 else exit_script fi done while true; do if RAM_SIZE=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "RAM in MiB" 8 58 "${RAM_SIZE:-2048}" --title "MEMORY" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$RAM_SIZE" ]] && RAM_SIZE="2048" if [[ "$RAM_SIZE" =~ ^[1-9][0-9]*$ ]]; then echo -e "${RAMSIZE}${BOLD}${DGN}RAM: ${BGN}${RAM_SIZE} MiB${CL}" break fi whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "RAM must be a positive integer (MiB)." 8 58 else exit_script fi done while true; do if DISK_SIZE=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Disk size in GiB" 8 58 "${DISK_SIZE:-10}" --title "DISK" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then DISK_SIZE=$(echo "${DISK_SIZE:-10}" | tr -d ' Gg') [[ -z "$DISK_SIZE" ]] && DISK_SIZE="10" if [[ "$DISK_SIZE" =~ ^[1-9][0-9]*$ ]]; then echo -e "${DISKSIZE}${BOLD}${DGN}Disk: ${BGN}${DISK_SIZE}G${CL}" break fi whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "Disk must be a positive integer (GiB)." 8 58 else exit_script fi done if whiptail --backtitle "MyOffice Proxmox" --title "NETWORK" --yesno "Use DHCP for IP?\n\nYes = DHCP\nNo = static IP" --yes-button DHCP --no-button Static 12 58; then NET="dhcp" GATEWAY_IP="" echo -e "${GATEWAY}${BOLD}${DGN}IP: ${BGN}DHCP${CL}" else while true; do if NET=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Static IP with CIDR\n(e.g. 192.168.1.120/24)" 10 58 "${NET:-}" --title "IP ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [[ -n "$NET" && "$NET" == */* ]]; then break fi whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "Enter IP/CIDR, e.g. 192.168.1.120/24" 8 58 else exit_script fi done while true; do if GATEWAY_IP=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Gateway\n(e.g. 192.168.1.1)" 10 58 "${GATEWAY_IP:-}" --title "GATEWAY" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then if [[ -n "$GATEWAY_IP" ]]; then break fi whiptail --backtitle "MyOffice Proxmox" --title "INVALID" --msgbox "Gateway is required for static IP." 8 58 else exit_script fi done echo -e "${GATEWAY}${BOLD}${DGN}IP: ${BGN}${NET} gw ${GATEWAY_IP}${CL}" fi } default_settings() { CTID=$(get_valid_nextid) HN="myoffice" CORE_COUNT="2" RAM_SIZE="2048" DISK_SIZE="10" BRG="vmbr0" NET="dhcp" GATEWAY_IP="" API_PORT="9100" PUBLIC_URL="" # filled after CT IP known START_CT="yes" METHOD="default" echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CTID}${CL}" echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}" echo -e "${OS}${BOLD}${DGN}OS: ${BGN}Debian 12${CL}" ask_ip_cpu_ram_disk echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}" echo -e "${DEFAULT}${BOLD}${DGN}API listen: ${BGN}:${API_PORT}${CL} (SPA from release wwwroot)" echo -e "${CONTAINERTYPE}${BOLD}${DGN}Stack: ${BGN}PostgreSQL + ASP.NET + Gitea release${CL}" echo -e "${CREATING}${BOLD}${DGN}Creating MyOffice LXC${CL}" } advanced_settings() { METHOD="advanced" [[ -z "${CTID:-}" ]] && CTID=$(get_valid_nextid) CORE_COUNT="${CORE_COUNT:-2}" RAM_SIZE="${RAM_SIZE:-2048}" DISK_SIZE="${DISK_SIZE:-10}" NET="${NET:-dhcp}" GATEWAY_IP="${GATEWAY_IP:-}" while true; do if CTID=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Set Container ID" 8 58 "$CTID" --title "CONTAINER ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$CTID" ]] && CTID=$(get_valid_nextid) if pct status "$CTID" &>/dev/null || qm status "$CTID" &>/dev/null; then echo -e "${CROSS}${RD}ID $CTID is already in use${CL}" sleep 2 continue fi echo -e "${CONTAINERID}${BOLD}${DGN}Container ID: ${BGN}${CTID}${CL}" break else exit_script fi done if HN_IN=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Set Hostname" 8 58 myoffice --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then HN=$(echo "${HN_IN:-myoffice}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//' || true) [[ -z "$HN" ]] && HN="myoffice" echo -e "${HOSTNAME}${BOLD}${DGN}Hostname: ${BGN}${HN}${CL}" else exit_script fi ask_ip_cpu_ram_disk if BRG=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Bridge" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$BRG" ]] && BRG="vmbr0" echo -e "${BRIDGE}${BOLD}${DGN}Bridge: ${BGN}${BRG}${CL}" else exit_script fi if API_PORT=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Kestrel listen port" 8 58 9100 --title "API PORT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$API_PORT" ]] && API_PORT="9100" else exit_script fi echo -e "${DEFAULT}${BOLD}${DGN}API listen: ${BGN}:${API_PORT}${CL}" if PUBLIC_URL=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Browser public URL (empty = http://CT_IP:PORT). Use your nginx proxy URL if any." 10 70 "" --title "PUBLIC URL" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then PUBLIC_URL=$(echo "$PUBLIC_URL" | sed 's:/*$::') else exit_script fi if RELEASE_TAG=$(whiptail --backtitle "MyOffice Proxmox" --inputbox "Gitea release tag (latest or v1.2.3)" 8 58 "${RELEASE_TAG}" --title "RELEASE TAG" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then [[ -z "$RELEASE_TAG" ]] && RELEASE_TAG="latest" else exit_script fi echo -e "${DEFAULT}${BOLD}${DGN}Release: ${BGN}${RELEASE_TAG}${CL}" if whiptail --backtitle "MyOffice Proxmox" --title "START CONTAINER" --yesno "Start CT when created?" 10 58; then START_CT="yes" else START_CT="no" fi if whiptail --backtitle "MyOffice Proxmox" --title "ADVANCED SETTINGS" --yesno "Ready to create MyOffice LXC?" --no-button Do-Over 10 58; then echo -e "${CREATING}${BOLD}${DGN}Creating MyOffice LXC with advanced settings${CL}" else header_info echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}" advanced_settings fi } start_script() { if whiptail --backtitle "MyOffice Proxmox" --title "SETTINGS" --yesno "Use Default Settings?" --no-button Advanced 10 58; then header_info echo -e "${BL}Using Default Settings${CL}" default_settings else header_info echo -e "${RD}Using Advanced Settings${CL}" advanced_settings fi } ensure_template() { local template="debian-12-standard" local store="local" msg_info "Ensuring Debian 12 LXC template..." if ! pveam list "$store" 2>/dev/null | grep -q "$template"; then pveam update >/dev/null local remote remote=$(pveam available -section system | awk '/debian-12-standard/ {print $2; exit}') if [[ -z "$remote" ]]; then msg_error "Could not find debian-12-standard template in pveam available." exit 1 fi pveam download "$store" "$remote" fi TEMPLATE_REF=$(pveam list "$store" | awk '/debian-12-standard/ {print $1; exit}') if [[ -z "${TEMPLATE_REF:-}" ]]; then msg_error "Debian 12 template not found after download." exit 1 fi msg_ok "Template ${CL}${BL}${TEMPLATE_REF}${CL}" } create_container() { local net_cfg if [[ "$NET" == "dhcp" ]]; then net_cfg="name=eth0,bridge=${BRG},ip=dhcp" else net_cfg="name=eth0,bridge=${BRG},ip=${NET},gw=${GATEWAY_IP}" fi msg_info "Creating LXC ${CTID} (${HN})..." pct create "$CTID" "$TEMPLATE_REF" \ --hostname "$HN" \ --cores "$CORE_COUNT" \ --memory "$RAM_SIZE" \ --swap 512 \ --rootfs "${STORAGE}:${DISK_SIZE}" \ --net0 "$net_cfg" \ --unprivileged 1 \ --ostype debian \ --onboot 1 \ --timezone host \ --tags "myoffice" \ --description "MyOffice: PostgreSQL + ASP.NET Core from Gitea release. Proxy via external nginx LXC." msg_ok "Created CT ${CL}${BL}${CTID}${CL}" if [[ "$START_CT" == "yes" ]]; then msg_info "Starting CT ${CTID}..." pct start "$CTID" msg_ok "Started CT ${CTID}" else msg_error "CT created but not started (Start=no). Start with: pct start ${CTID}" exit 0 fi } wait_for_network() { msg_info "Waiting for CT network..." local i ip for i in $(seq 1 60); do ip=$(pct exec "$CTID" -- bash -c "ip -4 -o addr show eth0 2>/dev/null | awk '{print \$4}' | cut -d/ -f1 | head -1" 2>/dev/null || true) if [[ -n "$ip" ]]; then CT_IP="$ip" msg_ok "CT IP ${CL}${BL}${CT_IP}${CL}" return 0 fi sleep 2 done msg_error "Timed out waiting for CT IP." exit 1 } resolve_script_dir() { if [[ -n "${BASH_SOURCE[0]:-}" && -f "${BASH_SOURCE[0]}" ]]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" else SCRIPT_DIR="" fi } push_install_assets() { local dest_dir="/tmp/myoffice-proxmox" pct exec "$CTID" -- mkdir -p "$dest_dir" if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/install-in-ct.sh" ]]; then for f in install-in-ct.sh fetch-release.sh myoffice-api.service update.sh; do pct push "$CTID" "${SCRIPT_DIR}/${f}" "${dest_dir}/${f}" done else msg_info "Downloading install assets from REPO_RAW_BASE..." for f in install-in-ct.sh fetch-release.sh myoffice-api.service update.sh; do curl -fsSL "${REPO_RAW_BASE}/proxmox/${f}" -o "/tmp/${f}" pct push "$CTID" "/tmp/${f}" "${dest_dir}/${f}" done msg_ok "Downloaded install assets" fi pct exec "$CTID" -- chmod +x "${dest_dir}/install-in-ct.sh" "${dest_dir}/fetch-release.sh" "${dest_dir}/update.sh" } # CT often cannot reach Gitea (isolated LAN). Download release on the Proxmox host, then push in. download_release_on_host() { local host_tar="/tmp/myoffice-publish.tar.gz" local fetch_src msg_info "Downloading release on Proxmox host..." if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/fetch-release.sh" ]]; then fetch_src="${SCRIPT_DIR}/fetch-release.sh" else curl -fsSL "${REPO_RAW_BASE}/proxmox/fetch-release.sh" -o /tmp/fetch-release.sh fetch_src=/tmp/fetch-release.sh fi # shellcheck disable=SC1090 GITEA_URL="$GITEA_URL" \ GITEA_OWNER="$GITEA_OWNER" \ GITEA_REPO="$GITEA_REPO" \ RELEASE_TAG="$RELEASE_TAG" \ RELEASE_ASSET_URL="$RELEASE_ASSET_URL" \ GITEA_TOKEN="$GITEA_TOKEN" \ bash -c 'source "'"$fetch_src"'" && download_asset_to "'"$host_tar"'"' pct push "$CTID" "$host_tar" "/tmp/myoffice-publish.tar.gz" msg_ok "Release pushed into CT" } run_ct_install() { if [[ -z "${PUBLIC_URL}" ]]; then PUBLIC_URL="http://${CT_IP}:${API_PORT}" fi if [[ -z "${GITEA_URL}" || -z "${GITEA_OWNER}" ]] && [[ -z "${RELEASE_ASSET_URL}" ]]; then msg_error "Set GITEA_URL + GITEA_OWNER (and GITEA_REPO), or RELEASE_ASSET_URL, before install." exit 1 fi download_release_on_host msg_info "Installing PostgreSQL + ASP.NET runtime + Gitea release..." pct exec "$CTID" -- env \ REPO_RAW_BASE="$REPO_RAW_BASE" \ APP_DIR="$APP_DIR" \ PUBLIC_URL="$PUBLIC_URL" \ API_PORT="$API_PORT" \ ASSETS_DIR="/tmp/myoffice-proxmox" \ GITEA_URL="$GITEA_URL" \ GITEA_OWNER="$GITEA_OWNER" \ GITEA_REPO="$GITEA_REPO" \ RELEASE_TAG="$RELEASE_TAG" \ RELEASE_ASSET_URL="$RELEASE_ASSET_URL" \ GITEA_TOKEN="$GITEA_TOKEN" \ LOCAL_RELEASE_TAR="/tmp/myoffice-publish.tar.gz" \ bash /tmp/myoffice-proxmox/install-in-ct.sh msg_ok "Stack installed" } # --- main --- header_info echo -e "\n Loading..." check_root arch_check pve_check if ! whiptail --backtitle "MyOffice Proxmox" --title "MyOffice LXC" --yesno \ "This will create a new LXC with:\n\n • PostgreSQL\n • ASP.NET Core runtime\n • App from Gitea Release\n\nNo Docker, no nginx (point your proxy LXC at :9100).\n\nProceed?" 16 58; then exit_script fi start_script select_storage ensure_template resolve_script_dir create_container wait_for_network push_install_assets run_ct_install echo -e "\n${GN}${BOLD}Completed successfully!${CL}\n" echo -e "${INFO}${YW}Stack in CT ${CTID}:${CL} PostgreSQL + ASP.NET + release ${RELEASE_TAG}" echo -e "${GATEWAY}${BGN}Public URL ${PUBLIC_URL}${CL}" echo -e "${GATEWAY}${BGN}Upstream http://${CT_IP}:${API_PORT}${CL} (for your nginx proxy)" echo -e "${INFO}Demo login: user_UAH@user_UAH.userUAH / user_UAH" echo -e "${INFO}Update: pct enter ${CTID} then ${APP_DIR}/proxmox/update.sh" echo