Deploy: Proxmox VE community-scripts LXC installer (Gitea, build-from-source)
ci / build-test (push) Successful in 1m18s

Adds deploy/ct/metervault.sh (host) + deploy/install/metervault-install.sh (in-LXC), modeled on MQTTower's community-scripts installer but adapted for MeterVault: it lives on public Gitea (not GitHub) and is a single Blazor app + TimescaleDB (not multi-mode). The host script sources the community-scripts framework, creates a Debian 12 LXC and runs the install script via pct exec. The install script sets up PostgreSQL 16 + TimescaleDB, installs the .NET SDK, builds the app from the public Gitea repo (no prebuilt tarball exists — releases ship as a container image), writes /etc/metervault/environment + a systemd unit on port 8760, and installs an 'update' command (git pull + rebuild). scripts/run-metervault-ct-install.sh is the maintainer helper. All three pass shellcheck (warning-level clean) and bash -n. README documents the one-liner.

Claude-Session: https://claude.ai/code/session_01Kib2MniVFbD95fkgLgBBnB
This commit is contained in:
2026-07-17 11:18:27 +02:00
parent 8550ed8d9e
commit 18dd4f720d
4 changed files with 553 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env bash
# shellcheck disable=SC1090 # community-scripts build.func is fetched at runtime
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: FinalFactory
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://git.finalfactory.de/FinalFactory/MeterVault
#
# Creates a Debian LXC and installs MeterVault (self-hosted energy & utility metering) natively:
# PostgreSQL + TimescaleDB, the .NET runtime, and the Blazor app built from the public Gitea repo,
# fronted by a systemd service. Unlike MQTTower (GitHub + prebuilt release tarballs), MeterVault
# lives on a public Gitea and ships only a container image, so this builds the app from source.
APP="MeterVault"
var_tags="${var_tags:-energy;metering;iot}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-3072}"
var_disk="${var_disk:-15}"
var_os="${var_os:-debian}"
# Debian 12 (bookworm): TimescaleDB's apt packages + PGDG PostgreSQL 16 are reliably built for it.
var_version="${var_version:-12}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
# Gitea (public) instead of GitHub: raw file URLs use /raw/branch/<branch>/<path>.
METERVAULT_DEPLOY_BASE="${METERVAULT_DEPLOY_BASE:-https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy}"
METERVAULT_INSTALL_URL="${METERVAULT_INSTALL_URL:-${METERVAULT_DEPLOY_BASE}/install/metervault-install.sh}"
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/metervault ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if [[ ! -d /opt/metervault-src ]]; then
msg_error "No source checkout at /opt/metervault-src — cannot rebuild. Reinstall to restore it."
exit
fi
msg_info "Stopping ${APP}"
systemctl stop metervault
msg_ok "Stopped ${APP}"
msg_info "Pulling latest source and rebuilding (this can take a few minutes)"
cd /opt/metervault-src || exit 1
git fetch --depth 1 origin master
git reset --hard origin/master
dotnet publish src/App/MeterVault.App.csproj -c Release -o /opt/metervault /p:UseAppHost=false
msg_ok "Rebuilt ${APP}"
msg_info "Starting ${APP}"
systemctl start metervault
msg_ok "Started ${APP}"
msg_ok "Updated successfully!"
exit
}
start
# =============================================================================
# Custom questions (after the standard wizard, before build)
# =============================================================================
rand_hex() {
openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p
}
# msg_info() runs a background spinner on stderr; whiptail also draws on stderr. Stop the spinner
# and clear the line first so dialogs don't render corrupted.
metervault_whiptail() {
stop_spinner
clear_line
whiptail "$@" 3>&1 1>&2 2>&3
}
collect_metervault_env() {
export METERVAULT_PORT="${METERVAULT_PORT:-8760}"
export METERVAULT_TIMEZONE="${METERVAULT_TIMEZONE:-Europe/Berlin}"
if [[ -z "${METERVAULT_DB_PASSWORD:-}" ]]; then
METERVAULT_DB_PASSWORD="$(rand_hex)"
msg_info "Generated PostgreSQL password for the metervault user: ${METERVAULT_DB_PASSWORD}"
fi
export METERVAULT_DB_PASSWORD
# Demo dataset: honour an explicit env value, otherwise ask.
local seed="${METERVAULT_SEED:-}"
case "${seed,,}" in
true | 1 | yes) export METERVAULT_SEED="true" ;;
false | 0 | no) export METERVAULT_SEED="false" ;;
*)
if metervault_whiptail --title "${APP}" --yesno \
"Load the bundled Energiebilanz demo dataset (meters, tariffs, ~2000 readings) on first start?" 10 70; then
export METERVAULT_SEED="true"
else
export METERVAULT_SEED="false"
fi
;;
esac
}
# =============================================================================
# Custom build_container — identical to the framework except the install script URL.
# The framework build_container() hardcodes the install URL to community-scripts/ProxmoxVE;
# once MeterVault is accepted upstream this whole function can be replaced by build_container.
# =============================================================================
metervault_build_container() {
# --- Network string (same logic as build.func) ---
NET_STRING="-net0 name=eth0,bridge=${BRG:-vmbr0}"
[[ -n "${MAC:-}" ]] && case "$MAC" in ,hwaddr=*) NET_STRING+="$MAC" ;; *) NET_STRING+=",hwaddr=$MAC" ;; esac
NET_STRING+=",ip=${NET:-dhcp}"
[[ -n "${GATE:-}" ]] && case "$GATE" in ,gw=) ;; ,gw=*) NET_STRING+="$GATE" ;; *) NET_STRING+=",gw=$GATE" ;; esac
[[ -n "${VLAN:-}" ]] && case "$VLAN" in ,tag=*) NET_STRING+="$VLAN" ;; *) NET_STRING+=",tag=$VLAN" ;; esac
[[ -n "${MTU:-}" ]] && case "$MTU" in ,mtu=*) NET_STRING+="$MTU" ;; *) NET_STRING+=",mtu=$MTU" ;; esac
case "${IPV6_METHOD:-none}" in
auto) NET_STRING+=",ip6=auto" ;;
dhcp) NET_STRING+=",ip6=dhcp" ;;
static) [[ -n "${IPV6_ADDR:-}" ]] && { NET_STRING+=",ip6=$IPV6_ADDR"; [[ -n "${IPV6_GATE:-}" ]] && NET_STRING+=",gw6=$IPV6_GATE"; } ;;
esac
# --- Features ---
FEATURES="nesting=1"
[[ "$CT_TYPE" == "1" ]] && FEATURES="${FEATURES},keyctl=1"
# --- Download install.func for the container ---
export FUNCTIONS_FILE_PATH
FUNCTIONS_FILE_PATH="$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func)"
if [[ -z "$FUNCTIONS_FILE_PATH" || ${#FUNCTIONS_FILE_PATH} -lt 100 ]]; then
msg_error "Failed to download install.func"
exit 115
fi
# --- Standard exports (same as build.func build_container) ---
export DIAGNOSTICS="${DIAGNOSTICS:-no}"
export RANDOM_UUID="${RANDOM_UUID:-$(cat /proc/sys/kernel/random/uuid)}"
export EXECUTION_ID="${EXECUTION_ID:-$RANDOM_UUID}"
export SESSION_ID="${SESSION_ID:-${RANDOM_UUID:0:8}}"
export CACHER="${APT_CACHER:-}"
export CACHER_IP="${APT_CACHER_IP:-}"
export tz="${timezone:-$(timedatectl show --value --property=Timezone 2>/dev/null || echo UTC)}"
export APPLICATION="$APP"
export app="$NSAPP"
export PASSWORD="${PW:-}"
export VERBOSE="${VERBOSE:-no}"
export SSH_ROOT="${SSH:-no}"
export SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY:-}"
export CTID="${CT_ID:-}"
export CTTYPE="${CT_TYPE:-1}"
export PCT_OSTYPE="$var_os"
export PCT_OSVERSION="$var_version"
export PCT_DISK_SIZE="${DISK_SIZE:-$var_disk}"
export IPV6_METHOD="${IPV6_METHOD:-none}"
BUILD_LOG="${BUILD_LOG:-/tmp/create-lxc-${SESSION_ID}.log}"
export BUILD_LOG
export INSTALL_LOG="/root/.install-${SESSION_ID}.log"
# --- MeterVault-specific exports ---
export METERVAULT_CT_URL="${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}"
# --- Build PCT_OPTIONS string ---
PCT_OPTIONS_STRING=" -hostname ${HN:-metervault}"
[[ -n "${TAGS:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" -tags $TAGS"
[[ -n "$FEATURES" ]] && PCT_OPTIONS_STRING=" -features $FEATURES"$'\n'"$PCT_OPTIONS_STRING"
[[ -n "${SD:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $SD"
[[ -n "${NS:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $NS"
PCT_OPTIONS_STRING+=$'\n'" $NET_STRING"
PCT_OPTIONS_STRING+=$'\n'" -onboot 1"
PCT_OPTIONS_STRING+=$'\n'" -cores ${CORE_COUNT:-$var_cpu}"
PCT_OPTIONS_STRING+=$'\n'" -memory ${RAM_SIZE:-$var_ram}"
PCT_OPTIONS_STRING+=$'\n'" -unprivileged ${CT_TYPE:-1}"
[[ -n "${PW:-}" ]] && PCT_OPTIONS_STRING+=$'\n'" $PW"
export PCT_OPTIONS="$PCT_OPTIONS_STRING"
export TEMPLATE_STORAGE="${var_template_storage:-}"
export CONTAINER_STORAGE="${var_container_storage:-}"
# --- Create LXC (framework function) ---
create_lxc_container || exit $?
# --- Start container and wait for network ---
msg_info "Starting LXC Container"
pct start "$CTID"
for i in {1..10}; do
if pct status "$CTID" | grep -q "status: running"; then
msg_ok "Started LXC Container"
break
fi
sleep 1
[[ "$i" -eq 10 ]] && { msg_error "LXC Container did not start"; exit 117; }
done
msg_info "Waiting for network in LXC container"
local ip_in_lxc="" wait_secs=0
while true; do
ip_in_lxc=$(pct exec "$CTID" -- ip -4 addr show dev eth0 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
[[ -z "$ip_in_lxc" ]] && ip_in_lxc=$(pct exec "$CTID" -- ip -6 addr show dev eth0 scope global 2>/dev/null | awk '/inet6 / {print $2}' | cut -d/ -f1 | head -n1)
[[ -n "$ip_in_lxc" ]] && break
sleep 1
wait_secs=$((wait_secs + 1))
if ((wait_secs % 20 == 0)); then
msg_warn "No IP on eth0 after ${wait_secs}s — still waiting (Ctrl+C to abort)"
fi
done
msg_ok "Network in LXC is reachable (${ip_in_lxc})"
# --- Base packages ---
msg_info "Customizing LXC Container"
sleep 2
pct exec "$CTID" -- bash -c "apt-get update 2>&1 && apt-get install -y sudo curl mc gnupg2 jq 2>&1" >>"$BUILD_LOG" 2>&1 || {
msg_error "Failed to install base packages"
exit 116
}
msg_ok "Customized LXC Container"
# --- Run MeterVault install script (the ONLY difference vs the framework build_container) ---
# pct exec (not lxc-attach): unprivileged LXC often returns EPERM from lxc-attach, and pct exec
# does not inherit the host env — pass METERVAULT_* explicitly (collect_metervault_env). Append to
# BUILD_LOG so failures show the real error.
msg_info "Running MeterVault install script (full output: ${BUILD_LOG})"
pct exec "$CTID" -- env \
CONTAINER_INSTALLING=true \
"METERVAULT_CT_URL=${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}" \
"METERVAULT_PORT=${METERVAULT_PORT:-8760}" \
"METERVAULT_TIMEZONE=${METERVAULT_TIMEZONE:-Europe/Berlin}" \
"METERVAULT_DB_PASSWORD=${METERVAULT_DB_PASSWORD:-}" \
"METERVAULT_SEED=${METERVAULT_SEED:-false}" \
bash -c "$(curl -fsSL "$METERVAULT_INSTALL_URL")" >>"$BUILD_LOG" 2>&1 || {
msg_error "MeterVault install script failed — see tail of ${BUILD_LOG}"
exit 1
}
}
collect_metervault_env
metervault_build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:${METERVAULT_PORT:-8760}${CL}"
echo -e "${TAB}${INFO}First run: open Import → \"Load reference data\" for a demo (unless you enabled seeding).${CL}"
echo -e "${TAB}${INFO}PostgreSQL user 'metervault' password: ${METERVAULT_DB_PASSWORD:-<generated>} (also in /etc/metervault/environment)${CL}"