Compare commits

..

7 Commits

Author SHA1 Message Date
push-app-to-main[bot]
e5f2cc58b8 Add nextexplorer (ct) 2026-03-24 09:27:05 +00:00
community-scripts-pr-app[bot]
5d2fea107d Update CHANGELOG.md (#13250)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-24 08:58:07 +00:00
CanbiZ (MickLesk)
86c658909a Classify exit-1 errors & guard telemetry
Analyze logs for generic exit code 1 and export an ERROR_CATEGORY_OVERRIDE so telemetry receives a more accurate error category (apt, oom, network, storage, dependency). Preserve any existing TELEMETRY_TYPE when posting updates. Add defense-in-depth by disabling strict error traps before running grep/sed log analysis to avoid spurious error_handler invocations. Mark successful installs with INSTALL_COMPLETE and update the error handler to only report a successful "done" telemetry state when INSTALL_COMPLETE is explicitly set, preventing false-positive success reports from early zero-exit exits.
2026-03-24 09:57:43 +01:00
community-scripts-pr-app[bot]
9aa0390553 Update CHANGELOG.md (#13245)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-23 21:22:47 +00:00
CanbiZ (MickLesk)
c8606e9fcc core: harden shell scripts against injection and insecure permissions (#13239) 2026-03-23 22:22:23 +01:00
community-scripts-pr-app[bot]
283e762b83 Update CHANGELOG.md (#13243)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-23 20:54:05 +00:00
MickLesk
15b5542ad6 fix(nginxproxymanager): reset PWD before certbot update to avoid deleted cwd
After building OpenResty, the script does cd /opt/openresty followed by
rm -rf /opt/openresty, leaving $PWD pointing to a deleted directory.
When pip runs in the certbot block, the subprocess inherits the invalid
PWD and fails with OSError: No such file or directory.

Add cd /root before the certbot block to reset to a valid directory.

Closes #13240
2026-03-23 21:53:35 +01:00
14 changed files with 585 additions and 371 deletions

View File

@@ -426,35 +426,15 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-03-24
## 2026-03-23
### 🆕 New Scripts
- Alpine-Borgbackup-Server ([#13219](https://github.com/community-scripts/ProxmoxVE/pull/13219))
### 🚀 Updated Scripts
- NginxProxyManager: build OpenResty from source via GitHub releases [@MickLesk](https://github.com/MickLesk) ([#13134](https://github.com/community-scripts/ProxmoxVE/pull/13134))
- #### 🐞 Bug Fixes
- Tracearr: modify service restart and modify build ressources [@MickLesk](https://github.com/MickLesk) ([#13230](https://github.com/community-scripts/ProxmoxVE/pull/13230))
- #### ✨ New Features
- Kometa: optimize config.yml sed patterns, add Quickstart integration [@MickLesk](https://github.com/MickLesk) ([#13198](https://github.com/community-scripts/ProxmoxVE/pull/13198))
- #### 🔧 Refactor
- Refactor: nginxproxymanager update and OpenResty flow [@MickLesk](https://github.com/MickLesk) ([#13216](https://github.com/community-scripts/ProxmoxVE/pull/13216))
- Refactor: PartDB [@MickLesk](https://github.com/MickLesk) ([#13229](https://github.com/community-scripts/ProxmoxVE/pull/13229))
### 💾 Core
- #### 🔧 Refactor
- core: alpine - Improve network connectivity and DNS checks [@MickLesk](https://github.com/MickLesk) ([#13222](https://github.com/community-scripts/ProxmoxVE/pull/13222))
- core: allow /31 and /32 CIDR with out-of-subnet gateway [@MickLesk](https://github.com/MickLesk) ([#13231](https://github.com/community-scripts/ProxmoxVE/pull/13231))
- core: harden shell scripts against injection and insecure permissions [@MickLesk](https://github.com/MickLesk) ([#13239](https://github.com/community-scripts/ProxmoxVE/pull/13239))
## 2026-03-22

6
ct/headers/nextexplorer Normal file
View File

@@ -0,0 +1,6 @@
__ ______ __
____ ___ _ __/ /_/ ____/ ______ / /___ ________ _____
/ __ \/ _ \| |/_/ __/ __/ | |/_/ __ \/ / __ \/ ___/ _ \/ ___/
/ / / / __/> </ /_/ /____> </ /_/ / / /_/ / / / __/ /
/_/ /_/\___/_/|_|\__/_____/_/|_/ .___/_/\____/_/ \___/_/
/_/

76
ct/nextexplorer.sh Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
# Copyright (c) 2021-2026 community-scripts ORG
# Author: vhsdream
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/nxzai/nextExplorer
APP="nextExplorer"
var_tags="${var_tags:-files;documents}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-3072}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/nextExplorer ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
NODE_VERSION="24" setup_nodejs
if check_for_gh_release "nextExplorer" "nxzai/nextExplorer"; then
msg_info "Stopping nextExplorer"
$STD systemctl stop nextexplorer
msg_ok "Stopped nextExplorer"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "nextExplorer" "nxzai/nextExplorer" "tarball" "latest" "/opt/nextExplorer"
msg_info "Updating nextExplorer"
APP_DIR="/opt/nextExplorer/app"
mkdir -p "$APP_DIR"
cd /opt/nextExplorer
export NODE_ENV=production
$STD npm ci --omit=dev --workspace backend
mv node_modules "$APP_DIR"
mv backend/{src,package.json} "$APP_DIR"
unset NODE_ENV
export NODE_ENV=development
$STD npm ci --workspace frontend
$STD npm run -w frontend build -- --sourcemap false
unset NODE_ENV
mv frontend/dist/ "$APP_DIR"/src/public
chown -R explorer:explorer "$APP_DIR" /etc/nextExplorer
sed -i "\|version|s|$(jq -cr '.version' ${APP_DIR}/package.json)|$(cat ~/.nextexplorer)|" "$APP_DIR"/package.json
sed -i 's/app.js/server.js/' /etc/systemd/system/nextexplorer.service && systemctl daemon-reload
msg_ok "Updated nextExplorer"
msg_info "Starting nextExplorer"
$STD systemctl start nextexplorer
msg_ok "Started nextExplorer"
msg_ok "Updated successfully!"
fi
exit
}
start
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}:3000${CL}"

View File

@@ -102,6 +102,7 @@ EOF
msg_ok "Built OpenResty"
fi
cd /root
if [ -d /opt/certbot ]; then
msg_info "Updating Certbot"
$STD /opt/certbot/bin/pip install --upgrade pip setuptools wheel

View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: vhsdream
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/nxzai/nextExplorer
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
ripgrep \
imagemagick \
ffmpeg \
libva-drm2 \
libva2 \
mesa-va-drivers \
vainfo
msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "nextExplorer" "nxzai/nextExplorer" "tarball" "latest" "/opt/nextExplorer"
msg_info "Building nextExplorer"
APP_DIR="/opt/nextExplorer/app"
LOCAL_IP="$(hostname -I | awk '{print $1}')"
mkdir -p "$APP_DIR"
mkdir -p /etc/nextExplorer
cd /opt/nextExplorer
export NODE_ENV=production
$STD npm ci --omit=dev --workspace backend
mv node_modules "$APP_DIR"
mv backend/{src,package.json} "$APP_DIR"
unset NODE_ENV
export NODE_ENV=development
export NODE_OPTIONS="--max-old-space-size=2048"
$STD npm ci --workspace frontend
$STD npm run -w frontend build -- --sourcemap false
unset NODE_ENV
mv frontend/dist/ "$APP_DIR"/src/public
msg_ok "Built nextExplorer"
msg_info "Configuring nextExplorer"
SECRET=$(openssl rand -hex 32)
cat <<EOF >/etc/nextExplorer/.env
NODE_ENV=production
PORT=3000
VOLUME_ROOT=/mnt
CONFIG_DIR=/etc/nextExplorer
CACHE_DIR=/etc/nextExplorer/cache
# USER_ROOT=
PUBLIC_URL=${LOCAL_IP}:3000
# TRUST_PROXY=
# CORS_ORIGINS=
TERMINAL_ENABLED=false
LOG_LEVEL=info
DEBUG=false
ENABLE_HTTP_LOGGING=false
AUTH_ENABLED=true
AUTH_MODE=both
SESSION_SECRET="${SECRET}"
# AUTH_MAX_FAILED=
# AUTH_LOCK_MINUTES=
# AUTH_USER_EMAIL=
# AUTH_USER_PASSWORD=
# OIDC_ENABLED=
# OIDC_ISSUER=
# OIDC_AUTHORIZATION_URL=
# OIDC_TOKEN_URL=
# OIDC_USERINFO_URL=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_CALLBACK_URL=
# OIDC_LOGOUT_URL=
# OIDC_SCOPES=
# OIDC_AUTO_CREATE_USERS=true
# SEARCH_DEEP=
# SEARCH_RIPGREP=
# SEARCH_MAX_FILESIZE=
# ONLYOFFICE_URL=
# ONLYOFFICE_SECRET=
# ONLYOFFICE_LANG=
# ONLYOFFICE_FORCE_SAVE=
# ONLYOFFICE_FILE_EXTENSIONS=
# COLLABORA_URL=
# COLLABORA_DISCOVERY_URL=
# COLLABORA_SECRET=
# COLLABORA_LANG=
# COLLABORA_FILE_EXTENSIONS=
SHOW_VOLUME_USAGE=true
# USER_DIR_ENABLED=
# SKIP_HOME=
# EDITOR_EXTENSIONS=
# FFMPEG_PATH=
# FFPROBE_PATH=
## Hardware acceleration
# FFMPEG_HWACCEL=vaapi
# FFMPEG_HWACCEL_DEVICE=/dev/dri/renderD128
# FFMPEG_HWACCEL_OUTPUT_FORMAT=nv12
FAVORITES_DEFAULT_ICON=outline.StarIcon
SHARES_ENABLED=true
# SHARES_TOKEN_LENGTH=10
# SHARES_MAX_PER_USER=100
# SHARES_DEFAULT_EXPIRY_DAYS=30
# SHARES_GUEST_SESSION_HOURS=24
# SHARES_ALLOW_PASSWORD=true
# SHARES_ALLOW_ANONYMOUS=true
EOF
chmod 600 /etc/nextExplorer/.env
$STD useradd -U -s /usr/sbin/nologin -m -d /home/explorer explorer
chown -R explorer:explorer "$APP_DIR" /etc/nextExplorer
sed -i "\|version|s|$(jq -cr '.version' ${APP_DIR}/package.json)|$(cat ~/.nextexplorer)|" "$APP_DIR"/package.json
msg_ok "Configured nextExplorer"
msg_info "Creating nextExplorer Service"
cat <<EOF >/etc/systemd/system/nextexplorer.service
[Unit]
Description=nextExplorer Service
After=network.target
[Service]
Type=simple
User=explorer
Group=explorer
WorkingDirectory=/opt/nextExplorer/app
EnvironmentFile=/etc/nextExplorer/.env
ExecStart=/usr/bin/node ./src/server.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
$STD systemctl enable -q --now nextexplorer
msg_ok "Created nextExplorer Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -35,7 +35,7 @@ cd Shinobi
gitVersionNumber=$(git rev-parse HEAD)
theDateRightNow=$(date)
touch version.json
chmod 777 version.json
chmod 644 version.json
echo '{"Product" : "'"Shinobi"'" , "Branch" : "'"master"'" , "Version" : "'"$gitVersionNumber"'" , "Date" : "'"$theDateRightNow"'" , "Repository" : "'"https://gitlab.com/Shinobi-Systems/Shinobi.git"'"}' >version.json
msg_ok "Cloned Shinobi"

View File

@@ -23,7 +23,7 @@ fetch_and_deploy_gh_release "tasmoadmin" "TasmoAdmin/TasmoAdmin" "prebuild" "lat
msg_info "Configuring TasmoAdmin"
rm -rf /etc/php/8.4/apache2/conf.d/10-opcache.ini
chown -R www-data:www-data /var/www/tasmoadmin
chmod 777 /var/www/tasmoadmin/tmp /var/www/tasmoadmin/data
chmod 775 /var/www/tasmoadmin/tmp /var/www/tasmoadmin/data
cat <<EOF >/etc/apache2/sites-available/tasmoadmin.conf
<VirtualHost *:9999>
ServerName tasmoadmin

View File

@@ -348,10 +348,10 @@ explain_exit_code() {
json_escape() {
# Escape a string for safe JSON embedding using awk (handles any input size).
# Pipeline: strip ANSI → remove control chars → escape \ " TAB → join lines with \n
printf '%s' "$1" \
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
| tr -d '\000-\010\013\014\016-\037\177\r' \
| awk '
printf '%s' "$1" |
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' |
tr -d '\000-\010\013\014\016-\037\177\r' |
awk '
BEGIN { ORS = "" }
{
gsub(/\\/, "\\\\") # backslash → \\
@@ -627,7 +627,7 @@ post_to_api() {
[[ "${DEV_MODE:-}" == "true" ]] && echo "[DEBUG] post_to_api() DIAGNOSTICS=$DIAGNOSTICS RANDOM_UUID=$RANDOM_UUID NSAPP=$NSAPP" >&2
# Set type for later status updates (respect pre-set value, e.g. "turnkey")
# Set type for later status updates (preserve if already set, e.g. turnkey)
TELEMETRY_TYPE="${TELEMETRY_TYPE:-lxc}"
local pve_version=""
@@ -664,7 +664,7 @@ post_to_api() {
{
"random_id": "${RANDOM_UUID}",
"execution_id": "${EXECUTION_ID:-${RANDOM_UUID}}",
"type": "${TELEMETRY_TYPE}",
"type": "lxc",
"nsapp": "${NSAPP:-unknown}",
"status": "installing",
"ct_type": ${CT_TYPE:-1},
@@ -692,6 +692,7 @@ EOF
# Send initial "installing" record with retry.
# This record MUST exist for all subsequent updates to succeed.
local http_code="" attempt
local _post_success=false
for attempt in 1 2 3; do
if [[ "${DEV_MODE:-}" == "true" ]]; then
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
@@ -703,11 +704,19 @@ EOF
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
fi
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
_post_success=true
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
POST_TO_API_DONE=true
# Only mark done if at least one attempt succeeded.
# If all 3 failed, POST_TO_API_DONE stays false so post_update_to_api
# and on_exit() know the initial record was never created.
# The server has fallback logic to create a new record on status updates,
# so subsequent calls can still succeed even without the initial record.
POST_TO_API_DONE=${_post_success}
}
# ------------------------------------------------------------------------------
@@ -798,15 +807,19 @@ EOF
# Send initial "installing" record with retry (must succeed for updates to work)
local http_code="" attempt
local _post_success=false
for attempt in 1 2 3; do
http_code=$(curl -sS -w "%{http_code}" -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" -o /dev/null 2>/dev/null) || http_code="000"
[[ "$http_code" =~ ^2[0-9]{2}$ ]] && break
if [[ "$http_code" =~ ^2[0-9]{2}$ ]]; then
_post_success=true
break
fi
[[ "$attempt" -lt 3 ]] && sleep 1
done
POST_TO_API_DONE=true
POST_TO_API_DONE=${_post_success}
}
# ------------------------------------------------------------------------------
@@ -1083,6 +1096,12 @@ EOF
# - Used to group errors in dashboard
# ------------------------------------------------------------------------------
categorize_error() {
# Allow build.func to override category based on log analysis (exit code 1 subclassification)
if [[ -n "${ERROR_CATEGORY_OVERRIDE:-}" ]]; then
echo "$ERROR_CATEGORY_OVERRIDE"
return
fi
local code="$1"
case "$code" in
# Network errors (curl/wget)

View File

@@ -221,6 +221,14 @@ update_motd_ip() {
local current_hostname="$(hostname)"
local current_ip="$(hostname -I | awk '{print $1}')"
# Escape sed special chars in replacement strings (& \ |)
current_os="${current_os//\\/\\\\}"
current_os="${current_os//&/\\&}"
current_hostname="${current_hostname//\\/\\\\}"
current_hostname="${current_hostname//&/\\&}"
current_ip="${current_ip//\\/\\\\}"
current_ip="${current_ip//&/\\&}"
# Update only if values actually changed
if ! grep -q "OS:.*$current_os" "$PROFILE_FILE" 2>/dev/null; then
sed -i "s|OS:.*|OS: \${GN}$current_os\${CL}\\\"|" "$PROFILE_FILE"
@@ -4076,8 +4084,8 @@ EOF
if [ "$var_os" == "alpine" ]; then
sleep 3
pct exec "$CTID" -- /bin/sh -c 'cat <<EOF >/etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/latest-stable/main
http://dl-cdn.alpinelinux.org/alpine/latest-stable/community
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF'
pct exec "$CTID" -- ash -c "apk add bash newt curl openssh nano mc ncurses jq" >>"$BUILD_LOG" 2>&1 || {
msg_error "Failed to install base packages in Alpine container"
@@ -4086,7 +4094,9 @@ EOF'
else
sleep 3
LANG=${LANG:-en_US.UTF-8}
pct exec "$CTID" -- bash -c "sed -i \"/$LANG/ s/^# //\" /etc/locale.gen"
local LANG_ESC="${LANG//./\\.}"
LANG_ESC="${LANG_ESC//|/\\|}"
pct exec "$CTID" -- bash -c "sed -i \"/$LANG_ESC/ s/^# //\" /etc/locale.gen"
pct exec "$CTID" -- bash -c "locale_line=\$(grep -v '^#' /etc/locale.gen | grep -E '^[a-zA-Z]' | awk '{print \$1}' | head -n 1) && \
echo LANG=\$locale_line >/etc/default/locale && \
locale-gen >/dev/null && \
@@ -4216,6 +4226,53 @@ EOF'
fi
fi
# Defense-in-depth: Ensure error handling stays disabled during recovery.
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
# and trap 'error_handler' ERR. If any code path above called such a function,
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
set +Eeuo pipefail
trap - ERR
# --- Exit code 1 subclassification: analyze logs BEFORE telemetry call ---
# Exit code 1 is generic ("General error"). Analyze logs to determine the
# real error category so telemetry gets a useful classification instead of "shell".
local is_oom=false
local is_network_issue=false
local is_apt_issue=false
local is_cmd_not_found=false
local is_disk_full=false
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
is_apt_issue=true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
is_oom=true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
is_network_issue=true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
is_cmd_not_found=true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
fi
# Set override for categorize_error() so telemetry gets the real category
if [[ "$is_apt_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
elif [[ "$is_oom" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="resource"
elif [[ "$is_network_issue" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="network"
elif [[ "$is_disk_full" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="storage"
elif [[ "$is_cmd_not_found" == true ]]; then
export ERROR_CATEGORY_OVERRIDE="dependency"
fi
# Report failure to telemetry API (now with log available on host)
# NOTE: Do NOT use msg_info/spinner here — the background spinner process
# causes SIGTSTP in non-interactive shells (bash -c "$(curl ...)"), which
@@ -4224,13 +4281,6 @@ EOF'
post_update_to_api "failed" "$install_exit_code"
$STD echo -e "${TAB}${CM:-} Failure reported"
# Defense-in-depth: Ensure error handling stays disabled during recovery.
# Some functions (e.g. silent/$STD) unconditionally re-enable set -Eeuo pipefail
# and trap 'error_handler' ERR. If any code path above called such a function,
# the grep/sed pipelines below would trigger error_handler on non-match (exit 1).
set +Eeuo pipefail
trap - ERR
# Show combined log location
if [[ -n "$CTID" && -n "${SESSION_ID:-}" ]]; then
msg_custom "📋" "${YW}" "Installation log: ${combined_log}"
@@ -4259,12 +4309,9 @@ EOF'
# Prompt user for cleanup with 60s timeout
echo ""
# Detect error type for smart recovery options
local is_oom=false
local is_network_issue=false
local is_apt_issue=false
local is_cmd_not_found=false
local is_disk_full=false
# Extend error detection for non-exit-1 codes (exit 1 was already analyzed above)
# The is_* flags were set above for exit code 1 log analysis; here we add
# exit-code-specific detections for other codes.
local error_explanation=""
if declare -f explain_exit_code >/dev/null 2>&1; then
error_explanation="$(explain_exit_code "$install_exit_code")"
@@ -4314,26 +4361,6 @@ EOF'
;;
esac
# Exit 1 subclassification: analyze logs to identify actual root cause
# Many exit 1 errors are actually APT, OOM, network, or command-not-found issues
if [[ $install_exit_code -eq 1 && -f "$combined_log" ]]; then
if grep -qiE 'E: Unable to|E: Package|E: Failed to fetch|dpkg.*error|broken packages|unmet dependencies|dpkg --configure -a' "$combined_log"; then
is_apt_issue=true
fi
if grep -qiE 'Cannot allocate memory|Out of memory|oom-killer|Killed process|JavaScript heap' "$combined_log"; then
is_oom=true
fi
if grep -qiE 'Could not resolve|DNS|Connection refused|Network is unreachable|No route to host|Temporary failure resolving|Failed to fetch' "$combined_log"; then
is_network_issue=true
fi
if grep -qiE ': command not found|No such file or directory.*/s?bin/' "$combined_log"; then
is_cmd_not_found=true
fi
if grep -qiE 'ENOSPC|no space left on device|Disk quota exceeded|errno -28' "$combined_log"; then
is_disk_full=true
fi
fi
# Show error explanation if available
if [[ -n "$error_explanation" ]]; then
echo -e "${TAB}${RD}Error: ${error_explanation}${CL}"
@@ -4535,6 +4562,7 @@ EOF'
if [[ $apt_retry_code -eq 0 ]]; then
msg_ok "Installation completed successfully after APT repair!"
INSTALL_COMPLETE=true
post_update_to_api "done" "0" "force"
return 0
else
@@ -4759,6 +4787,10 @@ fix_gpu_gids() {
pct stop "$CTID" >/dev/null 2>&1
sleep 1
# Validate GIDs are numeric before sed
[[ "$render_gid" =~ ^[0-9]+$ ]] || render_gid="104"
[[ "$video_gid" =~ ^[0-9]+$ ]] || video_gid="44"
# Update dev entries with correct GIDs
sed -i.bak -E "s|(dev[0-9]+: /dev/dri/renderD[0-9]+),gid=[0-9]+|\1,gid=${render_gid}|g" "$LXC_CONFIG"
sed -i -E "s|(dev[0-9]+: /dev/dri/card[0-9]+),gid=[0-9]+|\1,gid=${video_gid}|g" "$LXC_CONFIG"
@@ -5705,6 +5737,7 @@ EOF
systemctl start ping-instances.service
fi
INSTALL_COMPLETE=true
post_update_to_api "done" "none"
}

View File

@@ -507,14 +507,23 @@ _stop_container_if_installing() {
on_exit() {
local exit_code=$?
# Report orphaned "installing" records to telemetry API
# Catches ALL exit paths: errors, signals, AND clean exits where
# post_to_api was called but post_update_to_api was never called
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
if [[ $exit_code -ne 0 ]]; then
_send_abort_telemetry "$exit_code"
elif declare -f post_update_to_api >/dev/null 2>&1; then
post_update_to_api "done" "0" 2>/dev/null || true
# Report orphaned telemetry records
# Two scenarios handled:
# 1. POST_TO_API_DONE=true but POST_UPDATE_DONE=false: Record was created but
# never got a final status update → send abort/done now.
# 2. POST_TO_API_DONE=false but DIAGNOSTICS=yes: Initial post failed (server
# unreachable/timeout), but the server has fallback create-on-update logic,
# so a status update can still create the record. Worth one last try.
if [[ "${POST_UPDATE_DONE:-}" != "true" ]]; then
if [[ "${POST_TO_API_DONE:-}" == "true" || "${DIAGNOSTICS:-no}" == "yes" ]]; then
if [[ $exit_code -ne 0 ]]; then
_send_abort_telemetry "$exit_code"
elif [[ "${INSTALL_COMPLETE:-}" == "true" ]] && declare -f post_update_to_api >/dev/null 2>&1; then
# Only report success if the install was explicitly marked complete.
# Without this guard, early bailouts (e.g. user cancelled) with exit 0
# would be falsely reported as successful installations.
post_update_to_api "done" "0" 2>/dev/null || true
fi
fi
fi

View File

@@ -309,14 +309,14 @@ customize() {
if [[ "$PASSWORD" == "" ]]; then
msg_info "Customizing Container"
GETTY_OVERRIDE="/etc/systemd/system/container-getty@1.service.d/override.conf"
mkdir -p $(dirname $GETTY_OVERRIDE)
cat <<EOF >$GETTY_OVERRIDE
mkdir -p "$(dirname "$GETTY_OVERRIDE")"
cat <<EOF >"$GETTY_OVERRIDE"
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM
EOF
systemctl daemon-reload
systemctl restart $(basename $(dirname $GETTY_OVERRIDE) | sed 's/\.d//')
systemctl restart "$(basename "$(dirname "$GETTY_OVERRIDE")" | sed 's/\.d//')"
msg_ok "Customized Container"
fi
echo "bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/${app}.sh)\"" >/usr/bin/update

View File

@@ -242,7 +242,7 @@ download_gpg_key() {
# Process based on mode
if [[ "$mode" == "dearmor" ]]; then
if gpg --dearmor --yes -o "$output" <"$temp_key" 2>/dev/null; then
if gpg --dearmor --yes -o "$output" <"$temp_key" 2>/dev/null && [[ -s "$output" ]]; then
rm -f "$temp_key"
debug_log "GPG key installed (dearmored): $output"
return 0
@@ -5192,7 +5192,7 @@ _setup_gpu_permissions() {
for nvidia_dev in /dev/nvidia*; do
[[ -e "$nvidia_dev" ]] && {
chgrp video "$nvidia_dev" 2>/dev/null || true
chmod 666 "$nvidia_dev" 2>/dev/null || true
chmod 660 "$nvidia_dev" 2>/dev/null || true
}
done
if [[ -d /dev/nvidia-caps ]]; then
@@ -5200,7 +5200,7 @@ _setup_gpu_permissions() {
for caps_dev in /dev/nvidia-caps/*; do
[[ -e "$caps_dev" ]] && {
chgrp video "$caps_dev" 2>/dev/null || true
chmod 666 "$caps_dev" 2>/dev/null || true
chmod 660 "$caps_dev" 2>/dev/null || true
}
done
fi
@@ -5217,7 +5217,8 @@ _setup_gpu_permissions() {
# /dev/kfd permissions (AMD ROCm)
if [[ -e /dev/kfd ]]; then
chmod 666 /dev/kfd 2>/dev/null || true
chgrp render /dev/kfd 2>/dev/null || true
chmod 660 /dev/kfd 2>/dev/null || true
msg_info "AMD ROCm compute device configured"
fi

View File

@@ -150,7 +150,7 @@ function install() {
curl -fsSL "https://raw.githubusercontent.com/runtipi/runtipi/master/scripts/install.sh" -o "install.sh"
chmod +x install.sh
$STD ./install.sh
chmod 666 /opt/runtipi/state/settings.json 2>/dev/null || true
chmod 660 /opt/runtipi/state/settings.json 2>/dev/null || true
rm -f /opt/install.sh
msg_ok "Installed ${APP}"

View File

@@ -1,23 +1,10 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Copyright (c) 2021-2026 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# License: MIT
# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source shared libraries
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func)
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func)
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func)
load_functions
catch_errors
APP="TurnKey LXC"
NSAPP="turnkey"
DIAGNOSTICS="no"
METHOD="default"
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
EXECUTION_ID="${RANDOM_UUID}"
header_info() {
function header_info {
clear
cat <<"EOF"
______ __ __ __ _ _______
@@ -28,343 +15,281 @@ header_info() {
EOF
}
# Validate if a container ID is available (cluster-aware)
validate_container_id() {
set -euo pipefail
shopt -s expand_aliases
alias die='EXIT=$? LINE=$LINENO error_exit'
trap die ERR
function error_exit() {
trap - ERR
local DEFAULT='Unknown failure occured.'
local REASON="\e[97m${1:-$DEFAULT}\e[39m"
local FLAG="\e[91m[ERROR] \e[93m$EXIT@$LINE"
msg "$FLAG $REASON" 1>&2
[ ! -z ${CTID-} ] && cleanup_ctid
exit $EXIT
}
function warn() {
local REASON="\e[97m$1\e[39m"
local FLAG="\e[93m[WARNING]\e[39m"
msg "$FLAG $REASON"
}
function info() {
local REASON="$1"
local FLAG="\e[36m[INFO]\e[39m"
msg "$FLAG $REASON"
}
function msg() {
local TEXT="$1"
echo -e "$TEXT"
}
function validate_container_id() {
local ctid="$1"
[[ "$ctid" =~ ^[0-9]+$ ]] || return 1
# Cluster-wide check via pvesh
if command -v pvesh &>/dev/null; then
local cluster_ids
cluster_ids=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null |
grep -oP '"vmid":\s*\K[0-9]+' 2>/dev/null || true)
if [[ -n "$cluster_ids" ]] && echo "$cluster_ids" | grep -qw "$ctid"; then
return 1
fi
# Check if ID is numeric
if ! [[ "$ctid" =~ ^[0-9]+$ ]]; then
return 1
fi
# Local fallback
# Check if config file exists for VM or LXC
if [[ -f "/etc/pve/qemu-server/${ctid}.conf" ]] || [[ -f "/etc/pve/lxc/${ctid}.conf" ]]; then
return 1
fi
# Check all cluster nodes
if [[ -d "/etc/pve/nodes" ]]; then
for node_dir in /etc/pve/nodes/*/; do
if [[ -f "${node_dir}qemu-server/${ctid}.conf" ]] || [[ -f "${node_dir}lxc/${ctid}.conf" ]]; then
return 1
fi
done
fi
# Check LVM volumes
# Check if ID is used in LVM logical volumes
if lvs --noheadings -o lv_name 2>/dev/null | grep -qE "(^|[-_])${ctid}($|[-_])"; then
return 1
fi
return 0
}
get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid 2>/dev/null || echo 100)}"
function get_valid_container_id() {
local suggested_id="${1:-$(pvesh get /cluster/nextid)}"
while ! validate_container_id "$suggested_id"; do
suggested_id=$((suggested_id + 1))
done
echo "$suggested_id"
}
cleanup_ctid() {
if pct status "$CTID" &>/dev/null; then
if [[ "$(pct status "$CTID" | awk '{print $2}')" == "running" ]]; then
pct stop "$CTID"
function cleanup_ctid() {
if pct status $CTID &>/dev/null; then
if [ "$(pct status $CTID | awk '{print $2}')" == "running" ]; then
pct stop $CTID
fi
pct destroy "$CTID"
pct destroy $CTID
fi
}
select_storage() {
local class="$1" content content_label
case "$class" in
container)
content='rootdir'
content_label='Container'
;;
template)
content='vztmpl'
content_label='Container template'
;;
*)
msg_error "Invalid storage class '$class'"
return 1
;;
esac
local -a MENU=()
local MSG_MAX_LENGTH=0
while read -r line; do
local TAG TYPE FREE ITEM OFFSET=2
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 | awk '{printf( "%9sB", $6)}')
ITEM=" Type: $TYPE Free: $FREE "
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=$((${#ITEM} + OFFSET))
MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content "$content" | awk 'NR>1')
if [[ $((${#MENU[@]} / 3)) -eq 0 ]]; then
msg_error "'$content_label' needs to be selected for at least one storage location."
return 1
elif [[ $((${#MENU[@]} / 3)) -eq 1 ]]; then
printf '%s' "${MENU[0]}"
else
local STORAGE
while [[ -z "${STORAGE:+x}" ]]; do
STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
"Which storage pool for the ${content_label,,}?\n\n" \
16 $((MSG_MAX_LENGTH + 23)) 6 \
"${MENU[@]}" 3>&1 1>&2 2>&3) || exit_script
done
printf '%s' "$STORAGE"
fi
}
# ==============================================================================
# MAIN
# ==============================================================================
# Cleanup on error: destroy container, report telemetry, and restart monitor
turnkey_cleanup() {
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
# Report failure to telemetry
if [[ "${POST_TO_API_DONE:-}" == "true" && "${POST_UPDATE_DONE:-}" != "true" ]]; then
post_update_to_api "failed" "$exit_code" 2>/dev/null || true
fi
# Destroy failed container
if [[ -n "${CTID:-}" ]]; then
cleanup_ctid 2>/dev/null || true
fi
fi
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service 2>/dev/null || true
fi
}
trap turnkey_cleanup EXIT
# Stop Proxmox VE Monitor-All if running
if systemctl is-active -q ping-instances.service; then
systemctl stop ping-instances.service
fi
pve_check
shell_check
root_check
# Read diagnostics preference (same logic as build.func diagnostics_check)
DIAG_CONFIG="/usr/local/community-scripts/diagnostics"
if [[ -f "$DIAG_CONFIG" ]]; then
DIAGNOSTICS=$(awk -F '=' '/^DIAGNOSTICS/ {print $2}' "$DIAG_CONFIG") || true
DIAGNOSTICS="${DIAGNOSTICS:-no}"
fi
header_info
whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --yesno \
"This will allow for the creation of one of the many TurnKey LXC Containers. Proceed?" 10 68 || exit_script
# Update template catalog early so the menu reflects the latest available templates
msg_info "Updating LXC template list"
pveam update >/dev/null
msg_ok "Updated LXC template list"
# Build TurnKey selection menu dynamically from available templates
declare -A TURNKEY_TEMPLATES
whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --yesno "This will allow for the creation of one of the many TurnKey LXC Containers. Proceed?" 10 68
TURNKEY_MENU=()
MSG_MAX_LENGTH=0
while IFS=$'\t' read -r TEMPLATE_FILE TAG ITEM; do
TURNKEY_TEMPLATES["$TAG"]="$TEMPLATE_FILE"
while read -r TAG ITEM; do
OFFSET=2
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=$((${#ITEM} + OFFSET))
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=${#ITEM}+OFFSET
TURNKEY_MENU+=("$TAG" "$ITEM " "OFF")
done < <(pveam available -section turnkeylinux | awk '{
tpl = $2
if (match(tpl, /debian-([0-9]+)-turnkey-([^_]+)_([^_]+)_/, m)) {
app = m[2]; deb = m[1]; ver = m[3]
display = app
gsub(/-/, " ", display)
n = split(display, words, " ")
display = ""
for (i = 1; i <= n; i++) {
words[i] = toupper(substr(words[i], 1, 1)) substr(words[i], 2)
display = display (i > 1 ? " " : "") words[i]
}
tag = app "-" deb
printf "%s\t%s\t%s | Debian %s | %s\n", tpl, tag, display, deb, ver
}
}' | sort -t$'\t' -k2,2)
done < <(
cat <<EOF
ansible Ansible
bookstack BookStack
core Core
faveo-helpdesk Faveo Helpdesk
fileserver File Server
gallery Gallery
gameserver Game Server
gitea Gitea
gitlab GitLab
invoice-ninja Invoice Ninja
mediaserver Media Server
nextcloud Nextcloud
observium Observium
odoo Odoo
openldap OpenLDAP
openvpn OpenVPN
owncloud ownCloud
phpbb phpBB
torrentserver Torrent Server
wireguard WireGuard
wordpress Wordpress
zoneminder ZoneMinder
EOF
)
turnkey=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --radiolist "\nSelect a TurnKey LXC to create:\n" 16 $((MSG_MAX_LENGTH + 58)) 6 "${TURNKEY_MENU[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
[ -z "$turnkey" ] && {
whiptail --backtitle "Proxmox VE Helper Scripts" --title "No TurnKey LXC Selected" --msgbox "It appears that no TurnKey LXC container was selected" 10 68
msg "Done"
exit
}
if [[ ${#TURNKEY_MENU[@]} -eq 0 ]]; then
msg_error "No TurnKey templates found. Check your internet connection or template repository."
exit 1
fi
selected=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "TurnKey LXCs" --radiolist \
"\nSelect a TurnKey LXC to create:\n" 20 $((MSG_MAX_LENGTH + 58)) 12 \
"${TURNKEY_MENU[@]}" 3>&1 1>&2 2>&3 | tr -d '"') || exit_script
if [[ -z "$selected" ]]; then
whiptail --backtitle "Proxmox VE Helper Scripts" --title "No TurnKey LXC Selected" \
--msgbox "It appears that no TurnKey LXC container was selected" 10 68
exit_script
fi
# Extract template filename and app name from selection
TEMPLATE="${TURNKEY_TEMPLATES[$selected]}"
turnkey="${selected%-*}"
# Generate random password
# Setup script environment
PASS="$(openssl rand -base64 8)"
# Prompt for Container ID
NEXT_ID=$(pvesh get /cluster/nextid 2>/dev/null || echo 100)
# Prompt user to confirm container ID
while true; do
CTID=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Container ID" \
--inputbox "Enter the container ID..." 8 40 "$NEXT_ID" 3>&1 1>&2 2>&3) || exit_script
CTID=$(whiptail --backtitle "Container ID" --title "Choose the Container ID" --inputbox "Enter the container ID..." 8 40 $(pvesh get /cluster/nextid) 3>&1 1>&2 2>&3)
if [[ -z "$CTID" ]]; then
msg_error "No Container ID selected"
exit_script
fi
# Check if user cancelled
[ -z "$CTID" ] && die "No Container ID selected"
# Validate Container ID
if ! validate_container_id "$CTID"; then
SUGGESTED_ID=$(get_valid_container_id "$CTID")
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "ID Already In Use" --yesno \
"Container/VM ID $CTID is already in use.\n\nWould you like to use the next available ID ($SUGGESTED_ID)?" 10 58; then
if whiptail --backtitle "Container ID" --title "ID Already In Use" --yesno "Container/VM ID $CTID is already in use.\n\nWould you like to use the next available ID ($SUGGESTED_ID)?" 10 58; then
CTID="$SUGGESTED_ID"
break
fi
# User declined, loop back to input
else
break
fi
done
# Prompt for Hostname
HOST_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Hostname" \
--inputbox "Enter the container hostname..." 8 40 "turnkey-${turnkey}" 3>&1 1>&2 2>&3) || exit_script
# Container options
PCT_OPTIONS=(
-features keyctl=1,nesting=1
-hostname "$HOST_NAME"
-tags community-script
-onboot 1
-cores 2
-memory 2048
-password "$PASS"
-net0 name=eth0,bridge=vmbr0,ip=dhcp
-unprivileged 1
-arch "$(dpkg --print-architecture)"
# Prompt user to confirm Hostname
HOST_NAME=$(whiptail --backtitle "Hostname" --title "Choose the Hostname" --inputbox "Enter the containers Hostname..." 8 40 "turnkey-${turnkey}" 3>&1 1>&2 2>&3)
PCT_OPTIONS="
-features keyctl=1,nesting=1
-hostname $HOST_NAME
-tags community-script
-onboot 1
-cores 2
-memory 2048
-password $PASS
-net0 name=eth0,bridge=vmbr0,ip=dhcp
-unprivileged 1
"
DEFAULT_PCT_OPTIONS=(
-arch $(dpkg --print-architecture)
)
# Storage selection
TEMPLATE_STORAGE=$(select_storage template) || {
msg_error "Failed to select template storage"
exit 1
}
msg_ok "Using '${BL}${TEMPLATE_STORAGE}${CL}' for template storage"
# Set the CONTENT and CONTENT_LABEL variables
function select_storage() {
local CLASS=$1
local CONTENT
local CONTENT_LABEL
case $CLASS in
container)
CONTENT='rootdir'
CONTENT_LABEL='Container'
;;
template)
CONTENT='vztmpl'
CONTENT_LABEL='Container template'
;;
*) false || die "Invalid storage class." ;;
esac
CONTAINER_STORAGE=$(select_storage container) || {
msg_error "Failed to select container storage"
exit 1
}
msg_ok "Using '${BL}${CONTAINER_STORAGE}${CL}' for container storage"
# Query all storage locations
local -a MENU
while read -r line; do
local TAG=$(echo $line | awk '{print $1}')
local TYPE=$(echo $line | awk '{printf "%-10s", $2}')
local FREE=$(echo $line | numfmt --field 4-6 --from-unit=K --to=iec --format %.2f | awk '{printf( "%9sB", $6)}')
local ITEM=" Type: $TYPE Free: $FREE "
local OFFSET=2
if [[ $((${#ITEM} + $OFFSET)) -gt ${MSG_MAX_LENGTH:-} ]]; then
local MSG_MAX_LENGTH=$((${#ITEM} + $OFFSET))
fi
MENU+=("$TAG" "$ITEM" "OFF")
done < <(pvesm status -content $CONTENT | awk 'NR>1')
# Download template if not already cached
if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then
msg_info "Downloading LXC template"
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null || {
msg_error "Failed to download LXC template '${TEMPLATE}'"
exit 1
}
msg_ok "Downloaded LXC template"
# Select storage location
if [ $((${#MENU[@]} / 3)) -eq 0 ]; then
warn "'$CONTENT_LABEL' needs to be selected for at least one storage location."
die "Unable to detect valid storage location."
elif [ $((${#MENU[@]} / 3)) -eq 1 ]; then
printf ${MENU[0]}
else
local STORAGE
while [ -z "${STORAGE:+x}" ]; do
STORAGE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Storage Pools" --radiolist \
"Which storage pool would you like to use for the ${CONTENT_LABEL,,}?\n\n" \
16 $(($MSG_MAX_LENGTH + 23)) 6 \
"${MENU[@]}" 3>&1 1>&2 2>&3) || die "Menu aborted."
done
printf $STORAGE
fi
}
# Get template storage
TEMPLATE_STORAGE=$(select_storage template)
info "Using '$TEMPLATE_STORAGE' for template storage."
# Get container storage
CONTAINER_STORAGE=$(select_storage container)
info "Using '$CONTAINER_STORAGE' for container storage."
# Update LXC template list
msg "Updating LXC template list..."
pveam update >/dev/null
# Get LXC template string
mapfile -t TEMPLATES < <(pveam available -section turnkeylinux | awk -v turnkey="${turnkey}" '$0 ~ turnkey {print $2}' | sort -t - -k 2 -V)
[ ${#TEMPLATES[@]} -gt 0 ] || die "Unable to find a template when searching for '${turnkey}'."
TEMPLATE="${TEMPLATES[-1]}"
# Download LXC template
if ! pveam list $TEMPLATE_STORAGE | grep -q $TEMPLATE; then
msg "Downloading LXC template (Patience)..."
pveam download $TEMPLATE_STORAGE $TEMPLATE >/dev/null ||
die "A problem occured while downloading the LXC template."
fi
# Add rootfs if not specified
[[ " ${PCT_OPTIONS[*]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs "${CONTAINER_STORAGE}:${PCT_DISK_SIZE:-8}")
# Create variable for 'pct' options
PCT_OPTIONS=(${PCT_OPTIONS[@]:-${DEFAULT_PCT_OPTIONS[@]}})
[[ " ${PCT_OPTIONS[@]} " =~ " -rootfs " ]] || PCT_OPTIONS+=(-rootfs $CONTAINER_STORAGE:${PCT_DISK_SIZE:-8})
# Set telemetry variables for the selected turnkey
TELEMETRY_TYPE="turnkey"
NSAPP="turnkey-${turnkey}"
CT_TYPE=1
DISK_SIZE="${PCT_DISK_SIZE:-8}"
CORE_COUNT=2
RAM_SIZE=2048
var_os="turnkey"
var_version="${turnkey}"
# Create LXC
msg "Creating LXC container..."
pct create $CTID ${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE} ${PCT_OPTIONS[@]} >/dev/null ||
die "A problem occured while trying to create container."
# Report installation start to telemetry
post_to_api
# Save password
echo "TurnKey ${turnkey} password: ${PASS}" >>~/turnkey-${turnkey}.creds # file is located in the Proxmox root directory
# Create LXC container
msg_info "Creating LXC container"
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" "${PCT_OPTIONS[@]}" >/dev/null || {
msg_error "Failed to create container"
exit 1
}
msg_ok "Created LXC container (ID: ${BL}${CTID}${CL})"
# Save credentials securely
CREDS_FILE=~/turnkey-${turnkey}.creds
echo "TurnKey ${turnkey} password: ${PASS}" >>"$CREDS_FILE"
chmod 600 "$CREDS_FILE"
# Configure TUN device access for VPN-based turnkeys
TUN_DEVICE_REQUIRED=("openvpn")
# If turnkey is "OpenVPN", add access to the tun device
TUN_DEVICE_REQUIRED=("openvpn") # Setup this way in case future turnkeys also need tun access
if printf '%s\n' "${TUN_DEVICE_REQUIRED[@]}" | grep -qw "${turnkey}"; then
msg_info "Configuring TUN device access for ${turnkey}"
{
echo "lxc.cgroup2.devices.allow: c 10:200 rwm"
echo "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0"
} >>"/etc/pve/lxc/${CTID}.conf"
msg_ok "TUN device access configured"
info "${turnkey} requires access to /dev/net/tun on the host. Modifying the container configuration to allow this."
echo "lxc.cgroup2.devices.allow: c 10:200 rwm" >>/etc/pve/lxc/${CTID}.conf
echo "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0" >>/etc/pve/lxc/${CTID}.conf
sleep 5
fi
# Start container
msg_info "Starting LXC container"
msg "Starting LXC Container..."
pct start "$CTID"
msg_ok "Started LXC container"
sleep 10
# Detect container IP
msg_info "Detecting IP address"
# Get container IP
set +euo pipefail # Turn off error checking
max_attempts=5
attempt=1
IP=""
for attempt in $(seq 1 5); do
IP=$(pct exec "$CTID" -- ip -4 a show dev eth0 2>/dev/null | grep -oP 'inet \K[^/]+' || true)
if [[ -n "$IP" ]]; then
while [[ $attempt -le $max_attempts ]]; do
IP=$(pct exec $CTID ip a show dev eth0 | grep -oP 'inet \K[^/]+')
if [[ -n $IP ]]; then
break
else
warn "Attempt $attempt: IP address not found. Pausing for 5 seconds..."
sleep 5
((attempt++))
fi
[[ $attempt -lt 5 ]] && sleep 5
done
if [[ -z "$IP" ]]; then
msg_warn "IP address not found after 5 attempts"
if [[ -z $IP ]]; then
warn "Maximum number of attempts reached. IP address not found."
IP="NOT FOUND"
else
msg_ok "IP address: ${BL}${IP}${CL}"
fi
# Report success to telemetry
post_update_to_api "done" "none"
# Start Proxmox VE Monitor-All if available
if [[ -f /etc/systemd/system/ping-instances.service ]]; then
systemctl start ping-instances.service
fi
# Success summary
# Success message
header_info
echo
msg_ok "TurnKey ${BL}${turnkey}${CL} LXC container '${BL}${CTID}${CL}' was successfully created."
info "LXC container '$CTID' was successfully created, and its IP address is ${IP}."
echo
echo -e " ${TAB}${YW}IP Address:${CL} ${BL}${IP}${CL}"
echo -e " ${TAB}${YW}Login:${CL} ${GN}root${CL}"
echo -e " ${TAB}${YW}Password:${CL} ${GN}${PASS}${CL}"
info "Proceed to the LXC console to complete the setup."
echo
echo -e " ${TAB}Proceed to the LXC console to complete the TurnKey setup."
echo -e " ${TAB}Credentials stored in: ${BL}~/turnkey-${turnkey}.creds${CL}"
info "login: root"
info "password: $PASS"
info "(credentials also stored in the root user's root directory in the 'turnkey-${turnkey}.creds' file.)"
echo