Skip to content

Latest commit

 

History

History
1083 lines (855 loc) · 34.8 KB

File metadata and controls

1083 lines (855 loc) · 34.8 KB

🤖 AI Contribution Guidelines for ProxmoxVED

This documentation is intended for all AI assistants (GitHub Copilot, Claude, ChatGPT, etc.) contributing to this project.

🎯 Core Principles

1. Maximum Use of tools.func Functions

We have an extensive library of helper functions. NEVER implement your own solutions when a function already exists!

2. No Pointless Variables

Only create variables when they:

  • Are used multiple times
  • Improve readability
  • Are intended for configuration

3. Consistent Script Structure

All scripts follow an identical structure. Deviations are not acceptable.

4. Bare-Metal Installation

We do NOT use Docker for our installation scripts. All applications are installed directly on the system.


📁 Script Types and Their Structure

CT Script (ct/AppName.sh)

#!/usr/bin/env bash
# Engine comes from community-scripts/core; this repo only ships the scripts.
# Local checkout wins (COMMUNITY_SCRIPTS_CORE_DIR, else a sibling ../core), so a
# fork/branch of core can be tested without touching this file.
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")

# Copyright (c) 2021-2026 community-scripts ORG
# Author: AuthorName (GitHubUsername)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://application-url.com

APP="AppName"
var_tags="${var_tags:-tag1;tag2;tag3}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"

# Values the install script accepts up front (see "Application Settings").
# Without the export they never reach the container.
#export var_admin_user="${var_admin_user:-}"

header_info "$APP"
variables
color
catch_errors

function update_script() {
  header_info
  check_container_storage
  check_container_resources

  if [[ ! -d /opt/appname ]]; then
    msg_error "No ${APP} Installation Found!"
    exit
  fi

  if check_for_gh_release "appname" "owner/repo"; then
    msg_info "Stopping Service"
    systemctl stop appname
    msg_ok "Stopped Service"

    create_backup /opt/appname/.env /opt/appname/data

    CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo" "tarball"

    restore_backup

    # Build steps...

    msg_info "Starting Service"
    systemctl start appname
    msg_ok "Started Service"
    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 "${GATEWAY}${BGN}http://${IP}:PORT${CL}"

Install Script (install/AppName-install.sh)

#!/usr/bin/env bash

# Copyright (c) 2021-2026 community-scripts ORG
# Author: AuthorName (GitHubUsername)
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
# Source: https://application-url.com

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 \
  dependency1 \
  dependency2
msg_ok "Installed Dependencies"

# Runtime Setup (ALWAYS use our functions!)
NODE_VERSION="22" setup_nodejs
# or
PG_VERSION="16" setup_postgresql
# or
setup_uv
# etc.

fetch_and_deploy_gh_release "appname" "owner/repo" "tarball"

msg_info "Setting up Application"
cd /opt/appname
# Build/Setup Schritte...
msg_ok "Set up Application"

msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/appname.service
[Unit]
Description=AppName Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/appname
ExecStart=/path/to/executable
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now appname
msg_ok "Created Service"

motd_ssh
customize
cleanup_lxc

🔧 Available Helper Functions

Release Management

Function Description Example
fetch_and_deploy_gh_release Fetches and installs GitHub Release fetch_and_deploy_gh_release "app" "owner/repo" "tarball"
check_for_gh_release Checks for new version if check_for_gh_release "app" "owner/repo"; then
get_latest_github_release Returns latest release version string VERSION=$(get_latest_github_release "owner/repo")

Modes for fetch_and_deploy_gh_release:

# Tarball/Source (Standard) - always specify "tarball" explicitly
fetch_and_deploy_gh_release "appname" "owner/repo" "tarball"

# Binary (.deb)
fetch_and_deploy_gh_release "appname" "owner/repo" "binary"

# Prebuilt Archive
fetch_and_deploy_gh_release "appname" "owner/repo" "prebuild" "latest" "/opt/appname" "filename.tar.gz"

# Single Binary
fetch_and_deploy_gh_release "appname" "owner/repo" "singlefile" "latest" "/opt/appname" "binary-linux-amd64"

Clean Install Flag:

CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo" "tarball"

Pre-release projects: /releases/latest on GitHub hides pre-releases. For projects that only ship betas (e.g. RustFS), set GH_INCLUDE_PRERELEASE=1 — it applies to both fetch_and_deploy_gh_release and check_for_gh_release:

GH_INCLUDE_PRERELEASE=1 fetch_and_deploy_gh_release "app" "owner/repo" "prebuild" "latest" "/opt/app" "app-linux-amd64.zip"
if GH_INCLUDE_PRERELEASE=1 check_for_gh_release "app" "owner/repo"; then

Version file: After fetch_and_deploy_gh_release, the deployed version is stored in ~/.appname. You can read it with cat ~/.appname — useful when you need the version later (e.g. for build-time environment variables).

Runtime/Language Setup

Function Variable(s) Example
setup_nodejs NODE_VERSION, NODE_MODULE NODE_VERSION="22" setup_nodejs
setup_uv UV_PYTHON UV_PYTHON="3.12" setup_uv
setup_go GO_VERSION GO_VERSION="1.22" setup_go
setup_rust RUST_VERSION, RUST_CRATES RUST_CRATES="monolith" setup_rust
setup_ruby RUBY_VERSION RUBY_VERSION="3.3" setup_ruby
setup_java JAVA_VERSION JAVA_VERSION="21" setup_java
setup_php PHP_VERSION, PHP_MODULES PHP_VERSION="8.3" PHP_MODULES="redis,gd" setup_php

Database Setup

Function Variable(s) Example
setup_postgresql PG_VERSION, PG_MODULES PG_VERSION="16" setup_postgresql
setup_postgresql_db PG_DB_NAME, PG_DB_USER PG_DB_NAME="mydb" PG_DB_USER="myuser" setup_postgresql_db
setup_mariadb_db MARIADB_DB_NAME, MARIADB_DB_USER MARIADB_DB_NAME="mydb" setup_mariadb_db
setup_mysql MYSQL_VERSION setup_mysql
setup_mongodb MONGO_VERSION setup_mongodb
setup_clickhouse - setup_clickhouse

Tools & Utilities

Function Description
setup_adminer Installs Adminer for DB management
setup_composer Install PHP Composer
setup_ffmpeg Install FFmpeg (see below)
setup_imagemagick Install ImageMagick
setup_gs Install Ghostscript
setup_hwaccel Configure hardware acceleration

FFmpeg acquisition (FFMPEG_TYPE):

Value Source When to use
repo (default) Distribution package (apt install ffmpeg) Almost always. Debian 13 ships 7.1.x.
github Prebuilt static build from BtbN/FFmpeg-Builds Newer than the distro, or a specific release line
minimal / medium / full Compiled from source Only for codecs the above cannot provide
setup_ffmpeg                                    # distribution package
FFMPEG_TYPE="github" setup_ffmpeg               # latest master, GPL
FFMPEG_TYPE="github" FFMPEG_LICENSE="lgpl" setup_ffmpeg
FFMPEG_TYPE="github" FFMPEG_VERSION="n7.1" setup_ffmpeg
FFMPEG_TYPE="full" setup_ffmpeg                 # 20+ min build, avoid

Source builds use --enable-gpl --enable-nonfree. Those binaries must not be redistributed - building them on the target host for its own use is fine, shipping them is not. Use repo or FFMPEG_LICENSE=lgpl when an app requires LGPL FFmpeg.

Helper Utilities

Function/Variable Description Example
$LOCAL_IP Always available - contains the container's IP address echo "Access: http://${LOCAL_IP}:3000"
ensure_dependencies Checks/installs dependencies ensure_dependencies curl jq
install_packages_with_retry APT install with retry install_packages_with_retry nginx redis
create_backup Backs up paths before an update create_backup /opt/app/.env /opt/app/data
restore_backup Restores everything create_backup recorded restore_backup

❌ Anti-Patterns (NEVER use!)

1. Pointless Variables

# ❌ WRONG - unnecessary variables
APP_NAME="myapp"
APP_DIR="/opt/${APP_NAME}"
APP_USER="root"
APP_PORT="3000"
cd $APP_DIR

# ✅ CORRECT - use directly
cd /opt/myapp

2. Custom Download Logic

# ❌ WRONG - custom wget/curl logic
RELEASE=$(curl -s https://api.github.com/repos/owner/repo/releases/latest | jq -r '.tag_name')
wget https://github.com/owner/repo/archive/${RELEASE}.tar.gz
tar -xzf ${RELEASE}.tar.gz
mv repo-${RELEASE} /opt/myapp

# ✅ CORRECT - use our function
fetch_and_deploy_gh_release "myapp" "owner/repo"

3. Custom Version-Check Logic

# ❌ WRONG - custom version check
CURRENT=$(cat /opt/myapp/version.txt)
LATEST=$(curl -s https://api.github.com/repos/owner/repo/releases/latest | jq -r '.tag_name')
if [[ "$CURRENT" != "$LATEST" ]]; then
  # update...
fi

# ✅ CORRECT - use our function
if check_for_gh_release "myapp" "owner/repo"; then
  # update...
fi

4. Docker-based Installation

# ❌ WRONG - using Docker
docker pull myapp/myapp:latest
docker run -d --name myapp myapp/myapp:latest

# ✅ CORRECT - Bare-Metal Installation
fetch_and_deploy_gh_release "myapp" "owner/repo"
npm install && npm run build

5. Custom Runtime Installation

# ❌ WRONG - custom Node.js installation
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs

# ✅ CORRECT - use our function
NODE_VERSION="22" setup_nodejs

6. Redundant echo Statements

# ❌ WRONG - custom logging messages
echo "Installing dependencies..."
apt install -y curl
echo "Done!"

# ✅ CORRECT - use msg_info/msg_ok
msg_info "Installing Dependencies"
$STD apt install -y curl
msg_ok "Installed Dependencies"

7. Missing $STD Usage

# ❌ WRONG - apt without $STD
apt install -y nginx

# ✅ CORRECT - with $STD for silent output
$STD apt install -y nginx

8. Wrapping tools.func Functions in msg Blocks

# ❌ WRONG - tools.func functions have their own msg_info/msg_ok!
msg_info "Installing Node.js"
NODE_VERSION="22" setup_nodejs
msg_ok "Installed Node.js"

msg_info "Updating Application"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo"
msg_ok "Updated Application"

# ✅ CORRECT - call directly without msg wrapper
NODE_VERSION="22" setup_nodejs

CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo"

Functions with built-in messages (NEVER wrap in msg blocks):

  • fetch_and_deploy_gh_release
  • check_for_gh_release
  • setup_nodejs
  • setup_postgresql / setup_postgresql_db
  • setup_mariadb / setup_mariadb_db
  • setup_mongodb
  • setup_mysql
  • setup_ruby
  • setup_go
  • setup_java
  • setup_php
  • setup_uv
  • setup_rust
  • setup_composer
  • setup_ffmpeg
  • setup_imagemagick
  • setup_gs
  • setup_adminer
  • setup_hwaccel
  • create_backup / restore_backup

9. Creating Unnecessary System Users

# ❌ WRONG - LXC containers run as root, no separate user needed
useradd -m -s /usr/bin/bash appuser
chown -R appuser:appuser /opt/appname
sudo -u appuser npm install

# ✅ CORRECT - run directly as root
cd /opt/appname
$STD npm install

10. Using export in .env Files

# ❌ WRONG - export is unnecessary in .env files
cat <<EOF >/opt/appname/.env
export DATABASE_URL=postgres://...
export SECRET_KEY=abc123
export NODE_ENV=production
EOF

# ✅ CORRECT - simple KEY=VALUE format (files are sourced with set -a)
cat <<EOF >/opt/appname/.env
DATABASE_URL=postgres://...
SECRET_KEY=abc123
NODE_ENV=production
EOF

11. Using External Shell Scripts

# ❌ WRONG - external script that gets executed
cat <<'EOF' >/opt/appname/install_script.sh
#!/bin/bash
cd /opt/appname
npm install
npm run build
EOF
chmod +x /opt/appname/install_script.sh
$STD bash /opt/appname/install_script.sh
rm -f /opt/appname/install_script.sh

# ✅ CORRECT - run commands directly
cd /opt/appname
$STD npm install
$STD npm run build

12. Using sudo in LXC Containers

# ❌ WRONG - sudo is unnecessary in LXC (already root)
sudo -u postgres psql -c "CREATE DATABASE mydb;"
sudo -u appuser npm install

# ✅ CORRECT - use functions or run directly as root
PG_DB_NAME="mydb" PG_DB_USER="myuser" setup_postgresql_db

cd /opt/appname
$STD npm install

13. Unnecessary systemctl daemon-reload

# ❌ WRONG - daemon-reload is only needed when MODIFYING existing services
cat <<EOF >/etc/systemd/system/appname.service
# ... service config ...
EOF
systemctl daemon-reload  # Unnecessary for new services!
systemctl enable -q --now appname

# ✅ CORRECT - new services don't need daemon-reload
cat <<EOF >/etc/systemd/system/appname.service
# ... service config ...
EOF
systemctl enable -q --now appname

14. Creating Custom Credentials Files

# ❌ WRONG - custom credentials file is not part of the standard template
msg_info "Saving Credentials"
cat <<EOF >~/appname.creds
Database User: ${DB_USER}
Database Pass: ${DB_PASS}
EOF
msg_ok "Saved Credentials"

# ✅ CORRECT - credentials are stored in .env or shown in final message only
# The .env file contains credentials, no need for separate file

15. Wrong Footer Pattern

# ❌ WRONG - old cleanup pattern with msg blocks
motd_ssh
customize

msg_info "Cleaning up"
$STD apt-get -y autoremove
$STD apt-get -y autoclean
msg_ok "Cleaned"

# ✅ CORRECT - use cleanup_lxc function
motd_ssh
customize
cleanup_lxc

16. Manual Database Creation Instead of Functions

# ❌ WRONG - manual database creation
DB_USER="myuser"
DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | cut -c1-13)
$STD sudo -u postgres psql -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASS';"
$STD sudo -u postgres psql -c "CREATE DATABASE mydb WITH OWNER $DB_USER;"
$STD sudo -u postgres psql -d mydb -c "CREATE EXTENSION IF NOT EXISTS postgis;"

# ✅ CORRECT - use setup_postgresql_db function
# This sets PG_DB_USER, PG_DB_PASS, PG_DB_NAME automatically
PG_DB_NAME="mydb" PG_DB_USER="myuser" PG_DB_EXTENSIONS="postgis" setup_postgresql_db

18. Hardcoded Versions for External Tools

# ❌ WRONG - hardcoded versions that will become outdated
RESTIC_VERSION="0.18.1"
RCLONE_VERSION="1.73.0"
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"

# ✅ CORRECT - use fetch_and_deploy_gh_release (always fetches latest)
fetch_and_deploy_gh_release "restic" "restic/restic" "singlefile" "latest" "/usr/local/bin" "restic_*_linux_amd64.bz2"

# If you need the version number later, read from the version file:
RES_VERSION=$(cat ~/.restic)
# Or use get_latest_github_release:
VERSION=$(get_latest_github_release "restic/restic")

19. Hand-rolled Backups in Update Scripts

# ❌ WRONG - manual cp/mv dance (and /tmp can be cleared by the system)
msg_info "Backing up Configuration"
cp /opt/appname/.env /tmp/appname.env.bak
msg_ok "Backed up Configuration"
# ... update ...
cp /tmp/appname.env.bak /opt/appname/.env

# ✅ CORRECT - use the helpers (they bring their own msg_info/msg_ok)
create_backup /opt/appname/.env /opt/appname/data
# ... update ...
restore_backup

create_backup stores into /opt/<NSAPP>.backup (override with BACKUP_DIR), records a manifest so restore_backup needs no arguments, uses cp -a so permissions survive, skips re-backing-up on a retry so the last-known-good copy is kept, and aborts the update if the backup itself fails.

20. Using "(Patience)" in msg_info by Default

# ❌ WRONG - "(Patience)" should not be a default label
msg_info "Building Application (Patience)"
$STD npm run build
msg_ok "Built Application"

# ✅ CORRECT - use a plain label; only add (Patience) if the build truly takes 10+ minutes
msg_info "Building Application"
$STD npm run build
msg_ok "Built Application"

21. Writing Files Without Heredocs

# ❌ WRONG - echo / printf / tee
echo "# Config" > /opt/app/config.yml
echo "port: 3000" >> /opt/app/config.yml

printf "# Config\nport: 3000\n" > /opt/app/config.yml
cat config.yml | tee /opt/app/config.yml
# ✅ CORRECT - always use a single heredoc
cat <<EOF >/opt/app/config.yml
# Config
port: 3000
EOF

22. Using apt-get Instead of apt

# ❌ WRONG - apt-get is not the project convention
$STD apt-get install -y nginx
$STD apt-get update

# ✅ CORRECT - always use apt (consistent with tools.func)
$STD apt install -y nginx
$STD apt update

23. Listing Core/Pre-installed Packages as Dependencies

# ❌ WRONG - curl is already installed by _bootstrap() in install.func
# sudo is already available in LXC, mc is not a dependency
msg_info "Installing Dependencies"
$STD apt install -y \
  curl \
  sudo \
  mc \
  fuse3
msg_ok "Installed Dependencies"

# ✅ CORRECT - only list packages that are actually needed by the application
msg_info "Installing Dependencies"
$STD apt install -y fuse3
msg_ok "Installed Dependencies"

Packages that must NOT be listed as dependencies (already available):

  • curl — installed by _bootstrap() in install.func
  • sudo — base LXC package (and scripts run as root anyway)
  • wget — base Debian LXC package
  • gnupg / gpg — base Debian package
  • ca-certificates — base Debian package
  • apt-transport-https — obsolete on Debian 12+
  • jq — auto-installed by ensure_dependencies in tools.func when needed
  • mc — not a dependency, personal preference tool

When to omit the dependency block entirely: If the app only needs packages provided by setup_* helpers (e.g., Node.js, PostgreSQL, Go) or is a prebuilt binary with no native deps, skip the "Installing Dependencies" block completely.

24. Prompting Without an Escape Hatch

A read that always fires cannot be answered in advance, so the script can only ever be installed by hand. Read the variable first and prompt only when it is unset:

# ❌ WRONG - the environment is overwritten before it is ever read.
# read assigns an empty string when stdin is closed, so the :- fallback
# fires and whatever the caller passed is gone.
read -rp "${TAB3}Admin username: " admin_user
admin_user="${admin_user:-admin}"

# ✅ CORRECT
if [[ -z "${var_admin_user:-}" ]]; then
  read -rp "${TAB3}Admin username: " var_admin_user
fi
var_admin_user="${var_admin_user:-admin}"

Name it var_<something> — the same namespace the container variables use — export it from ct/<app>.sh, and declare it in the JSON app_vars. All three are needed: without the export it never reaches the container, and without the declaration the website cannot offer it as a field.

install/forgejo-runner-install.sh and install/pangolin-install.sh follow this.


📝 Important Rules

Variable Declarations (CT Script)

# Standard declarations (ALWAYS present)
APP="AppName"
var_tags="${var_tags:-tag1;tag2}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_unprivileged="${var_unprivileged:-1}"

Optional declarations

Variable Values Meaning
var_gpu yes / no Offer GPU passthrough. Set for transcoding and AI workloads.
var_arm64 yes / no / (unset) arm64 support — see below.
var_testurl an https:// URL Where feedback for this script goes — see below.

var_testurl names the thread collecting feedback for a script that is still being tested. Create the issue, then point the script at it:

var_testurl="${var_testurl:-https://github.com/community-scripts/ProxmoxVED/issues/2135}"

The container then asks for feedback on every login, in its Proxmox description, through a testing tag, and on the last line of the install — always with that one link, so a tester never has to work out where to report.

Leaving it out changes nothing: a script here still gets the generic development warning. Only https:// URLs are accepted, and a rejected value falls back to that generic warning rather than failing the build. It is not settable from a .vars file, because it describes the script rather than the user's preferences.

Keep it set if the script is promoted to ProxmoxVE while feedback is still wanted — the request follows the script and stops naming ProxmoxVED.

var_arm64 has three states. Only claim yes when it has actually been run on arm64 — the mere existence of an arm64 artifact is not verification:

  • yes — verified working, proceeds silently
  • no — known broken (x64-only artifact, x86 dependency, CUDA), aborts
  • unset — never tried. The user is told so and asked whether to attempt it anyway, with a pointer to report the result. Aborts non-interactively.

Leave the line in place but commented out, so the option stays discoverable where someone would look for it:

#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified

When setting it to no, state the reason next to it — otherwise the value degrades back into "nobody checked".

Application settings

Anything the install script should be able to receive up front is declared here too, and must be exportedlxc-attach carries the caller's environment, but only what was exported:

export var_admin_user="${var_admin_user:-}"
export var_admin_token="${var_admin_token:-}"

Without the export the variable stays on the host, the install script finds it empty, and an unattended run stops at a prompt inside the container where nobody can answer it. Declare the same names in the JSON app_vars so the website can offer them as fields.

If a value is required, fail early — checking it in the CT script costs the user seconds, checking it inside the container costs a full build:

if [[ -n "${mode:-}" ]]; then
  if [[ -z "${var_admin_token:-}" ]]; then
    msg_error "var_admin_token is required for unattended installs."
    exit 1
  fi
fi

Update-Script Pattern

function update_script() {
  header_info
  check_container_storage
  check_container_resources

  # 1. Check if installation exists
  if [[ ! -d /opt/appname ]]; then
    msg_error "No ${APP} Installation Found!"
    exit
  fi

  # 2. Check for update
  if check_for_gh_release "appname" "owner/repo"; then
    # 3. Stop service
    msg_info "Stopping Service"
    systemctl stop appname
    msg_ok "Stopped Service"

    # 4. Backup config/data (if present) - has its own messages, do not wrap
    create_backup /opt/appname/.env /opt/appname/data

    # 5. Perform clean install
    CLEAN_INSTALL=1 fetch_and_deploy_gh_release "appname" "owner/repo" "tarball"

    # 6. Restore BEFORE any build step that reads the config
    restore_backup

    # 7. Rebuild (if needed)
    cd /opt/appname
    $STD npm install
    $STD npm run build

    # 8. Start service
    msg_info "Starting Service"
    systemctl start appname
    msg_ok "Started Service"
    msg_ok "Updated successfully!"
  fi
  exit  # IMPORTANT: Always end with exit!
}

Systemd Service Pattern

msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/appname.service
[Unit]
Description=AppName Service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/appname
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /opt/appname/server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now appname
msg_ok "Created Service"

Installation Script Footer

# ALWAYS at the end of the install script:
motd_ssh
customize
cleanup_lxc

🔍 Checklist Before PR Creation

  • No Docker installation used
  • fetch_and_deploy_gh_release used for GitHub releases (with explicit mode like "tarball")
  • check_for_gh_release used for update checks
  • setup_* functions used for runtimes (nodejs, postgresql, etc.)
  • tools.func functions NOT wrapped in msg_info/msg_ok blocks
  • No redundant variables
  • No hardcoded versions for external tools (use fetch_and_deploy_gh_release or get_latest_github_release)
  • $STD before all apt/npm/build commands
  • apt used (NOT apt-get) — consistent with tools.func
  • No core packages listed as dependencies (curl, sudo, wget, jq, mc are pre-installed)
  • msg_info/msg_ok/msg_error for logging (only for custom code)
  • Correct script structure followed
  • Update function present and functional
  • Data backup implemented in update function (backups go to /opt, NOT /tmp)
  • motd_ssh, customize, cleanup_lxc at the end
  • No custom download/version-check logic
  • No default (Patience) text in msg_info labels
  • JSON metadata file created in json/<appname>.json

📖 Reference: Good Example (Termix)

CT Script: ct/termix.sh

  • Uses check_for_gh_release for version checking
  • Uses CLEAN_INSTALL=1 fetch_and_deploy_gh_release for clean updates
  • Backup/restore of /opt/termix/data
  • Correct structure with all required variables
  • NODE_VERSION="22" setup_nodejs instead of manual installation
  • fetch_and_deploy_gh_release "termix" "Termix-SSH/Termix" instead of wget/curl
  • Clean service configuration
  • Correct footer with motd_ssh, customize, cleanup_lxc

� JSON Metadata Files

Every application requires a JSON metadata file in json/<appname>.json.

JSON Structure

{
  "name": "AppName",
  "slug": "appname",
  "categories": [1],
  "date_created": "2026-01-16",
  "type": "ct",
  "updateable": true,
  "privileged": false,
  "interface_port": 3000,
  "documentation": "https://docs.appname.com/",
  "website": "https://appname.com/",
  "repository": "https://github.com/owner/appname",
  "architectures": ["amd64"],
  "platforms": ["pve"],
  "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/appname.webp",
  "description": "Short description of the application and its purpose.",
  "install_methods": [
    {
      "type": "default",
      "script": "ct/appname.sh",
      "config_path": "/opt/appname/.env",
      "resources": {
        "cpu": 2,
        "ram": 2048,
        "hdd": 8,
        "os": "Debian",
        "version": "13"
      }
    }
  ],
  "default_credentials": {
    "username": null,
    "password": null
  },
  "notes": []
}

Required Fields

Field Type Description
name string Display name of the application
slug string Lowercase, no spaces, used for filenames
categories array Category ID(s) - see category list below
date_created string Creation date (YYYY-MM-DD)
type string ct for container, vm for virtual machine
updateable boolean Whether update_script is implemented
privileged boolean Whether container needs privileged mode
interface_port number Primary web interface port (or null)
documentation string Link to official docs
website string Link to official website
repository string Upstream repository as a full URL. A bare owner/repo could only ever mean GitHub, and the release sync also reads GitLab, Gitea, Forgejo and Codeberg
architectures array Must agree with var_arm64 in the CT script — that is the one arch_check obeys. yes["amd64", "arm64"], no["amd64"], unset → omit the field. The site reads an absent field as amd64, so "known broken" and "never tried" look the same there; only var_arm64 keeps them apart
logo string URL to application logo (preferably selfhst icons)
description string Brief description of the application
install_methods array Installation configurations
default_credentials object Default username/password (or null)
notes array Additional notes/warnings

Optional Fields

Field Type Description
platforms array ["pve"], ["incus"] or both. Omit to mean Proxmox VE
app_vars array Values the install script accepts up front, so a deployment can run unattended

app_vars describes what the script already reads from the environment. Name each one var_<something>, read it before prompting, and export it from ct/<app>.sh — without the export it never reaches the container:

# ct/appname.sh
export var_admin_user="${var_admin_user:-}"

# install/appname-install.sh
if [[ -z "${var_admin_user:-}" ]]; then
  read -r -p "${TAB3}Admin username: " var_admin_user
fi
var_admin_user="${var_admin_user:-admin}"
"app_vars": [
  { "name": "var_admin_user", "label": "Admin Username", "type": "text", "default": "admin" },
  { "name": "var_admin_pass", "label": "Admin Password", "type": "password", "secret": true, "required": true }
]

type is one of text, password, number, boolean (emits yes/no) or select (with options). A declaration whose name the script never reads produces a generated command that looks right and changes nothing.

Categories

ID Category
0 Miscellaneous
1 Proxmox & Virtualization
2 Operating Systems
3 Containers & Docker
4 Network & Firewall
5 Adblock & DNS
6 Authentication & Security
7 Backup & Recovery
8 Databases
9 Monitoring & Analytics
10 Dashboards & Frontends
11 Files & Downloads
12 Documents & Notes
13 Media & Streaming
14 *Arr Suite
15 NVR & Cameras
16 IoT & Smart Home
17 ZigBee, Z-Wave & Matter
18 MQTT & Messaging
19 Automation & Scheduling
20 AI / Coding & Dev-Tools
21 Webservers & Proxies
22 Bots & ChatOps
23 Finance & Budgeting
24 Gaming & Leisure
25 Business & ERP

Notes Format

"notes": [
    {
        "text": "Change the default password after first login!",
        "type": "warning"
    },
    {
        "text": "Requires at least 4GB RAM for optimal performance.",
        "type": "info"
    }
]

Note types: info, warning, error

Examples with Credentials

"default_credentials": {
    "username": "admin",
    "password": "admin"
}

Or no credentials:

"default_credentials": {
    "username": null,
    "password": null
}

�💡 Tips for AI Assistants

  1. Search tools.func first before implementing custom solutions
  2. Use existing scripts as reference (e.g., linkwarden-install.sh, homarr-install.sh)
  3. Ask when uncertain instead of introducing wrong patterns
  4. Consistency > Creativity - follow established patterns
  5. Test local variables - use ${VAR:-default} pattern for optional values

📚 Further Documentation