From c5aea8a7ada97ef281e2c2e53e8a71d4b9151a36 Mon Sep 17 00:00:00 2001 From: caoimhinr Date: Sun, 12 Apr 2026 20:40:34 +0200 Subject: [PATCH] Add OpenCode community-style LXC with web startup --- pve_community/AGENTS.md | 28 ++ pve_community/README.md | 84 +++++ pve_community/ct/opencode.sh | 364 ++++++++++++++++++++++ pve_community/install/opencode-install.sh | 123 ++++++++ 4 files changed, 599 insertions(+) create mode 100644 pve_community/AGENTS.md create mode 100644 pve_community/README.md create mode 100644 pve_community/ct/opencode.sh create mode 100644 pve_community/install/opencode-install.sh diff --git a/pve_community/AGENTS.md b/pve_community/AGENTS.md new file mode 100644 index 0000000..3d2fa0f --- /dev/null +++ b/pve_community/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md + +## Repo Purpose + +This repository contains small Proxmox LXC helper scripts. + +- `ct/` contains host-side Proxmox creation entrypoints +- `install/` contains in-container install logic pushed into the created CT + +## Working Rules + +- Keep changes minimal and shell-first +- Prefer aligning host scripts with the `community-scripts` lifecycle when practical +- Preserve the documented legacy environment variables in `README.md` even when adding `var_*` support +- Use `apply_patch` for manual file edits +- Do not add extra tooling or framework files unless they directly support the scripts + +## Script Expectations + +- Host scripts should stay runnable with `bash ct/.sh` on a Proxmox host +- In-container installers should stay runnable as root inside the CT +- Favor documented Proxmox long options like `--content` and `--perms` +- Any new runtime service should be enabled explicitly and remain configurable through environment variables + +## Verification + +- Run `bash -n` on changed shell scripts after edits +- Update `README.md` when user-facing behavior or environment variables change diff --git a/pve_community/README.md b/pve_community/README.md new file mode 100644 index 0000000..2f11475 --- /dev/null +++ b/pve_community/README.md @@ -0,0 +1,84 @@ +# Personal Proxmox LXC Scripts + +This repo is a small personal version of the `community-scripts` pattern: + +- `ct/` contains host-side Proxmox creation scripts +- `install/` contains the in-container install logic + +## OpenCode LXC + +`ct/opencode.sh` creates a Debian LXC and installs the `opencode` CLI for a normal user. + +It now follows the upstream `community-scripts` host-side flow more closely, including the standard `Default Install`, `Advanced Install`, and `User Defaults` entry points. + +### What it does + +- picks a Debian 12 template from your configured Proxmox storages +- creates an unprivileged LXC with sensible defaults for a coding box +- installs common CLI tooling (`git`, `ripgrep`, `fd`, `curl`, `build-essential`) +- installs OpenCode via the official installer +- creates a writable `/workspace` directory owned by the `opencode` user +- enables a systemd-managed OpenCode web interface on boot + +### Usage + +Run on the Proxmox host as `root`: + +```bash +bash ct/opencode.sh +``` + +Example with overrides: + +```bash +CTID=220 \ +HOSTNAME=opencode-dev \ +BRIDGE=vmbr1 \ +CONTAINER_STORAGE=local-lvm \ +TEMPLATE_STORAGE=local \ +MEMORY=8192 \ +CORES=4 \ +DISK_GB=24 \ +OPENCODE_USER=dev \ +OPENCODE_WEB_PORT=4096 \ +bash ct/opencode.sh +``` + +You can also use the upstream-style `var_*` overrides, for example `var_cpu`, `var_ram`, `var_disk`, `var_brg`, `var_net`, `var_ctid`, and `var_hostname`. + +### Common Variables + +- `CTID` default `120` +- `HOSTNAME` default `opencode` +- `CORES` default `2` +- `MEMORY` default `4096` +- `DISK_GB` default `12` +- `BRIDGE` default `vmbr0` +- `IP_CONFIG` default `dhcp` +- `TEMPLATE_STORAGE` auto-selects first storage with `vztmpl` +- `CONTAINER_STORAGE` auto-selects first storage with `rootdir` +- `OPENCODE_USER` default `opencode` +- `OPENCODE_VERSION` empty means latest release +- `OPENCODE_WEB_HOSTNAME` default `0.0.0.0` +- `OPENCODE_WEB_PORT` default `4096` +- `OPENCODE_SERVER_USERNAME` default `opencode` +- `OPENCODE_SERVER_PASSWORD` optional basic-auth password for the web UI +- `SSH_PUBLIC_KEY_FILE` optional path to a host public key file to inject into the CT + +### Community-Scripts Behavior + +- supports the standard `community-scripts` settings flow on the Proxmox host +- supports `default.vars` and app-default handling from the shared `build.func` base +- still accepts the legacy environment variable names documented above for quick one-shot runs + +### After Creation + +```bash +pct enter 120 +su - opencode +opencode +``` + +The web interface is started automatically at boot and listens on `http://:4096` by default. + +Then configure a provider inside OpenCode with `/connect`, or set your provider credentials manually. diff --git a/pve_community/ct/opencode.sh b/pve_community/ct/opencode.sh new file mode 100644 index 0000000..b1a1f6f --- /dev/null +++ b/pve_community/ct/opencode.sh @@ -0,0 +1,364 @@ +#!/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 diff --git a/pve_community/install/opencode-install.sh b/pve_community/install/opencode-install.sh new file mode 100644 index 0000000..4550888 --- /dev/null +++ b/pve_community/install/opencode-install.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +OPENCODE_USER="${OPENCODE_USER:-opencode}" +OPENCODE_VERSION="${OPENCODE_VERSION:-}" +OPENCODE_HOME="/home/${OPENCODE_USER}" +OPENCODE_BIN="${OPENCODE_HOME}/.opencode/bin/opencode" +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:-}" + +log() { + printf '[opencode-install] %s\n' "$*" +} + +fail() { + printf '[opencode-install] Error: %s\n' "$*" >&2 + exit 1 +} + +run_as_user() { + su - "$OPENCODE_USER" -c "$1" +} + +install_web_service() { + local password_line="# OPENCODE_SERVER_PASSWORD=" + if [[ -n "$OPENCODE_SERVER_PASSWORD" ]]; then + password_line="OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD}" + fi + + cat >/etc/default/opencode-web </etc/systemd/system/opencode-web.service </dev/null 2>&1; then + log "Creating user ${OPENCODE_USER}" + useradd --create-home --shell /bin/bash "$OPENCODE_USER" + fi + + usermod -aG sudo "$OPENCODE_USER" + install -d -m 0755 -o "$OPENCODE_USER" -g "$OPENCODE_USER" /workspace + + if [[ -x /usr/bin/fdfind && ! -e /usr/local/bin/fd ]]; then + ln -s /usr/bin/fdfind /usr/local/bin/fd + fi + + log "Installing OpenCode" + if [[ -n "$OPENCODE_VERSION" ]]; then + run_as_user "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path --version ${OPENCODE_VERSION}" + else + run_as_user "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path" + fi + + [[ -x "$OPENCODE_BIN" ]] || fail "OpenCode binary was not installed" + ln -sf "$OPENCODE_BIN" /usr/local/bin/opencode + + log "Configuring OpenCode web service" + install_web_service + + cat >/etc/profile.d/opencode-workspace.sh </dev/null || true +EOF + + log "Installed version: $(run_as_user "opencode --version" | tail -n 1)" + log "Web interface: http://$(hostname -I | awk '{print $1}'):${OPENCODE_WEB_PORT}" +} + +main "$@"