Skip to main content
Every script in the Proxmox VE Helper Scripts project follows consistent patterns for maintainability and reliability. Understanding these patterns helps you customize scripts or troubleshoot issues.

Container Script Anatomy

Let’s examine a complete container script structure using real examples from the codebase.

Host Script (ct/*.sh)

The script that runs on your Proxmox VE host to create containers.
#!/usr/bin/env bash
# 1. HEADER: Shebang and metadata

# 2. SOURCE DEPENDENCIES: Load build.func
source <(curl -fsSL https://raw.githubusercontent.com/.../misc/build.func)

# 3. METADATA: Copyright, license, source
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT

# 4. APP CONFIGURATION: Define application defaults
APP="Application Name"
var_tags="${var_tags:-tag1;tag2}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"

# 5. INITIALIZATION: Setup environment
header_info "$APP"
variables
color
catch_errors

# 6. UPDATE FUNCTION: Logic for existing containers
function update_script() {
  # Update implementation
}

# 7. EXECUTION: Start build process
start
build_container
description

# 8. COMPLETION: Display success message
msg_ok "Completed successfully!\n"

Configuration Variables

Every host script defines these standard variables:
APP
string
required
Human-readable application name (e.g., “2FAuth”, “Docker”, “Pi-hole”)
var_tags
string
Semicolon-separated tags for categorization (e.g., “2fa;authenticator”)
var_cpu
number
default:"1"
CPU cores allocated to container. Uses ${var_cpu:-1} pattern for defaults.
var_ram
number
default:"1024"
RAM in MB (e.g., 512, 1024, 2048)
var_disk
number
default:"4"
Root disk size in GB
var_os
string
default:"debian"
Operating system (debian, ubuntu, alpine)
var_version
string
default:"13"
OS version (13 for Debian 13, 24.04 for Ubuntu, etc.)
var_unprivileged
number
default:"1"
Container type: 1=unprivileged (secure), 0=privileged
The ${var_name:-default} syntax means: “Use environment variable var_name if set, otherwise use default”. This enables user customization while providing sensible defaults.

Build Process Flow

When build_container is called, this happens:

Install Script (install/*-install.sh)

The script that executes inside the container to install the application.

Install Script Structure

#!/usr/bin/env bash

# 1. METADATA: Copyright and license
# 2. SOURCE DEPENDENCIES: Load install.func
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"

# 3. INITIALIZATION: Setup container environment
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os

# 4. DEPENDENCIES: Install required packages
msg_info "Installing Dependencies"
$STD apt install -y package1 package2
msg_ok "Installed Dependencies"

# 5. APPLICATION SETUP: Install main application
# - Setup databases
# - Configure services
# - Download application

# 6. SERVICE CONFIGURATION: Configure systemd/init
msg_info "Configure Service"
# Create config files
msg_ok "Configured Service"

# 7. FINALIZATION: Cleanup and prepare
motd_ssh
customize
cleanup_lxc

Key Helper Functions

Install scripts use these standard functions from install.func and core.func:
color - Sets up color codes for output
color  # Enables colored terminal output
verb_ip6 - Configures IPv6 and verbose mode
verb_ip6  # Sets up IPv6 based on user selection
catch_errors - Enables error trapping
catch_errors  # Trap errors and cleanup on failure
setting_up_container - Initial container setup
setting_up_container  # Display setup banner
network_check - Verifies internet connectivity
network_check
# Tests DNS resolution and internet access
# Exits if network unavailable
update_os - Updates package lists
update_os
# Runs apt update (Debian/Ubuntu)
# or apk update (Alpine)
setup_php - Installs and configures PHP
export PHP_VERSION="8.4"
PHP_FPM="YES" setup_php
# Installs PHP 8.4 with FPM support
setup_mariadb - Installs MariaDB server
setup_mariadb
# Installs and secures MariaDB
setup_mariadb_db - Creates database and user
MARIADB_DB_NAME="myapp_db" MARIADB_DB_USER="myapp" setup_mariadb_db
# Creates database, user, and random password
# Password stored in $MARIADB_DB_PASS
setup_composer - Installs Composer
setup_composer
# Installs latest Composer globally
fetch_and_deploy_gh_release - Downloads GitHub releases
fetch_and_deploy_gh_release "destination" "owner/repo" "type"
# Types: tarball, zipball, binary
# Extracts to /opt/destination
get_latest_github_release - Gets latest release tag
VERSION=$(get_latest_github_release "moby/moby")
# Returns: "v27.0.3" (example)
motd_ssh - Configures message of the day
motd_ssh
# Sets up /etc/motd with container info
# Configures SSH if enabled
customize - Applies user customizations
customize
# Runs user-defined customization hooks
cleanup_lxc - Final cleanup
cleanup_lxc
# Clears package cache
# Removes temporary files

Update Script Pattern

Every host script includes an update_script() function for updating existing containers:

Update Script Structure

function update_script() {
  # 1. HEADER
  header_info
  check_container_storage
  check_container_resources
  
  # 2. VALIDATION: Check if app is installed
  if [[ ! -d "/opt/myapp" ]]; then
    msg_error "No ${APP} Installation Found!"
    exit
  fi
  
  # 3. VERSION CHECK: Only update if new version available
  if check_for_gh_release "myapp" "owner/repo"; then
    
    # 4. SYSTEM UPDATE
    $STD apt update
    $STD apt -y upgrade
    
    # 5. BACKUP: Create backup before updating
    msg_info "Creating Backup"
    mv "/opt/myapp" "/opt/myapp-backup"
    msg_ok "Backup Created"
    
    # 6. UPDATE APPLICATION
    fetch_and_deploy_gh_release "myapp" "owner/repo" "tarball"
    
    # 7. RESTORE DATA
    mv "/opt/myapp-backup/config" "/opt/myapp/config"
    mv "/opt/myapp-backup/data" "/opt/myapp/data"
    
    # 8. RESTART SERVICES
    $STD systemctl restart myapp
    
    msg_ok "Updated successfully!"
  fi
  exit
}

Real Update Examples

function update_script() {
  header_info
  check_container_storage
  check_container_resources

  if [[ ! -d "/opt/2fauth" ]]; then
    msg_error "No ${APP} Installation Found!"
    exit
  fi
  
  setup_mariadb  # Ensure DB helpers available
  
  if check_for_gh_release "2fauth" "Bubka/2FAuth"; then
    $STD apt update
    $STD apt -y upgrade

    msg_info "Creating Backup"
    mv "/opt/2fauth" "/opt/2fauth-backup"
    msg_ok "Backup Created"

    # Upgrade PHP if needed
    if ! dpkg -l | grep -q 'php8.4'; then
      PHP_VERSION="8.4" PHP_FPM="YES" setup_php
      sed -i 's/php8\.[0-9]/php8.4/g' /etc/nginx/conf.d/2fauth.conf
    fi
    
    fetch_and_deploy_gh_release "2fauth" "Bubka/2FAuth" "tarball"
    setup_composer
    
    # Restore data
    mv "/opt/2fauth-backup/.env" "/opt/2fauth/.env"
    mv "/opt/2fauth-backup/storage" "/opt/2fauth/storage"
    
    cd "/opt/2fauth" || return
    chown -R www-data: "/opt/2fauth"
    export COMPOSER_ALLOW_SUPERUSER=1
    $STD composer install --no-dev --prefer-dist
    php artisan 2fauth:install
    
    $STD systemctl restart nginx
    msg_ok "Updated successfully!"
  fi
  exit
}

Common Patterns

The $STD Variable

The $STD variable controls command output visibility:
# When VERBOSE=no (default)
$STD apt install nginx
# Output: Hidden (redirected to /dev/null)

# When VERBOSE=yes
$STD apt install nginx
# Output: Visible (shows all apt output)

# Implementation (from core.func)
if [[ "$VERBOSE" == "yes" ]]; then
  STD=""  # Show output
else
  STD="&>/dev/null"  # Hide output
fi
Use $STD prefix for all commands that produce verbose output to respect user’s verbosity setting.

Error Handling Pattern

# Check if directory exists before proceeding
if [[ ! -d "/opt/myapp" ]]; then
  msg_error "No ${APP} Installation Found!"
  exit
fi

# Check if service is running
if ! systemctl is-active --quiet myapp; then
  msg_warn "Service is not running, starting it..."
  systemctl start myapp
fi

# Validate command success
if ! command -v docker &>/dev/null; then
  msg_error "Docker is not installed!"
  exit 1
fi

Message Formatting

Standard message functions provide consistent UI:
msg_info "Installing Dependencies"  # Blue info message
$STD apt install -y nginx
msg_ok "Installed Dependencies"     # Green success message

msg_warn "Configuration file missing, using defaults"  # Yellow warning

msg_error "Critical error occurred!"  # Red error message

Configuration File Creation

Use heredoc for multi-line config files:
msg_info "Configure Service"
cat <<EOF >/etc/nginx/conf.d/myapp.conf
server {
    listen 80;
    server_name $LOCAL_IP;
    root /opt/myapp/public;
    index index.php index.html;
    
    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }
    
    location ~ \.php\$ {
        fastcgi_pass unix:/var/run/php/php${PHP_VERSION}-fpm.sock;
        include fastcgi_params;
    }
}
EOF
systemctl reload nginx
msg_ok "Configured Service"
Remember to escape $ as \$ inside heredocs when you want literal dollar signs in the config file.

File Naming Conventions

TypePatternExample
Container Scriptct/<app>.shct/2fauth.sh, ct/docker.sh
Install Scriptinstall/<app>-install.shinstall/2fauth-install.sh
VM Scriptvm/<os>-vm.shvm/haos-vm.sh, vm/opnsense-vm.sh
Function Librarymisc/<name>.funcmisc/build.func, misc/core.func
App Name (NSAPP)Lowercase, no spaces2fauth, docker, uptimekuma

Variable Naming Conventions

# User-configurable variables (prefixed with var_)
var_cpu=2
var_ram=1024
var_disk=10
var_hostname="myapp"
var_unprivileged=1

# Internal variables (UPPERCASE)
APP="MyApp"
NSAPP="myapp"  # Normalized app name
CT_ID=100
CORE_COUNT=2
RAM_SIZE=1024
DISK_SIZE="10"

# Function-local variables (lowercase)
local file="/path/to/file"
local version="1.2.3"

Best Practices

#!/usr/bin/env bash
source <(curl -fsSL .../misc/build.func)  # First line after shebang

# Then define variables
APP="MyApp"
var_cpu="${var_cpu:-2}"
# ✅ GOOD: Provides fallback
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"

# ❌ BAD: No fallback if not set
var_cpu="$var_cpu"
function update_script() {
  # ✅ GOOD: Check installation exists
  if [[ ! -d "/opt/myapp" ]]; then
    msg_error "No ${APP} Installation Found!"
    exit
  fi
  # ... update logic
}
msg_info "Creating Backup"
mv "/opt/myapp" "/opt/myapp-backup"
cp "/etc/myapp/config.yml" "/etc/myapp/config.yml.bak"
msg_ok "Backup Created"

# ... perform update

# Restore data
mv "/opt/myapp-backup/data" "/opt/myapp/data"
# Set ownership to web server user
chown -R www-data:www-data /opt/myapp

# Set appropriate permissions
chmod -R 755 /opt/myapp

# Protect sensitive files
chmod 600 /opt/myapp/.env

Debugging Tips

Enable Verbose Mode

# Set before running script
export var_verbose=yes
bash -c "$(wget -qLO - https://github.com/.../ct/myapp.sh)"

# Or in advanced settings menu, select verbose option

Check Build Logs

# On Proxmox host
tail -f /tmp/create-lxc-*.log

# Find all build logs
ls -lht /tmp/create-lxc-*.log | head

Check Install Logs Inside Container

# Enter container
pct enter <CTID>

# Check system logs
journalctl -xe

# Check service status
systemctl status myapp

Common Issues

# Inside container, test connectivity
ping -c 3 8.8.8.8
ping -c 3 google.com

# Check DNS
cat /etc/resolv.conf
# Check if unprivileged container can access files
ls -la /opt/myapp

# Fix ownership
chown -R www-data:www-data /opt/myapp
# Check service status
systemctl status myapp

# View logs
journalctl -u myapp -n 50

# Check config syntax
nginx -t  # For nginx
php -m    # For PHP modules

Next Steps

Architecture

Learn about the overall system architecture

Containers vs VMs

Understand when to use containers or VMs

Build docs developers (and LLMs) love