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
+15
View File
@@ -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-scriptsstyle 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 |
+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}"
+248
View File
@@ -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 "$@"
+39
View File
@@ -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")"