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:
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 "$@"
|
||||
Reference in New Issue
Block a user