Created
January 25, 2026 00:41
-
-
Save shekohex/efd7655fd6c8fd80075279ecb692dcbb to your computer and use it in GitHub Desktop.
Full, robust setup for Proxmox LXC (unprivileged) to make: root's `systemctl --user` work reliably, root user manager starts at boot, env available in pct enter shells (bash non-login) + login shells, unit name mismatch fixed WITHOUT symlinks (gateway unit is a real file copy)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # clawdbot-lxc-root-user-systemd-setup.sh | |
| # | |
| # Full, robust setup for Proxmox LXC (unprivileged) to make: | |
| # - root's `systemctl --user` work reliably | |
| # - root user manager starts at boot | |
| # - env available in pct enter shells (bash non-login) + login shells | |
| # - unit name mismatch fixed WITHOUT symlinks (gateway unit is a real file copy) | |
| # | |
| # Usage: | |
| # sudo bash clawdbot-lxc-root-user-systemd-setup.sh | |
| # | |
| # Optional overrides: | |
| # EXPECTED_UNIT=clawdbot-gateway.service ACTUAL_UNIT=clawdbot-node.service bash clawdbot-lxc-root-user-systemd-setup.sh | |
| # | |
| set -Eeuo pipefail | |
| EXPECTED_UNIT="${EXPECTED_UNIT:-clawdbot-gateway.service}" | |
| ACTUAL_UNIT="${ACTUAL_UNIT:-clawdbot-node.service}" | |
| LOG_DIR="/var/log/clawdbot-setup" | |
| TS="$(date +'%Y%m%d-%H%M%S')" | |
| LOG_FILE="${LOG_DIR}/setup-${TS}.log" | |
| ROOT_UNIT_DIR="/root/.config/systemd/user" | |
| EXPECTED_PATH="${ROOT_UNIT_DIR}/${EXPECTED_UNIT}" | |
| ACTUAL_PATH="${ROOT_UNIT_DIR}/${ACTUAL_UNIT}" | |
| RUN_USER_DIR="/run/user/0" | |
| BUS_PATH="${RUN_USER_DIR}/bus" | |
| TMPFILES_CONF="/etc/tmpfiles.d/root-runtime.conf" | |
| BOOT_SERVICE="/etc/systemd/system/root-user-manager.service" | |
| PROFILED_FILE="/etc/profile.d/root-user-bus.sh" | |
| BASHBASHRC_FILE="/etc/bash.bashrc" | |
| mkdir -p "$LOG_DIR" | |
| touch "$LOG_FILE" | |
| chmod 600 "$LOG_FILE" || true | |
| log() { | |
| local lvl="$1"; shift | |
| printf '%s [%s] %s\n' "$(date +'%F %T')" "$lvl" "$*" | tee -a "$LOG_FILE" >&2 | |
| } | |
| run() { | |
| log "CMD" "$*" | |
| "$@" 2>&1 | tee -a "$LOG_FILE" | |
| } | |
| die() { | |
| log "ERROR" "$*" | |
| log "ERROR" "Log: $LOG_FILE" | |
| exit 1 | |
| } | |
| need_root() { | |
| [[ "$(id -u)" -eq 0 ]] || die "Run this script as root." | |
| } | |
| ensure_block_in_file() { | |
| # ensure_block_in_file <file> <marker> <block> | |
| local file="$1" | |
| local marker="$2" | |
| local block="$3" | |
| if [[ ! -f "$file" ]]; then | |
| run install -Dm0644 /dev/null "$file" | |
| fi | |
| if grep -qF "$marker" "$file"; then | |
| log "INFO" "Already configured: $file" | |
| return 0 | |
| fi | |
| log "INFO" "Patching: $file" | |
| { | |
| echo "" | |
| echo "$block" | |
| } >> "$file" | |
| } | |
| banner() { | |
| cat <<EOF | tee -a "$LOG_FILE" | |
| ============================================================ | |
| Clawdbot Root LXC Setup | |
| - Fix root systemd --user (user@0 + user dbus) | |
| - Make env available in pct enter shells (bashrc + profile.d) | |
| - Fix unit name mismatch (expected -> actual) WITHOUT symlinks | |
| Expected unit: ${EXPECTED_UNIT} | |
| Actual unit: ${ACTUAL_UNIT} | |
| Log file: ${LOG_FILE} | |
| ============================================================ | |
| EOF | |
| } | |
| show_next_steps() { | |
| cat <<EOF | tee -a "$LOG_FILE" | |
| ------------------------------------------------------------ | |
| Done ✅ | |
| Important: | |
| - Your CURRENT shell won't gain env vars retroactively. | |
| - Open a new shell or run: | |
| exec bash | |
| Verify: | |
| echo "\$XDG_RUNTIME_DIR" | |
| systemctl --user status | |
| Service: | |
| systemctl --user status ${EXPECTED_UNIT} -n 80 | |
| journalctl --user -u ${EXPECTED_UNIT} -e --no-pager | |
| ------------------------------------------------------------ | |
| EOF | |
| } | |
| need_root | |
| banner | |
| log "INFO" "Step 1/8: Ensure /run/user/0 exists at boot (tmpfiles) + right now" | |
| run bash -c "cat >'${TMPFILES_CONF}' <<'EOF' | |
| d /run/user/0 0700 root root - | |
| EOF" | |
| run systemd-tmpfiles --create | |
| run mkdir -p "$RUN_USER_DIR" | |
| run chmod 700 "$RUN_USER_DIR" | |
| log "INFO" "Step 2/8: Ensure root user manager ([email protected]) starts at boot" | |
| run bash -c "cat >'${BOOT_SERVICE}' <<'EOF' | |
| [Unit] | |
| Description=Start root user systemd manager (user@0) | |
| After=systemd-user-sessions.service | |
| Wants=systemd-user-sessions.service | |
| [Service] | |
| Type=oneshot | |
| RemainAfterExit=yes | |
| ExecStart=/bin/systemctl start [email protected] | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF" | |
| log "INFO" "Step 3/8: Enable boot helper + start [email protected] now" | |
| run systemctl daemon-reload | |
| run systemctl enable --now root-user-manager.service | |
| run systemctl start [email protected] || true | |
| log "INFO" "Step 4/8: Install env exports for BOTH login shells and pct-enter bash shells" | |
| ENV_BLOCK=$'# >>> clawdbot root user bus >>>\n# Make systemctl --user work in LXC shells (pct enter / lxc-attach)\nif [ \"$(id -u)\" -eq 0 ]; then\n export XDG_RUNTIME_DIR=/run/user/0\n export DBUS_SESSION_BUS_ADDRESS=\"unix:path=/run/user/0/bus\"\nfi\n# <<< clawdbot root user bus <<<' | |
| # login shells | |
| if [[ ! -f "$PROFILED_FILE" ]] || ! grep -qF '# >>> clawdbot root user bus >>>' "$PROFILED_FILE"; then | |
| run bash -c "cat >'${PROFILED_FILE}' <<'EOF' | |
| ${ENV_BLOCK} | |
| EOF" | |
| run chmod 0644 "$PROFILED_FILE" | |
| else | |
| log "INFO" "Already configured: $PROFILED_FILE" | |
| fi | |
| # interactive non-login bash shells (common for pct enter) | |
| ensure_block_in_file "$BASHBASHRC_FILE" "# >>> clawdbot root user bus >>>" "$ENV_BLOCK" | |
| log "INFO" "Step 5/8: Ensure the user bus exists at ${BUS_PATH}" | |
| if [[ ! -S "$BUS_PATH" ]]; then | |
| log "WARN" "Bus not found yet; restarting [email protected]" | |
| run systemctl restart [email protected] || true | |
| fi | |
| if [[ ! -S "$BUS_PATH" ]]; then | |
| log "ERROR" "Still no user bus at ${BUS_PATH}. Diagnostics:" | |
| run systemctl status [email protected] -n 150 || true | |
| run ls -la "$RUN_USER_DIR" || true | |
| die "Root user bus not available; cannot use systemctl --user." | |
| fi | |
| log "INFO" "Step 6/8: Verify systemctl --user works (explicit env, should pass)" | |
| run env XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/0/bus" systemctl --user status | |
| log "INFO" "Step 7/8: Fix unit name mismatch WITHOUT symlink (make expected a real unit file)" | |
| run mkdir -p "$ROOT_UNIT_DIR" | |
| run chmod 700 "/root/.config" || true | |
| run chmod 700 "$ROOT_UNIT_DIR" || true | |
| if [[ ! -f "$ACTUAL_PATH" ]]; then | |
| log "ERROR" "Actual unit not found at: ${ACTUAL_PATH}" | |
| log "INFO" "Files in ${ROOT_UNIT_DIR}:" | |
| run ls -la "$ROOT_UNIT_DIR" || true | |
| die "Install the app first so it creates ${ACTUAL_UNIT}, then re-run this script." | |
| fi | |
| # Remove symlink/old file if it's a symlink (enable refuses symlinks/aliases) | |
| if [[ -L "$EXPECTED_PATH" ]]; then | |
| log "INFO" "Removing symlinked expected unit (systemctl enable refuses linked units): ${EXPECTED_PATH}" | |
| run rm -f "$EXPECTED_PATH" | |
| fi | |
| # If missing, create a REAL unit file by copying the actual unit | |
| if [[ ! -f "$EXPECTED_PATH" ]]; then | |
| log "INFO" "Creating real unit file: ${EXPECTED_PATH} (copy of ${ACTUAL_UNIT})" | |
| run install -Dm0644 "$ACTUAL_PATH" "$EXPECTED_PATH" | |
| else | |
| log "INFO" "Expected unit already exists as a real file: ${EXPECTED_PATH}" | |
| fi | |
| log "INFO" "Step 8/8: Reload user daemon and enable/start expected unit" | |
| run env XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/0/bus" systemctl --user daemon-reload | |
| run env XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/0/bus" systemctl --user enable --now "$EXPECTED_UNIT" | |
| run env XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/0/bus" systemctl --user status "$EXPECTED_UNIT" -n 80 | |
| log "INFO" "Installed clawdbot units:" | |
| run env XDG_RUNTIME_DIR=/run/user/0 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/0/bus" systemctl --user list-unit-files | grep -i clawdbot || true | |
| show_next_steps |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment