Skip to content

[Script]: AppName.sh

CanbiZ edited this page Dec 1, 2025 · 1 revision

🚀 Application Container Scripts (ct/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


📋 Table of Contents


Overview

Purpose

Container scripts (ct/AppName.sh) are entry points for creating LXC containers with specific applications pre-installed. They:

  1. Define container defaults (CPU, RAM, disk, OS)
  2. Call the main build orchestrator (build.func)
  3. Implement application-specific update mechanisms
  4. Provide user-facing success messages

Execution Context

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

Key Integration Points

  • 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

Architecture & Flow

Container Creation Flow

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

Default Values Precedence

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)

File Structure

Minimal ct/AppName.sh Template

#!/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"

Complete Script Template

1. File Header & Imports

#!/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 to community-scripts repo!

2. Application Metadata

# 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)

3. Display & Initialization

# 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_errors

4. Update Function (Highly Recommended)

function 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
}

5. Script Launch

# 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}"

Function Reference

Core Functions (From build.func)

variables()

Purpose: Initialize container variables, load user arguments, setup orchestration

Triggered by: Called automatically at script start

Behavior:

  1. Parse command-line arguments (if any)
  2. Generate random UUID for session tracking
  3. Load container storage from Proxmox
  4. Initialize application-specific defaults
  5. Setup SSH/environment configuration

Example Output:

Setting up variables...
Container ID: 100
Storage: local-lvm
Install method: Default

start()

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

build_container()

Purpose: Main orchestrator for LXC container creation

Operations:

  1. Validates all variables
  2. Creates LXC container via pct create
  3. Executes install/AppName-install.sh inside container
  4. Monitors installation progress
  5. Handles errors and rollback on failure

Exit Codes:

  • 0 - Success
  • 1-255 - Various error conditions (see error_handler.func)

description()

Purpose: Set container description/notes visible in Proxmox UI

Format:

AppName
IP: [IP]
Version: [Version]
Tags: [Tags]

header_info()

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

Advanced Features

1. Integration with Defaults System

Save App Defaults After Installation

# 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

Load Saved Defaults During Container Creation

# In ct/AppName.sh, user selects "App Defaults" mode
# Automatically loads /defaults/appname.vars
# Container uses previously saved configuration

2. Custom Configuration Menus

If 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_settings

3. Version Tracking

Save 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"
fi

4. Health Check Functions

Add 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

Real Examples

Example 1: Simple Web App (Debian-based)

#!/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}"

Example 2: Database App (Alpine-based)

#!/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}"

Troubleshooting

Container Creation Fails

Symptom: pct create exits with error code 209

Causes:

  1. CTID already exists: pct list shows duplicate
  2. Storage full: Check storage space
  3. Network template unavailable

Solution:

# Check existing containers
pct list | grep CTID

# Remove conflicting container
pct destroy CTID

# Retry ct/AppName.sh

Update Function Doesn't Detect New Version

Symptom: Update available but script says "already at latest"

Causes:

  1. Version file missing: /opt/AppName_version.txt
  2. GitHub API rate limit exceeded
  3. 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_script

Header ASCII Art Not Displaying

Symptom: Container script runs but no header shown

Causes:

  1. Header file not in repository
  2. 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/appname

Contribution Checklist

Before submitting a PR:

Script Structure

  • Shebang is #!/usr/bin/env bash
  • Imports build.func from community-scripts repo (not personal fork)
  • Copyright header with author and source URL
  • APP variable matches filename
  • var_tags are semicolon-separated (no spaces)

Default Values

  • var_cpu set appropriately (2-4 for most apps)
  • var_ram set appropriately (1024-4096 MB minimum)
  • var_disk sufficient for app + data (5-20 GB)
  • var_os is realistic (Alpine if lightweight, Debian/Ubuntu otherwise)
  • var_unprivileged="1" unless app absolutely needs privileges

Functions

  • 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_error on failure

Output

  • Success message displayed with access URL
  • URL format: http://IP:PORT/path (if web-based)
  • Uses msg_ok, msg_info, msg_error for feedback

Testing

  • 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)

Best Practices

✅ DO:

  1. Use meaningful defaults

    var_cpu="2"        # ✅ Good: Typical workload
    var_cpu="128"      # ❌ Bad: Unrealistic
  2. Implement version tracking

    echo "${RELEASE}" > /opt/${APP}_version.txt  # ✅ Good
    # ❌ Bad: No version tracking
  3. Handle edge cases

    if [[ ! -f /opt/${APP}_version.txt ]]; then
      msg_info "First installation detected"
    fi
  4. Use proper messaging

    msg_info "Updating..."  # ✅ Good: Clear status
    echo "Updating..."      # ❌ Bad: No formatting

❌ DON'T:

  1. Hardcode versions

    RELEASE="1.2.3"    # ❌ Bad: Won't auto-update
  2. Use custom color codes

    echo -e "\033[32mSuccess"  # ❌ Bad: Use $GN instead
  3. Forget error handling

    wget file.zip      # ❌ Bad: No error check
    if ! wget -q file.zip; then  # ✅ Good
      msg_error "Download failed"
    fi
  4. Leave temporary files

    rm -rf /opt/file.zip     # ✅ Always cleanup

Related Documentation


Last Updated: December 2025
Compatibility: ProxmoxVED with build.func v3+
Questions? Open an issue in the repository

Clone this wiki locally