#!/usr/bin/env bash set -Eeuo pipefail BUILD_FUNC_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func" if command -v curl >/dev/null 2>&1; then source <(curl -fsSL "$BUILD_FUNC_URL") elif command -v wget >/dev/null 2>&1; then source <(wget -qO- "$BUILD_FUNC_URL") else printf '[OpenCode] Error: missing required command: curl or wget\n' >&2 exit 1 fi # Copyright (c) 2026 # License: MIT APP="OpenCode" DEFAULT_HOSTNAME="opencode" if [[ -n "${var_hostname:-}" ]]; then : elif [[ -n "${HOSTNAME:-}" && "${HOSTNAME}" != "$(hostname)" ]]; then var_hostname="$HOSTNAME" else var_hostname="$DEFAULT_HOSTNAME" fi ONBOOT="${ONBOOT:-1}" TEMPLATE_ARCH="${TEMPLATE_ARCH:-${ARCH:-amd64}}" var_tags="${var_tags:-opencode}" var_ctid="${var_ctid:-${CTID:-}}" var_cpu="${var_cpu:-${CORES:-2}}" var_ram="${var_ram:-${MEMORY:-4096}}" var_disk="${var_disk:-${DISK_GB:-12}}" var_os="${var_os:-debian}" var_version="${var_version:-${OSVERSION:-12}}" var_unprivileged="${var_unprivileged:-${UNPRIVILEGED:-1}}" var_brg="${var_brg:-${BRIDGE:-vmbr0}}" var_net="${var_net:-${IP_CONFIG:-dhcp}}" var_gateway="${var_gateway:-${GATEWAY:-}}" var_ns="${var_ns:-${NAMESERVER:-}}" var_searchdomain="${var_searchdomain:-${SEARCHDOMAIN:-}}" var_template_storage="${var_template_storage:-${TEMPLATE_STORAGE:-}}" var_container_storage="${var_container_storage:-${CONTAINER_STORAGE:-}}" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" INSTALL_SCRIPT="${SCRIPT_DIR%/ct}/install/opencode-install.sh" OPENCODE_USER="${OPENCODE_USER:-opencode}" OPENCODE_VERSION="${OPENCODE_VERSION:-}" OPENCODE_WEB_HOSTNAME="${OPENCODE_WEB_HOSTNAME:-0.0.0.0}" OPENCODE_WEB_PORT="${OPENCODE_WEB_PORT:-4096}" OPENCODE_SERVER_USERNAME="${OPENCODE_SERVER_USERNAME:-opencode}" OPENCODE_SERVER_PASSWORD="${OPENCODE_SERVER_PASSWORD:-}" SSH_PUBLIC_KEY_FILE="${SSH_PUBLIC_KEY_FILE:-}" SWAP="${SWAP:-512}" header_info "$APP" variables color catch_errors clean_password() { local raw="${var_pw:-}" [[ -n "$raw" ]] || return 0 case "$raw" in --password\ *) raw="${raw#--password }" ;; -password\ *) raw="${raw#-password }" ;; esac while [[ "$raw" == -* ]]; do raw="${raw#-}" done [[ -n "$raw" ]] && printf '%s\n' "$raw" } resolve_template_name() { pveam available --section system | awk -v os="$var_os" -v ver="$var_version" -v arch="$TEMPLATE_ARCH" ' $2 ~ ("^" os "-" ver "-standard_.*_" arch "\\.tar\\.(gz|xz|zst)$") {print $2; exit} ' } build_net_arg() { local net="name=eth0,bridge=${BRG:-vmbr0},ip=${NET:-dhcp}" [[ -n "${GATE:-}" ]] && net+=",gw=${GATE}" [[ -n "${VLAN:-}" ]] && net+=",tag=${VLAN}" [[ -n "${MTU:-}" ]] && net+=",mtu=${MTU}" [[ -n "${MAC:-}" ]] && net+=",hwaddr=${MAC}" case "${IPV6_METHOD:-none}" in auto) net+=",ip6=auto" ;; dhcp) net+=",ip6=dhcp" ;; static) [[ -n "${IPV6_STATIC:-}" ]] && net+=",ip6=${IPV6_STATIC}" ;; esac printf '%s\n' "$net" } build_features_arg() { if [[ -n "${FEATURES:-}" ]]; then printf '%s\n' "$FEATURES" return 0 fi local features=() if [[ "${ENABLE_NESTING:-1}" == "1" ]]; then features+=("nesting=1") fi if [[ "${CT_TYPE:-1}" == "1" ]]; then features+=("keyctl=1") fi if [[ "${ENABLE_FUSE:-no}" == "yes" ]]; then features+=("fuse=1") fi if [[ "${ENABLE_MKNOD:-0}" == "1" ]]; then features+=("mknod=1") fi if [[ -n "${var_mount_fs:-}" ]]; then features+=("mount=${var_mount_fs}") fi if [[ ${#features[@]} -gt 0 ]]; then local joined="${features[*]}" printf '%s\n' "${joined// /,}" fi } install_custom_ssh_keys() { [[ -n "$SSH_PUBLIC_KEY_FILE" ]] || return 0 [[ -f "$SSH_PUBLIC_KEY_FILE" ]] || { msg_error "SSH key file not found: $SSH_PUBLIC_KEY_FILE" exit 116 } msg_info "Installing custom SSH public keys" pct exec "$CTID" -- sh -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' pct push "$CTID" "$SSH_PUBLIC_KEY_FILE" /tmp/opencode-authorized_keys --perms 600 >/dev/null pct exec "$CTID" -- sh -c 'cat /tmp/opencode-authorized_keys >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && rm -f /tmp/opencode-authorized_keys' msg_ok "Installed custom SSH public keys" } resolve_opencode_user() { if id -u "$OPENCODE_USER" >/dev/null 2>&1; then printf '%s\n' "$OPENCODE_USER" return 0 fi if id -u opencode >/dev/null 2>&1; then printf '%s\n' "opencode" return 0 fi local uid_1000_user uid_1000_user="$(getent passwd 1000 | cut -d: -f1 || true)" [[ -n "$uid_1000_user" ]] && printf '%s\n' "$uid_1000_user" } update_script() { header_info check_container_storage check_container_resources msg_info "Updating base system" $STD apt-get update $STD apt-get upgrade -y msg_ok "Base system updated" local install_user install_user="$(resolve_opencode_user)" if [[ -z "$install_user" ]]; then msg_warn "No OpenCode user found, skipping OpenCode update" msg_ok "Updated successfully!" exit fi msg_info "Updating OpenCode" if [[ -n "$OPENCODE_VERSION" ]]; then $STD su - "$install_user" -c "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path --version ${OPENCODE_VERSION}" else $STD su - "$install_user" -c "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path" fi if [[ -x "/home/${install_user}/.opencode/bin/opencode" ]]; then ln -sf "/home/${install_user}/.opencode/bin/opencode" /usr/local/bin/opencode fi install -d -m 0755 -o "$install_user" -g "$install_user" /workspace if systemctl list-unit-files opencode-web.service >/dev/null 2>&1; then $STD systemctl daemon-reload $STD systemctl restart opencode-web.service fi msg_ok "OpenCode updated" msg_ok "Updated successfully!" exit } build_container() { [[ -f "$INSTALL_SCRIPT" ]] || { msg_error "Missing installer script at $INSTALL_SCRIPT" exit 117 } export CTID="$CT_ID" local template_storage="${TEMPLATE_STORAGE:-${var_template_storage:-}}" local container_storage="${CONTAINER_STORAGE:-${var_container_storage:-}}" local template_name local template_ref local net_arg local features_arg local clean_pw local tags local version local install_path="/root/opencode-install.sh" local -a pct_args [[ -n "$template_storage" ]] || template_storage="$(pvesm status --content vztmpl | awk 'NR>1 {print $1; exit}')" [[ -n "$container_storage" ]] || container_storage="$(pvesm status --content rootdir | awk 'NR>1 {print $1; exit}')" [[ -n "$template_storage" ]] || { msg_error "No storage found for content type 'vztmpl'" exit 118 } [[ -n "$container_storage" ]] || { msg_error "No storage found for content type 'rootdir'" exit 119 } msg_info "Updating template list" pveam update >/dev/null template_name="$(resolve_template_name)" [[ -n "$template_name" ]] || { msg_error "No template found for ${var_os}-${var_version} (amd64)" exit 120 } msg_info "Ensuring template ${template_name} exists on ${template_storage}" if ! pveam list "$template_storage" | awk 'NR>1 {print $2}' | grep -Fxq "$template_name"; then pveam download "$template_storage" "$template_name" fi template_ref="${template_storage}:vztmpl/${template_name}" net_arg="$(build_net_arg)" features_arg="$(build_features_arg)" clean_pw="$(clean_password || true)" tags="community-script;${var_tags}" pct_args=( create "$CTID" "$template_ref" --hostname "$HN" --ostype "$var_os" --unprivileged "$CT_TYPE" --cores "$CORE_COUNT" --memory "$RAM_SIZE" --swap "$SWAP" --rootfs "${container_storage}:${DISK_SIZE}" --net0 "$net_arg" --onboot "$ONBOOT" --tags "$tags" ) if [[ -n "$features_arg" ]]; then pct_args+=(--features "$features_arg") fi if [[ -n "${var_ns:-}" ]]; then pct_args+=(--nameserver "${var_ns}") fi if [[ -n "${var_searchdomain:-}" ]]; then pct_args+=(--searchdomain "${var_searchdomain}") fi if [[ -n "${CT_TIMEZONE:-}" ]]; then pct_args+=(--timezone "${CT_TIMEZONE}") fi if [[ -n "$clean_pw" ]]; then pct_args+=(--password "$clean_pw") fi msg_info "Creating LXC Container" pct "${pct_args[@]}" msg_ok "Created LXC Container" if [[ "${PROTECT_CT:-no}" == "yes" || "${PROTECT_CT:-0}" == "1" ]]; then pct set "$CTID" --protection 1 >/dev/null fi msg_info "Starting LXC Container" pct start "$CTID" sleep 5 msg_ok "Started LXC Container" install_ssh_keys_into_ct || true install_custom_ssh_keys msg_info "Pushing OpenCode installer" pct push "$CTID" "$INSTALL_SCRIPT" "$install_path" --perms 755 msg_ok "Pushed OpenCode installer" msg_info "Installing OpenCode" pct exec "$CTID" -- env \ OPENCODE_USER="$OPENCODE_USER" \ OPENCODE_VERSION="$OPENCODE_VERSION" \ OPENCODE_WEB_HOSTNAME="$OPENCODE_WEB_HOSTNAME" \ OPENCODE_WEB_PORT="$OPENCODE_WEB_PORT" \ OPENCODE_SERVER_USERNAME="$OPENCODE_SERVER_USERNAME" \ OPENCODE_SERVER_PASSWORD="$OPENCODE_SERVER_PASSWORD" \ bash "$install_path" msg_ok "Installed OpenCode" version="$(pct exec "$CTID" -- su - "$OPENCODE_USER" -c 'opencode --version' 2>/dev/null | tail -n 1 || true)" [[ -n "$version" ]] && export OPENCODE_INSTALLED_VERSION="$version" } description() { local version="${OPENCODE_INSTALLED_VERSION:-}" local ct_ip local description ct_ip="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}' || true)" description=$( cat <

${APP} LXC

OpenCode web-enabled development container.

GitHub  |  Web Docs

Web UI: ${ct_ip:-Unavailable}:${OPENCODE_WEB_PORT}

EOF ) pct set "$CTID" --description "$description" >/dev/null 2>&1 || true msg_ok "Completed successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" if [[ -n "$version" ]]; then echo -e "${INFO}${YW} Installed version: ${BGN}${version}${CL}" fi echo -e "${INFO}${YW} Next steps:${CL}" echo -e "${TAB}Web UI: http://${ct_ip:-}:${OPENCODE_WEB_PORT}" echo -e "${TAB}pct enter ${CTID}" echo -e "${TAB}su - ${OPENCODE_USER}" echo -e "${TAB}opencode" echo -e "${INFO}${YW} Configure a provider with /connect or provider environment variables.${CL}" } start build_container description