Installation scripts (install/AppName-install.sh) run inside LXC containers and are responsible for setting up the OS, installing dependencies, configuring applications, and creating systemd services.
Overview
Purpose
Installation scripts handle:
Setting up the container OS (updates, packages)
Installing application dependencies
Downloading and configuring the application
Setting up services and systemd units
Creating version tracking files for updates
Generating credentials/configurations
Final cleanup and validation
Execution Flow
ct/AppName.sh (Proxmox Host)
↓
build_container()
↓
pct exec CTID bash -c "$(cat install/AppName-install.sh)"
↓
install/AppName-install.sh (Inside Container)
↓
Container Ready with App Installed
Environment Variables
The following variables are automatically available inside install scripts:
Variable Description Example CTIDContainer ID 100, 101PCT_OSTYPEOS type alpine, debian, ubuntuHOSTNAMEContainer hostname piholeFUNCTIONS_FILE_PATHBash functions library (core.func + tools.func) VERBOSEVerbose mode yes/noSTDStandard redirection (silent/empty) APPApplication name PiholeNSAPPNormalized app name piholeMETHODInstallation method ct/installRANDOM_UUIDSession UUID (for telemetry)
Complete Script Template
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: YourUsername
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/application/repo
# Load all available functions (from core.func + tools.func)
source /dev/stdin <<< " $FUNCTIONS_FILE_PATH "
# Initialize environment
color # Setup ANSI colors and icons
verb_ip6 # Configure IPv6 (if needed)
catch_errors # Setup error traps
setting_up_container # Verify OS is ready
network_check # Verify internet connectivity
update_os # Update packages (apk/apt)
Phase 2: Dependency Installation
msg_info "Installing Dependencies"
$STD apt-get install -y \
curl \
wget \
git \
nano \
build-essential \
libssl-dev \
python3-dev
msg_ok "Installed Dependencies"
Always use $STD before commands to respect the verbose mode setting. This silences output when verbose mode is disabled.
Use the helper functions from tools.func for common tool installations:
# Setup Node.js
NODE_VERSION = "22" setup_nodejs
# Setup PHP with extensions
PHP_VERSION = "8.4" PHP_MODULE = "bcmath,curl,gd,intl,redis" setup_php
# Setup Python
PYTHON_VERSION = "3.12" setup_uv
# Setup databases
setup_mariadb # Uses distribution packages (recommended)
setup_postgresql # PostgreSQL
# Other tools
setup_docker # Docker Engine
setup_composer # PHP Composer
setup_ruby # Ruby
setup_rust # Rust
Phase 4: Application Download
GitHub Release
Git Clone
Direct Download
# Download from GitHub releases
RELEASE = $( curl -fsSL https://api.github.com/repos/user/repo/releases/latest | \
grep "tag_name" | awk '{print substr($2, 2, length($2)-3)}' )
msg_info "Installing Application v${ RELEASE }"
cd /opt
wget -q "https://github.com/user/repo/releases/download/v${ RELEASE }/app-${ RELEASE }.tar.gz"
tar -xzf app- ${ RELEASE } .tar.gz
rm -f app- ${ RELEASE } .tar.gz
msg_ok "Installed Application v${ RELEASE }"
Phase 5: Configuration Files
# Create configuration file
cat << 'EOF' > /etc/nginx/sites-available/appname
server {
listen 80;
server_name _;
root /opt/appname/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
}
}
EOF
# Modify existing configuration
sed -i -e "s|^DB_HOST=.*|DB_HOST=localhost|" \
-e "s|^DB_USER=.*|DB_USER=appuser|" \
-e "s|^DB_PASS=.*|DB_PASS=${ DB_PASS }|" \
/opt/appname/.env
Phase 6: Database Setup
Always generate random passwords for database users. Never use hardcoded passwords.
msg_info "Setting up Database"
DB_NAME = "appname_db"
DB_USER = "appuser"
DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13 )
# For MySQL/MariaDB
mysql -u root << EOF
CREATE DATABASE ${ DB_NAME };
CREATE USER '${ DB_USER }'@'localhost' IDENTIFIED BY '${ DB_PASS }';
GRANT ALL PRIVILEGES ON ${ DB_NAME }.* TO '${ DB_USER }'@'localhost';
FLUSH PRIVILEGES;
EOF
# Save credentials to config
cat > /opt/appname/.env << EOF
DB_HOST=localhost
DB_NAME=${ DB_NAME }
DB_USER=${ DB_USER }
DB_PASS=${ DB_PASS }
EOF
msg_ok "Database setup complete"
Phase 7: Permissions
msg_info "Setting permissions"
# Web applications typically run as www-data
chown -R www-data:www-data /opt/appname
chmod -R 755 /opt/appname
chmod 600 /opt/appname/.env # Protect sensitive files
msg_ok "Permissions set"
Phase 8: Service Configuration
Systemd (Debian/Ubuntu)
OpenRC (Alpine)
# Create systemd service
cat << EOF > /etc/systemd/system/appname.service
[Unit]
Description=Application Name
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/appname
ExecStart=/usr/bin/node /opt/appname/server.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Enable and start service
systemctl daemon-reload
systemctl enable -q --now appname
# Verify service is running
if systemctl is-active --quiet appname ; then
msg_ok "Service running successfully"
else
msg_error "Service failed to start"
journalctl -u appname -n 20
exit 1
fi
Phase 9: Version Tracking
Version tracking is essential for the update_script() function in the container script to detect when updates are available.
# Essential for update detection
echo "${ RELEASE }" > /opt/ ${ APP } _version.txt
# Or with additional metadata
cat > /opt/ ${ APP } _version.txt << EOF
Version: ${ RELEASE }
InstallDate: $( date )
InstallMethod: ${ METHOD }
EOF
Phase 10: Final Setup & Cleanup
# Display MOTD and enable autologin
motd_ssh
# Final customization
customize
# Clean up package manager cache
msg_info "Cleaning up"
apt-get -y autoremove
apt-get -y autoclean
msg_ok "Cleaned"
# System cleanup (removes logs, cache, etc.)
cleanup_lxc
Real-World Examples
Pi-hole
Docker
Node.js Application
install/pihole-install.sh
#!/usr/bin/env bash
# Copyright (c) 2021-2026 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://pi-hole.net/
source /dev/stdin <<< " $FUNCTIONS_FILE_PATH "
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_warn "WARNING: This script will run an external installer from a third-party source (https://pi-hole.net/)."
msg_warn "The following code is NOT maintained or audited by our repository."
msg_warn "If you have any doubts or concerns, please review the installer code before proceeding:"
msg_custom "${ TAB3 }${ GATEWAY }${ BGN }${ CL }" "\e[1;34m" "→ https://install.pi-hole.net"
echo
read -r -p "${ TAB3 }Do you want to continue? [y/N]: " CONFIRM
if [[ ! " $CONFIRM " =~ ^([ yY ][ eE ][ sS ] | [ yY ])$ ]]; then
msg_error "Aborted by user. No changes have been made."
exit 10
fi
msg_info "Installing Dependencies"
$STD apt install -y ufw
msg_ok "Installed Dependencies"
msg_info "Installing Pi-hole"
mkdir -p /etc/pihole
touch /etc/pihole/pihole.toml
$STD bash <( curl -fsSL https://install.pi-hole.net) --unattended
# Configure Pi-hole settings
sed -i -E '
/^\s*upstreams =/ s|=.*|= ["8.8.8.8", "8.8.4.4"]|
/^\s*interface =/ s|=.*|= "eth0"|
/^\s*queryLogging =/ s|=.*|= true|
/^\s*listeningMode =/ s|=.*|= "LOCAL"|
/^\s*pwhash =/ s|=.*|= ""|
' /etc/pihole/pihole.toml
systemctl restart pihole-FTL.service
msg_ok "Installed Pi-hole"
motd_ssh
customize
cleanup_lxc
install/docker-install.sh
#!/usr/bin/env bash
# Copyright (c) 2021-2026 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.docker.com/
source /dev/stdin <<< " $FUNCTIONS_FILE_PATH "
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
DOCKER_LATEST_VERSION = $( get_latest_github_release "moby/moby" )
PORTAINER_LATEST_VERSION = $( get_latest_github_release "portainer/portainer" )
msg_info "Installing Docker $DOCKER_LATEST_VERSION (with Compose, Buildx)"
DOCKER_CONFIG_PATH = '/etc/docker/daemon.json'
mkdir -p $( dirname $DOCKER_CONFIG_PATH )
echo -e '{\n "log-driver": "journald"\n}' > /etc/docker/daemon.json
$STD sh <( curl -fsSL https://get.docker.com)
msg_ok "Installed Docker $DOCKER_LATEST_VERSION "
read -r -p "${ TAB3 }Would you like to add Portainer (UI)? <y/N> " prompt
if [[ ${ prompt ,,} =~ ^( y | yes )$ ]]; then
msg_info "Installing Portainer $PORTAINER_LATEST_VERSION "
docker volume create portainer_data > /dev/null
$STD docker run -d \
-p 8000:8000 \
-p 9443:9443 \
--name=portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest
msg_ok "Installed Portainer $PORTAINER_LATEST_VERSION "
fi
motd_ssh
customize
cleanup_lxc
install/nodeapp-install.sh
#!/usr/bin/env bash
source /dev/stdin <<< " $FUNCTIONS_FILE_PATH "
color
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Node.js"
NODE_VERSION = "22" setup_nodejs
msg_ok "Node.js installed"
msg_info "Installing Application"
cd /opt
RELEASE = $( curl -fsSL https://api.github.com/repos/user/repo/releases/latest | \
grep "tag_name" | awk '{print substr($2, 2, length($2)-3)}' )
wget -q "https://github.com/user/repo/releases/download/v${ RELEASE }/app.tar.gz"
tar -xzf app.tar.gz
rm -f app.tar.gz
cd app
npm install --production
echo "${ RELEASE }" > /opt/app_version.txt
msg_ok "Application installed"
msg_info "Creating Service"
cat << EOF > /etc/systemd/system/app.service
[Unit]
Description=Node.js Application
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/app
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now app
msg_ok "Service created"
cleanup_lxc
Core Messaging Functions
msg_info(message)
Displays an info message with spinner animation:
msg_info "Installing application"
# Output: ⏳ Installing application (with spinning animation)
msg_ok(message)
Displays success message with checkmark:
msg_ok "Installation completed"
# Output: ✔️ Installation completed
msg_error(message)
Displays error message and exits:
msg_error "Installation failed"
# Output: ✖️ Installation failed
msg_warn(message)
Displays warning message:
msg_warn "This will download from a third-party source"
# Output: ⚠️ This will download from a third-party source
setup_nodejs()
Installs Node.js with optional global modules:
NODE_VERSION = "22" setup_nodejs
NODE_VERSION = "22" NODE_MODULE = "yarn,@vue/cli" setup_nodejs
setup_php()
Installs PHP with optional extensions:
PHP_VERSION = "8.4" setup_php
PHP_VERSION = "8.4" PHP_MODULE = "bcmath,curl,gd,intl,redis" setup_php
setup_mariadb # MariaDB database
setup_mysql # MySQL database
setup_postgresql # PostgreSQL
setup_docker # Docker Engine
setup_composer # PHP Composer
setup_python # Python 3
setup_ruby # Ruby
setup_rust # Rust
Best Practices
DO:
Always use $STD for commands
# ✅ Good: Respects VERBOSE setting
$STD apt-get install -y nginx
Generate random passwords safely
# ✅ Good: Alphanumeric only
DB_PASS = $( openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13 )
Check command success
# ✅ Good: Verify success
if ! wget -q "https://example.com/file.tar.gz" ; then
msg_error "Download failed"
exit 1
fi
Set proper permissions
# ✅ Good: Explicit permissions
chown -R www-data:www-data /opt/appname
chmod -R 755 /opt/appname
chmod 600 /opt/appname/.env # Protect secrets
Save version for update checks
# ✅ Good: Version tracked
echo "${ RELEASE }" > /opt/ ${ APP } _version.txt
Handle Alpine vs Debian differences
# ✅ Good: Detect OS
if grep -qi 'alpine' /etc/os-release ; then
apk add package
else
apt-get install -y package
fi
DON’T:
Hardcode versions
# ❌ Bad: Won't auto-update
wget https://example.com/app-1.2.3.tar.gz
# ✅ Good: Fetch latest
RELEASE = $( curl -fsSL https://api.github.com/repos/user/repo/releases/latest | \
grep "tag_name" | awk '{print substr($2, 2, length($2)-3)}' )
wget "https://example.com/app-${ RELEASE }.tar.gz"
Forget error handling
# ❌ Bad: Silent failures
wget https://example.com/file
tar -xzf file
# ✅ Good: Check errors
if ! wget https://example.com/file ; then
msg_error "Download failed"
exit 1
fi
Leave temporary files
# ✅ Always cleanup
rm -rf /opt/app- ${ RELEASE } .tar.gz /tmp/build-artifacts
Troubleshooting
Installation Hangs
Check internet connectivity :
Enable verbose mode :
# In ct/AppName.sh, before running
VERBOSE = "yes" bash install/AppName-install.sh
Package Not Found
Update package lists :
apt update
apt-cache search package_name
Service Won’t Start
Check logs :
journalctl -u appname -n 50
systemctl status appname
Contribution Checklist
Before submitting a PR: