Deploy: Proxmox VE community-scripts LXC installer (Gitea, build-from-source)
ci / build-test (push) Successful in 1m18s
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:
@@ -46,6 +46,21 @@ docker compose -f deploy/docker-compose.yml up -d
|
|||||||
# API docs at http://localhost:8760/swagger
|
# API docs at http://localhost:8760/swagger
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Quick start (Proxmox VE LXC)
|
||||||
|
|
||||||
|
A community-scripts–style installer builds a self-contained LXC (Debian + PostgreSQL/TimescaleDB +
|
||||||
|
the app as a systemd service). Run **on the Proxmox host**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c "$(curl -fsSL https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy/ct/metervault.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
It asks the standard container questions, optionally loads the demo dataset, and prints the URL
|
||||||
|
(`http://<ct-ip>:8760`) plus the generated DB password. Re-run `update` inside the container to pull
|
||||||
|
the latest source and rebuild. It builds from this public Gitea repo (there is no prebuilt tarball —
|
||||||
|
releases ship as a container image). See [`deploy/ct/metervault.sh`](deploy/ct/metervault.sh) and
|
||||||
|
[`deploy/install/metervault-install.sh`](deploy/install/metervault-install.sh).
|
||||||
|
|
||||||
Configuration is via environment variables (`Section__Key` double-underscore mapping), e.g.:
|
Configuration is via environment variables (`Section__Key` double-underscore mapping), e.g.:
|
||||||
|
|
||||||
| Variable | Purpose |
|
| Variable | Purpose |
|
||||||
|
|||||||
Executable
+251
@@ -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}"
|
||||||
Executable
+248
@@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# Runs inside the LXC: installs PostgreSQL + TimescaleDB and the .NET SDK, builds the MeterVault
|
||||||
|
# Blazor app from the public Gitea repo, and runs it as a systemd service. MeterVault publishes a
|
||||||
|
# container image (not a prebuilt tarball), so — unlike MQTTower — this builds from source.
|
||||||
|
|
||||||
|
: "${GITEA_BASE:=https://git.finalfactory.de}"
|
||||||
|
: "${GITEA_REPO:=FinalFactory/MeterVault}"
|
||||||
|
: "${METERVAULT_REPO_URL:=${GITEA_BASE}/${GITEA_REPO}.git}"
|
||||||
|
: "${METERVAULT_BRANCH:=master}"
|
||||||
|
|
||||||
|
: "${METERVAULT_PORT:=8760}"
|
||||||
|
: "${METERVAULT_TIMEZONE:=Europe/Berlin}"
|
||||||
|
: "${METERVAULT_SEED:=false}"
|
||||||
|
: "${METERVAULT_DB_PASSWORD:=}"
|
||||||
|
|
||||||
|
: "${PG_VERSION:=16}"
|
||||||
|
: "${DOTNET_CHANNEL:=10.0}"
|
||||||
|
: "${INSTALL_DIR:=/opt/metervault}"
|
||||||
|
: "${SOURCE_DIR:=/opt/metervault-src}"
|
||||||
|
: "${ENV_FILE:=/etc/metervault/environment}"
|
||||||
|
: "${DB_NAME:=metervault}"
|
||||||
|
: "${DB_USER:=metervault}"
|
||||||
|
|
||||||
|
INSTALL_FUNC_URL="${INSTALL_FUNC_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/install.func}"
|
||||||
|
FUNCTIONS_FILE_PATH="${FUNCTIONS_FILE_PATH:-$(curl -fsSL "$INSTALL_FUNC_URL")}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091 # community-scripts install.func is fetched at runtime
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
export APPLICATION="${APPLICATION:-MeterVault}"
|
||||||
|
export app="${app:-metervault}"
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || { msg_error "Missing command: $1"; exit 127; }
|
||||||
|
}
|
||||||
|
|
||||||
|
get_ipv4() {
|
||||||
|
hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
rand_hex() {
|
||||||
|
openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p
|
||||||
|
}
|
||||||
|
|
||||||
|
# Unprivileged LXC: systemctl enable may fail creating unit symlinks; start often works anyway.
|
||||||
|
systemctl_enable_now_best_effort() {
|
||||||
|
local svc="$1"
|
||||||
|
if systemctl enable -q --now "$svc" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
msg_info "systemctl enable --now failed for ${svc}; trying start only (common in unprivileged LXC)."
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
if systemctl start "$svc" 2>/dev/null && systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
msg_error "Could not start ${svc}. See: journalctl -xeu ${svc}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Append KEY=VALUE only if KEY is absent (safe re-runs / new keys across versions).
|
||||||
|
ensure_env_key() {
|
||||||
|
local file="$1" key="$2" value="$3"
|
||||||
|
if ! grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||||
|
printf '%s=%s\n' "$key" "$value" >>"$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_postgres_timescaledb() {
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
msg_info "Adding PostgreSQL (PGDG) and TimescaleDB apt repositories"
|
||||||
|
$STD apt-get install -y gnupg postgresql-common apt-transport-https lsb-release wget ca-certificates
|
||||||
|
# PGDG: the official PostgreSQL apt repo (provides a consistent postgresql-${PG_VERSION}).
|
||||||
|
$STD /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||||
|
local codename
|
||||||
|
codename="$(lsb_release -cs)"
|
||||||
|
echo "deb https://packagecloud.io/timescale/timescaledb/debian/ ${codename} main" >/etc/apt/sources.list.d/timescaledb.list
|
||||||
|
wget --quiet -O - https://packagecloud.io/timescale/timescaledb/gpgkey | gpg --dearmor -o /etc/apt/trusted.gpg.d/timescaledb.gpg
|
||||||
|
$STD apt-get update
|
||||||
|
msg_ok "Repositories added"
|
||||||
|
|
||||||
|
msg_info "Installing PostgreSQL ${PG_VERSION} + TimescaleDB"
|
||||||
|
$STD apt-get install -y "postgresql-${PG_VERSION}" "postgresql-client-${PG_VERSION}" "timescaledb-2-postgresql-${PG_VERSION}"
|
||||||
|
msg_ok "Installed PostgreSQL + TimescaleDB"
|
||||||
|
|
||||||
|
msg_info "Enabling the timescaledb preload"
|
||||||
|
local pgconf="/etc/postgresql/${PG_VERSION}/main/postgresql.conf"
|
||||||
|
if command -v timescaledb-tune >/dev/null 2>&1; then
|
||||||
|
timescaledb-tune --quiet --yes --pg-config "/usr/lib/postgresql/${PG_VERSION}/bin/pg_config" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if ! grep -qE "^\s*shared_preload_libraries\s*=.*timescaledb" "$pgconf" 2>/dev/null; then
|
||||||
|
echo "shared_preload_libraries = 'timescaledb'" >>"$pgconf"
|
||||||
|
fi
|
||||||
|
systemctl restart postgresql
|
||||||
|
msg_ok "PostgreSQL restarted with timescaledb preloaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
provision_database() {
|
||||||
|
msg_info "Creating the ${DB_NAME} database and role"
|
||||||
|
[[ -n "${METERVAULT_DB_PASSWORD}" ]] || METERVAULT_DB_PASSWORD="$(rand_hex)"
|
||||||
|
# Role: create or reset the password (idempotent re-runs).
|
||||||
|
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then
|
||||||
|
sudo -u postgres psql -c "ALTER ROLE ${DB_USER} WITH LOGIN PASSWORD '${METERVAULT_DB_PASSWORD}';" >/dev/null
|
||||||
|
else
|
||||||
|
sudo -u postgres psql -c "CREATE ROLE ${DB_USER} WITH LOGIN PASSWORD '${METERVAULT_DB_PASSWORD}';" >/dev/null
|
||||||
|
fi
|
||||||
|
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
|
||||||
|
sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" >/dev/null
|
||||||
|
fi
|
||||||
|
# Pre-create the extension as superuser (CREATE EXTENSION timescaledb needs superuser); the app's
|
||||||
|
# migration then finds it present and its own CREATE EXTENSION IF NOT EXISTS is a no-op.
|
||||||
|
sudo -u postgres psql -d "${DB_NAME}" -c "CREATE EXTENSION IF NOT EXISTS timescaledb;" >/dev/null
|
||||||
|
msg_ok "Database ready"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_dotnet_sdk() {
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
msg_info "Installing the .NET SDK (${DOTNET_CHANNEL}) to build from source"
|
||||||
|
local deb_ver
|
||||||
|
deb_ver="$(. /etc/os-release 2>/dev/null && echo "${VERSION_ID%%.*}" || echo "12")"
|
||||||
|
if [[ "$deb_ver" != "11" && "$deb_ver" != "12" && "$deb_ver" != "13" ]]; then
|
||||||
|
deb_ver="12"
|
||||||
|
fi
|
||||||
|
if [[ ! -f /etc/apt/sources.list.d/microsoft-prod.list && ! -f /etc/apt/sources.list.d/microsoft-prod.sources ]]; then
|
||||||
|
$STD curl -fsSL "https://packages.microsoft.com/config/debian/${deb_ver}/packages-microsoft-prod.deb" -o /tmp/packages-microsoft-prod.deb
|
||||||
|
$STD dpkg -i /tmp/packages-microsoft-prod.deb
|
||||||
|
rm -f /tmp/packages-microsoft-prod.deb
|
||||||
|
fi
|
||||||
|
$STD apt-get update -y
|
||||||
|
$STD apt-get install -y "dotnet-sdk-${DOTNET_CHANNEL}"
|
||||||
|
msg_ok "Installed .NET SDK"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_metervault() {
|
||||||
|
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
|
||||||
|
msg_info "Fetching MeterVault source (${METERVAULT_REPO_URL})"
|
||||||
|
if [[ -d "${SOURCE_DIR}/.git" ]]; then
|
||||||
|
git -C "${SOURCE_DIR}" fetch --depth 1 origin "${METERVAULT_BRANCH}"
|
||||||
|
git -C "${SOURCE_DIR}" reset --hard "origin/${METERVAULT_BRANCH}"
|
||||||
|
else
|
||||||
|
rm -rf "${SOURCE_DIR}"
|
||||||
|
$STD git clone --depth 1 --branch "${METERVAULT_BRANCH}" "${METERVAULT_REPO_URL}" "${SOURCE_DIR}"
|
||||||
|
fi
|
||||||
|
msg_ok "Source ready at ${SOURCE_DIR}"
|
||||||
|
|
||||||
|
msg_info "Building MeterVault (dotnet publish — this can take a few minutes)"
|
||||||
|
# sampledata/ is at the repo root; the App csproj links it as Content so publish ships the demo CSVs.
|
||||||
|
$STD dotnet publish "${SOURCE_DIR}/src/App/MeterVault.App.csproj" -c Release -o "${INSTALL_DIR}" /p:UseAppHost=false
|
||||||
|
msg_ok "Published to ${INSTALL_DIR}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_env() {
|
||||||
|
mkdir -p "$(dirname "${ENV_FILE}")"
|
||||||
|
umask 077
|
||||||
|
local conn="Host=127.0.0.1;Port=5432;Database=${DB_NAME};Username=${DB_USER};Password=${METERVAULT_DB_PASSWORD}"
|
||||||
|
if [[ -f "${ENV_FILE}" ]]; then
|
||||||
|
msg_info "Environment exists; merging new keys and refreshing the connection string"
|
||||||
|
ensure_env_key "${ENV_FILE}" "ASPNETCORE_ENVIRONMENT" "Production"
|
||||||
|
ensure_env_key "${ENV_FILE}" "DOTNET_SYSTEM_GLOBALIZATION_INVARIANT" "false"
|
||||||
|
ensure_env_key "${ENV_FILE}" "ASPNETCORE_URLS" "http://+:${METERVAULT_PORT}"
|
||||||
|
ensure_env_key "${ENV_FILE}" "MeterVault__TimeZone" "${METERVAULT_TIMEZONE}"
|
||||||
|
ensure_env_key "${ENV_FILE}" "MeterVault__Currency" "EUR"
|
||||||
|
ensure_env_key "${ENV_FILE}" "MeterVault__Locale" "en"
|
||||||
|
ensure_env_key "${ENV_FILE}" "MeterVault__SeedReferenceData" "${METERVAULT_SEED}"
|
||||||
|
sed -i "s|^ConnectionStrings__Default=.*|ConnectionStrings__Default=${conn}|" "${ENV_FILE}"
|
||||||
|
grep -q '^ConnectionStrings__Default=' "${ENV_FILE}" || printf 'ConnectionStrings__Default=%s\n' "${conn}" >>"${ENV_FILE}"
|
||||||
|
msg_ok "Updated ${ENV_FILE}"
|
||||||
|
else
|
||||||
|
cat <<EOF >"${ENV_FILE}"
|
||||||
|
ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||||
|
ASPNETCORE_URLS=http://+:${METERVAULT_PORT}
|
||||||
|
ConnectionStrings__Default=${conn}
|
||||||
|
MeterVault__TimeZone=${METERVAULT_TIMEZONE}
|
||||||
|
MeterVault__Currency=EUR
|
||||||
|
MeterVault__Locale=en
|
||||||
|
MeterVault__SeedReferenceData=${METERVAULT_SEED}
|
||||||
|
EOF
|
||||||
|
msg_ok "Wrote ${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
chmod 600 "${ENV_FILE}"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_systemd() {
|
||||||
|
cat <<'EOF' >/etc/systemd/system/metervault.service
|
||||||
|
[Unit]
|
||||||
|
Description=MeterVault (self-hosted energy & utility metering)
|
||||||
|
After=network-online.target postgresql.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=/etc/metervault/environment
|
||||||
|
WorkingDirectory=/opt/metervault
|
||||||
|
ExecStart=/usr/bin/dotnet /opt/metervault/MeterVault.App.dll
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
write_update_command() {
|
||||||
|
local url="${METERVAULT_CT_URL:-https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy/ct/metervault.sh}"
|
||||||
|
cat <<EOF >/usr/bin/update
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exec bash -c "\$(curl -fsSL '${url}')"
|
||||||
|
EOF
|
||||||
|
chmod +x /usr/bin/update
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
need_cmd curl
|
||||||
|
ensure_dependencies git jq openssl sudo
|
||||||
|
|
||||||
|
install_postgres_timescaledb
|
||||||
|
provision_database
|
||||||
|
install_dotnet_sdk
|
||||||
|
build_metervault
|
||||||
|
write_env
|
||||||
|
write_systemd
|
||||||
|
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
systemctl_enable_now_best_effort metervault
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize 2>/dev/null || true
|
||||||
|
write_update_command
|
||||||
|
cleanup_lxc 2>/dev/null || true
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip="$(get_ipv4)"
|
||||||
|
msg_ok "MeterVault is up at http://${ip:-127.0.0.1}:${METERVAULT_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Maintainer helper. Not part of the community-scripts tree under deploy/.
|
||||||
|
#
|
||||||
|
# If bash reports "$'\r': command not found" or "set: pipefail: invalid option", the file has
|
||||||
|
# CRLF line endings. Fix (use the real path to this file — not $0 from an interactive shell):
|
||||||
|
# sed -i 's/\r$//' /tmp/run-metervault-ct-install.sh && bash /tmp/run-metervault-ct-install.sh
|
||||||
|
#
|
||||||
|
# Run this **inside** an existing Debian LXC (not on the Proxmox host). It exports the same
|
||||||
|
# METERVAULT_* variables that deploy/ct/metervault.sh passes via pct exec, then runs the official
|
||||||
|
# install script from the public Gitea repo.
|
||||||
|
#
|
||||||
|
# Optional overrides file: export METERVAULT_INSTALL_ENV=/root/metervault-install.env
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && { echo "Usage: $0 (run as root inside the LXC)"; exit 0; }
|
||||||
|
[[ "$(id -u)" -eq 0 ]] || { echo "error: run as root inside the container" >&2; exit 1; }
|
||||||
|
|
||||||
|
rand_hex() { openssl rand -hex 16 2>/dev/null || head -c 16 /dev/urandom | xxd -p; }
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
[[ -n "${METERVAULT_INSTALL_ENV:-}" && -f "${METERVAULT_INSTALL_ENV}" ]] && { set -a; # shellcheck source=/dev/null
|
||||||
|
source "${METERVAULT_INSTALL_ENV}"; set +a; }
|
||||||
|
|
||||||
|
export CONTAINER_INSTALLING="${CONTAINER_INSTALLING:-true}"
|
||||||
|
export METERVAULT_CT_URL="${METERVAULT_CT_URL:-${METERVAULT_DEPLOY_BASE}/ct/metervault.sh}"
|
||||||
|
export METERVAULT_PORT="${METERVAULT_PORT:-8760}"
|
||||||
|
export METERVAULT_TIMEZONE="${METERVAULT_TIMEZONE:-Europe/Berlin}"
|
||||||
|
export METERVAULT_SEED="${METERVAULT_SEED:-false}"
|
||||||
|
if [[ -z "${METERVAULT_DB_PASSWORD:-}" ]]; then
|
||||||
|
METERVAULT_DB_PASSWORD="$(rand_hex)"
|
||||||
|
echo "Generated METERVAULT_DB_PASSWORD: ${METERVAULT_DB_PASSWORD}"
|
||||||
|
fi
|
||||||
|
export METERVAULT_DB_PASSWORD
|
||||||
|
|
||||||
|
echo "METERVAULT_INSTALL_URL=${METERVAULT_INSTALL_URL} PORT=${METERVAULT_PORT} SEED=${METERVAULT_SEED}"
|
||||||
|
exec bash -c "$(curl -fsSL "$METERVAULT_INSTALL_URL")"
|
||||||
Reference in New Issue
Block a user