-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
[Script]: AppName.sh
Modern Guide to Creating LXC Container Installation Scripts
Updated: December 2025
Context: Fully integrated with build.func, advanced_settings wizard, and defaults system
Example Used:/ct/pihole.sh,/ct/docker.sh
- Overview
- Architecture & Flow
- File Structure
- Complete Script Template
- Function Reference
- Advanced Features
- Real Examples
- Troubleshooting
- Contribution Checklist
Container scripts (ct/AppName.sh) are entry points for creating LXC containers with specific applications pre-installed. They:
- Define container defaults (CPU, RAM, disk, OS)
- Call the main build orchestrator (
build.func) - Implement application-specific update mechanisms
- Provide user-facing success messages
Proxmox Host
↓
ct/AppName.sh sourced (runs as root on host)
↓
build.func: Creates LXC container + runs install script inside
↓
install/AppName-install.sh (runs inside container)
↓
Container ready with app installed
- build.func - Main orchestrator (container creation, storage, variable management)
- install.func - Container-specific setup (OS update, package management)
- tools.func - Tool installation helpers (repositories, GitHub releases)
- core.func - UI/messaging functions (colors, spinners, validation)
- error_handler.func - Error handling and signal management
START: bash ct/pihole.sh
↓
[1] Set APP, var_*, defaults
↓
[2] header_info() → Display ASCII art
↓
[3] variables() → Parse arguments & load build.func
↓
[4] color() → Setup ANSI codes
↓
[5] catch_errors() → Setup trap handlers
↓
[6] install_script() → Show mode menu (5 options)
↓
├─ INSTALL_MODE="0" (Default)
├─ INSTALL_MODE="1" (Advanced - 19-step wizard)
├─ INSTALL_MODE="2" (User Defaults)
├─ INSTALL_MODE="3" (App Defaults)
└─ INSTALL_MODE="4" (Settings Menu)
↓
[7] advanced_settings() → Collect user configuration (if mode=1)
↓
[8] start() → Confirm or re-edit settings
↓
[9] build_container() → Create LXC + execute install script
↓
[10] description() → Set container description
↓
[11] SUCCESS → Display access URL
↓
END
Priority 1 (Highest): Environment Variables (var_cpu, var_ram, etc.)
Priority 2: App-Specific Defaults (/defaults/AppName.vars)
Priority 3: User Global Defaults (/default.vars)
Priority 4 (Lowest): Built-in Defaults (in build.func)
#!/usr/bin/env bash # [1] Shebang
# [2] Copyright/License
source <(curl -s .../misc/build.func) # [3] Import functions
# [4] APP metadata
APP="AppName" # [5] Default values
var_tags="tag1;tag2"
var_cpu="2"
var_ram="2048"
...
header_info "$APP" # [6] Display header
variables # [7] Process arguments
color # [8] Setup colors
catch_errors # [9] Setup error handling
function update_script() { ... } # [10] Update function (optional)
start # [11] Launch container creation
build_container
description
msg_ok "Completed Successfully!\n"
#!/usr/bin/env bash
# Copyright (c) 2021-2025 community-scripts ORG
# Author: YourUsername
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://github.com/example/project
# Import main orchestrator
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
⚠️ IMPORTANT: Before opening a PR, change URL tocommunity-scriptsrepo!
# Application Configuration
APP="ApplicationName"
var_tags="tag1;tag2;tag3" # Max 3-4 tags, no spaces, semicolon-separated
# Container Resources
var_cpu="2" # CPU cores
var_ram="2048" # RAM in MB
var_disk="10" # Disk in GB
# Container Type & OS
var_os="debian" # Options: alpine, debian, ubuntu
var_version="12" # Alpine: 3.20+, Debian: 11-13, Ubuntu: 20.04+
var_unprivileged="1" # 1=unprivileged (secure), 0=privileged (rarely needed)Variable Naming Convention:
- Variables exposed to user:
var_*(e.g.,var_cpu,var_hostname,var_ssh) - Internal variables: lowercase (e.g.,
container_id,app_version)
# Display header ASCII art
header_info "$APP"
# Process command-line arguments and load configuration
variables
# Setup ANSI color codes and formatting
color
# Initialize error handling (trap ERR, EXIT, INT, TERM)
catch_errorsfunction update_script() {
header_info
# Always start with these checks
check_container_storage
check_container_resources
# Verify app is installed
if [[ ! -d /opt/appname ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
# Get latest version from GitHub
RELEASE=$(curl -fsSL https://api.github.com/repos/user/repo/releases/latest | \
grep "tag_name" | awk '{print substr($2, 2, length($2)-3)}')
# Compare with saved version
if [[ ! -f /opt/${APP}_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]]; then
msg_info "Updating ${APP} to v${RELEASE}"
# Backup user data
cp -r /opt/appname /opt/appname-backup
# Perform update
cd /opt
wget -q "https://github.com/user/repo/releases/download/v${RELEASE}/app-${RELEASE}.tar.gz"
tar -xzf app-${RELEASE}.tar.gz
# Restore user data
cp /opt/appname-backup/config/* /opt/appname/config/
# Cleanup
rm -rf app-${RELEASE}.tar.gz /opt/appname-backup
# Save new version
echo "${RELEASE}" > /opt/${APP}_version.txt
msg_ok "Updated ${APP} to v${RELEASE}"
else
msg_ok "No update required. ${APP} is already at v${RELEASE}."
fi
exit
}# Start the container creation workflow
start
# Build the container with selected configuration
build_container
# Set container description/notes in Proxmox UI
description
# Display success message
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}:8080${CL}"Purpose: Initialize container variables, load user arguments, setup orchestration
Triggered by: Called automatically at script start
Behavior:
- Parse command-line arguments (if any)
- Generate random UUID for session tracking
- Load container storage from Proxmox
- Initialize application-specific defaults
- Setup SSH/environment configuration
Example Output:
Setting up variables...
Container ID: 100
Storage: local-lvm
Install method: Default
Purpose: Launch the container creation menu with 5 installation modes
Triggered by: Called just before build_container()
Menu Options:
1. Default Installation (Quick setup, predefined settings)
2. Advanced Installation (19-step wizard with full control)
3. User Defaults (Load ~/.community-scripts/default.vars)
4. App Defaults (Load /defaults/AppName.vars)
5. Settings Menu (Interactive mode selection)
User Flow:
Select installation mode:
1) Default
2) Advanced
3) User Defaults
4) App Defaults
5) Settings Menu
Enter choice: 2
Purpose: Main orchestrator for LXC container creation
Operations:
- Validates all variables
- Creates LXC container via
pct create - Executes
install/AppName-install.shinside container - Monitors installation progress
- Handles errors and rollback on failure
Exit Codes:
-
0- Success -
1-255- Various error conditions (see error_handler.func)
Purpose: Set container description/notes visible in Proxmox UI
Format:
AppName
IP: [IP]
Version: [Version]
Tags: [Tags]
Purpose: Display ASCII art header for application
Sources:
- Tries
/usr/local/community-scripts/headers/ct/appname(cached) - Falls back to remote fetch from GitHub
- Returns silently if not found
# At end of install script, after successful setup:
maybe_offer_save_app_defaults
# Output:
# "Save these settings as App Defaults for AppName? (Y/n)"
# Yes → Saves to /defaults/appname.vars
# No → Skips saving# In ct/AppName.sh, user selects "App Defaults" mode
# Automatically loads /defaults/appname.vars
# Container uses previously saved configurationIf your app has additional setup beyond standard vars:
# In ct/AppName.sh, after variables()
custom_app_settings() {
CONFIGURE_DB=$(whiptail --title "Database Setup" \
--yesno "Would you like to configure a custom database?" 8 60)
if [[ $? -eq 0 ]]; then
DB_HOST=$(whiptail --inputbox "Database Host:" 8 60 3>&1 1>&2 2>&3)
DB_PORT=$(whiptail --inputbox "Database Port:" 8 60 "3306" 3>&1 1>&2 2>&3)
fi
}
custom_app_settingsSave installed version for update checks:
# In install script, after successful app download:
RELEASE="1.2.3"
echo "${RELEASE}" > /opt/${APP}_version.txt
# In update function, compare:
CURRENT=$(cat /opt/${APP}_version.txt 2>/dev/null)
LATEST=$(curl -fsSL https://api.github.com/repos/user/repo/releases/latest | jq -r '.tag_name')
if [[ "$LATEST" != "$CURRENT" ]]; then
echo "Update available: $CURRENT → $LATEST"
fiAdd custom validation:
function health_check() {
header_info
if [[ ! -d /opt/appname ]]; then
msg_error "Application not found!"
exit 1
fi
if ! systemctl is-active --quiet appname; then
msg_error "Application service not running"
exit 1
fi
msg_ok "Health check passed"
}
# Called via: bash ct/appname.sh health_check#!/usr/bin/env bash
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
APP="Homarr"
var_tags="dashboard;homepage"
var_cpu="2"
var_ram="1024"
var_disk="5"
var_os="debian"
var_version="12"
var_unprivileged="1"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/homarr ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
RELEASE=$(curl -fsSL https://api.github.com/repos/ajnart/homarr/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4)}')
if [[ ! -f /opt/${APP}_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ]]; then
msg_info "Updating ${APP} to v${RELEASE}"
systemctl stop homarr
cd /opt/homarr
wget -q "https://github.com/ajnart/homarr/releases/download/v${RELEASE}/docker-compose.yml"
docker-compose up -d
echo "${RELEASE}" > /opt/${APP}_version.txt
msg_ok "Updated ${APP} to v${RELEASE}"
else
msg_ok "No update required. ${APP} is already at v${RELEASE}."
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}:5100${CL}"#!/usr/bin/env bash
source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func)
APP="PostgreSQL"
var_tags="database;sql"
var_cpu="4"
var_ram="4096"
var_disk="20"
var_os="alpine"
var_version="3.20"
var_unprivileged="1"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
if ! command -v psql &>/dev/null; then
msg_error "PostgreSQL not installed!"
exit
fi
msg_info "Updating Alpine packages"
apk update
apk upgrade
msg_ok "Updated Alpine packages"
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} Connect using:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}psql -h ${IP} -U postgres${CL}"Symptom: pct create exits with error code 209
Causes:
- CTID already exists:
pct listshows duplicate - Storage full: Check storage space
- Network template unavailable
Solution:
# Check existing containers
pct list | grep CTID
# Remove conflicting container
pct destroy CTID
# Retry ct/AppName.shSymptom: Update available but script says "already at latest"
Causes:
- Version file missing:
/opt/AppName_version.txt - GitHub API rate limit exceeded
- Release tag format mismatch
Debug:
# Check version file
cat /opt/AppName_version.txt
# Test GitHub API
curl -fsSL https://api.github.com/repos/user/repo/releases/latest | grep tag_name
# Inside container
bash ct/appname.sh update_scriptSymptom: Container script runs but no header shown
Causes:
- Header file not in repository
- Caching issue
Solution:
# Create header file manually
mkdir -p /usr/local/community-scripts/headers/ct
echo "Your ASCII art here" > /usr/local/community-scripts/headers/ct/appname
# Or remove cache to force re-download
rm -f /usr/local/community-scripts/headers/ct/appnameBefore submitting a PR:
- Shebang is
#!/usr/bin/env bash - Imports
build.funcfrom community-scripts repo (not personal fork) - Copyright header with author and source URL
- APP variable matches filename
-
var_tagsare semicolon-separated (no spaces)
-
var_cpuset appropriately (2-4 for most apps) -
var_ramset appropriately (1024-4096 MB minimum) -
var_disksufficient for app + data (5-20 GB) -
var_osis realistic (Alpine if lightweight, Debian/Ubuntu otherwise) -
var_unprivileged="1"unless app absolutely needs privileges
-
update_script()implemented (or marked as unavailable) - Update function checks if app installed
- Update function checks for new version
- Update function performs cleanup after update
- Proper error handling with
msg_erroron failure
- Success message displayed with access URL
- URL format:
http://IP:PORT/path(if web-based) - Uses
msg_ok,msg_info,msg_errorfor feedback
- Script tested with default installation
- Script tested with advanced (19-step) installation
- Update function tested on existing installation
- Error handling tested (invalid settings, network issues)
-
Use meaningful defaults
var_cpu="2" # ✅ Good: Typical workload var_cpu="128" # ❌ Bad: Unrealistic
-
Implement version tracking
echo "${RELEASE}" > /opt/${APP}_version.txt # ✅ Good # ❌ Bad: No version tracking
-
Handle edge cases
if [[ ! -f /opt/${APP}_version.txt ]]; then msg_info "First installation detected" fi
-
Use proper messaging
msg_info "Updating..." # ✅ Good: Clear status echo "Updating..." # ❌ Bad: No formatting
-
Hardcode versions
RELEASE="1.2.3" # ❌ Bad: Won't auto-update
-
Use custom color codes
echo -e "\033[32mSuccess" # ❌ Bad: Use $GN instead
-
Forget error handling
wget file.zip # ❌ Bad: No error check if ! wget -q file.zip; then # ✅ Good msg_error "Download failed" fi
-
Leave temporary files
rm -rf /opt/file.zip # ✅ Always cleanup
Last Updated: December 2025
Compatibility: ProxmoxVED with build.func v3+
Questions? Open an issue in the repository