Last active
April 25, 2026 21:48
-
-
Save smartynov/8908a5fefefe2c93bfe72054e5f9aeb8 to your computer and use it in GitHub Desktop.
Paperclip auto install
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 | |
| # setup-paperclip.sh — Install Paperclip AI with Caddy reverse proxy, basic auth, and systemd | |
| # | |
| # Usage: | |
| # sudo bash setup-paperclip.sh <domain> <auth_user> <auth_password> <run_as_user> | |
| # | |
| # Example: | |
| # sudo bash setup-paperclip.sh paperclip.example.com admin 's3cureP@ss!' deploy | |
| # | |
| # What this does: | |
| # 1. Installs Node.js 22 LTS + pnpm (via corepack) | |
| # 2. Installs Caddy (auto-HTTPS via Let's Encrypt) | |
| # 3. Clones Paperclip into ~<run_as_user>/paperclip/ | |
| # 4. Runs onboard to create config + embedded PostgreSQL | |
| # 5. Creates a systemd service (User=<run_as_user>, auto-restart, starts on boot) | |
| # 6. Configures Caddy as a reverse proxy with HTTP basic auth | |
| # 7. Disables Paperclip telemetry | |
| # | |
| # Paperclip runs in local_trusted mode (localhost only). | |
| # Caddy handles HTTPS termination and basic auth in front of it. | |
| # | |
| # The service runs as <run_as_user> so that agent CLIs (Claude Code, Codex) | |
| # share the same user, HOME, PATH, and auth tokens. After install: | |
| # | |
| # 1. SSH as <run_as_user> and install an agent runtime: | |
| # Claude Code: curl -fsSL https://claude.ai/install.sh | bash && claude login | |
| # Codex: npm i -g @openai/codex && codex | |
| # 2. Create a workspace directory, e.g. ~/workspaces/my-project/ | |
| # 3. Open https://<domain>, enter basic auth, complete the onboarding wizard | |
| # 4. When creating an agent, set Working Directory to ~/workspaces/my-project/ | |
| # | |
| # Prerequisites: | |
| # - Ubuntu 22.04+ or Debian 12+ | |
| # - DNS A record for <domain> pointing to this server | |
| # - Ports 80 and 443 open (Let's Encrypt + HTTPS) | |
| # - <run_as_user> must already exist on the system | |
| # - Run this script as root | |
| # | |
| # Management: | |
| # systemctl status paperclip | |
| # systemctl restart paperclip | |
| # journalctl -u paperclip -f | |
| # | |
| # Update: | |
| # cd ~/paperclip && git pull && pnpm install && sudo systemctl restart paperclip | |
| set -euo pipefail | |
| # ----- Arguments ----- | |
| DOMAIN="${1:?Usage: sudo bash setup-paperclip.sh <domain> <auth_user> <auth_password> <run_as_user>}" | |
| AUTH_USER="${2:?Missing argument: auth_user}" | |
| AUTH_PASS="${3:?Missing argument: auth_password}" | |
| RUN_USER="${4:?Missing argument: run_as_user (e.g. deploy, ubuntu, sm)}" | |
| PAPERCLIP_PORT=3100 | |
| if [ "$(id -u)" -ne 0 ]; then | |
| echo "Error: this script must be run as root." | |
| exit 1 | |
| fi | |
| # Resolve user's home directory | |
| if ! id "$RUN_USER" &>/dev/null; then | |
| echo "Error: user '${RUN_USER}' does not exist." | |
| exit 1 | |
| fi | |
| USER_HOME=$(getent passwd "$RUN_USER" | cut -d: -f6) | |
| PAPERCLIP_DIR="${USER_HOME}/paperclip" | |
| # Suppress corepack download prompts | |
| export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |
| echo "============================================" | |
| echo " Paperclip setup" | |
| echo " Domain: ${DOMAIN}" | |
| echo " Auth: ${AUTH_USER}" | |
| echo " Run as: ${RUN_USER} (${USER_HOME})" | |
| echo " Install: ${PAPERCLIP_DIR}" | |
| echo "============================================" | |
| echo "" | |
| # ----- 1. Node.js 22 LTS ----- | |
| echo "--- [1/7] Node.js ---" | |
| if command -v node &>/dev/null && node -v | grep -qE "^v2[2-9]"; then | |
| echo "OK: Node.js $(node -v) already installed" | |
| else | |
| curl -fsSL https://deb.nodesource.com/setup_22.x | bash - | |
| apt-get install -y nodejs | |
| echo "OK: Node.js $(node -v) installed" | |
| fi | |
| corepack enable | |
| corepack prepare pnpm@latest --activate 2>/dev/null || true | |
| echo "OK: pnpm $(pnpm -v)" | |
| echo "" | |
| # ----- 2. Caddy ----- | |
| echo "--- [2/7] Caddy ---" | |
| if command -v caddy &>/dev/null; then | |
| echo "OK: Caddy already installed" | |
| else | |
| apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl | |
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ | |
| | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg | |
| curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ | |
| | tee /etc/apt/sources.list.d/caddy-stable.list | |
| apt-get update | |
| apt-get install -y caddy | |
| echo "OK: Caddy installed" | |
| fi | |
| echo "" | |
| # ----- 3. Clone + install ----- | |
| echo "--- [3/7] Paperclip ---" | |
| if [ -d "${PAPERCLIP_DIR}/.git" ]; then | |
| echo "Updating existing installation..." | |
| cd "${PAPERCLIP_DIR}" | |
| sudo -u "$RUN_USER" git pull | |
| else | |
| echo "Cloning repository..." | |
| sudo -u "$RUN_USER" git clone https://github.com/paperclipai/paperclip.git "${PAPERCLIP_DIR}" | |
| cd "${PAPERCLIP_DIR}" | |
| fi | |
| echo "Installing dependencies (this may take a few minutes)..." | |
| sudo -u "$RUN_USER" \ | |
| COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ | |
| pnpm install | |
| echo "OK: Paperclip ready" | |
| echo "" | |
| # ----- 4. Workspaces directory ----- | |
| echo "--- [4/7] Workspaces ---" | |
| WORKSPACES="${USER_HOME}/workspaces" | |
| if [ ! -d "$WORKSPACES" ]; then | |
| sudo -u "$RUN_USER" mkdir -p "$WORKSPACES" | |
| fi | |
| echo "OK: ${WORKSPACES}" | |
| echo "" | |
| # ----- 5. Onboard + allowed hostname ----- | |
| echo "--- [5/7] Onboard ---" | |
| CONFIG_FILE="${USER_HOME}/.paperclip/instances/default/config.json" | |
| if [ -f "$CONFIG_FILE" ]; then | |
| echo "OK: config already exists, skipping onboard" | |
| else | |
| echo "Running onboard (creates config + embedded PostgreSQL)..." | |
| # onboard --yes creates config then starts the server (blocking). | |
| # Run in background, wait for health check, then stop it. | |
| sudo -u "$RUN_USER" bash -c " | |
| cd ${PAPERCLIP_DIR} && \ | |
| COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ | |
| PAPERCLIP_TELEMETRY_DISABLED=1 \ | |
| DO_NOT_TRACK=1 \ | |
| HOME=${USER_HOME} \ | |
| pnpm paperclipai onboard --yes | |
| " & | |
| ONBOARD_PID=$! | |
| echo "Waiting for server to start..." | |
| for i in $(seq 1 45); do | |
| if curl -sf http://127.0.0.1:${PAPERCLIP_PORT}/api/health >/dev/null 2>&1; then | |
| echo "OK: server is running" | |
| break | |
| fi | |
| if ! kill -0 "$ONBOARD_PID" 2>/dev/null; then | |
| echo "ERROR: onboard process exited unexpectedly." | |
| echo " Try manually: sudo -u ${RUN_USER} bash -c 'cd ${PAPERCLIP_DIR} && pnpm paperclipai onboard --yes'" | |
| exit 1 | |
| fi | |
| if [ "$i" -eq 45 ]; then | |
| echo "ERROR: server didn't start after 90s." | |
| kill "$ONBOARD_PID" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| # Stop the onboard server (systemd will take over) | |
| kill "$ONBOARD_PID" 2>/dev/null || true | |
| wait "$ONBOARD_PID" 2>/dev/null || true | |
| sleep 2 | |
| echo "OK: onboard complete, server stopped" | |
| fi | |
| # Add domain as allowed hostname (local_trusted mode blocks unknown hosts) | |
| echo "Adding '${DOMAIN}' as allowed hostname..." | |
| sudo -u "$RUN_USER" bash -c " | |
| cd ${PAPERCLIP_DIR} && \ | |
| COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ | |
| HOME=${USER_HOME} \ | |
| pnpm paperclipai allowed-hostname ${DOMAIN} | |
| " || echo "WARNING: could not add hostname (add manually: pnpm paperclipai allowed-hostname ${DOMAIN})" | |
| echo "" | |
| # ----- 6. Systemd service ----- | |
| echo "--- [6/7] Systemd service ---" | |
| cat > /etc/systemd/system/paperclip.service << EOF | |
| [Unit] | |
| Description=Paperclip AI Agent Orchestration | |
| After=network.target | |
| [Service] | |
| Type=simple | |
| User=${RUN_USER} | |
| WorkingDirectory=${PAPERCLIP_DIR} | |
| Environment=HOME=${USER_HOME} | |
| Environment=COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | |
| Environment=PAPERCLIP_TELEMETRY_DISABLED=1 | |
| Environment=DO_NOT_TRACK=1 | |
| Environment=NODE_ENV=production | |
| Environment=PATH=${USER_HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin | |
| ExecStart=/usr/bin/pnpm paperclipai run | |
| Restart=always | |
| RestartSec=5 | |
| LimitNOFILE=65536 | |
| TimeoutStopSec=30 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| systemctl daemon-reload | |
| systemctl enable paperclip | |
| echo "OK: systemd service created and enabled" | |
| echo "" | |
| # ----- 7. Caddy config ----- | |
| echo "--- [7/7] Caddy reverse proxy ---" | |
| HASHED_PASS=$(caddy hash-password --plaintext "$AUTH_PASS") | |
| cat > /etc/caddy/Caddyfile << EOF | |
| ${DOMAIN} { | |
| basicauth * { | |
| ${AUTH_USER} ${HASHED_PASS} | |
| } | |
| reverse_proxy localhost:${PAPERCLIP_PORT} | |
| encode gzip | |
| log { | |
| output file /var/log/caddy/${DOMAIN}.log { | |
| roll_size 10mb | |
| roll_keep 5 | |
| } | |
| } | |
| } | |
| EOF | |
| mkdir -p /var/log/caddy | |
| systemctl restart caddy | |
| echo "OK: Caddy configured for https://${DOMAIN}" | |
| echo "" | |
| # ----- Start ----- | |
| echo "--- Starting Paperclip ---" | |
| systemctl start paperclip | |
| for i in $(seq 1 30); do | |
| if curl -sf http://127.0.0.1:${PAPERCLIP_PORT}/api/health >/dev/null 2>&1; then | |
| echo "OK: Paperclip is running (health check passed)" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "WARNING: health check didn't pass after 60s." | |
| echo " Check logs: journalctl -u paperclip -f" | |
| fi | |
| sleep 2 | |
| done | |
| echo "" | |
| echo "============================================" | |
| echo " Installation complete!" | |
| echo "============================================" | |
| echo "" | |
| echo " URL: https://${DOMAIN}" | |
| echo " User: ${AUTH_USER}" | |
| echo " Pass: (as provided)" | |
| echo "" | |
| echo " Data: ${USER_HOME}/.paperclip/instances/default/" | |
| echo " Repo: ${PAPERCLIP_DIR}" | |
| echo " Work: ${WORKSPACES}/" | |
| echo "" | |
| echo " Telemetry: DISABLED" | |
| echo "" | |
| echo "--------------------------------------------" | |
| echo " NEXT STEPS (as ${RUN_USER}, not root):" | |
| echo "" | |
| echo " 1. Install an agent runtime:" | |
| echo "" | |
| echo " Claude Code:" | |
| echo " curl -fsSL https://claude.ai/install.sh | bash" | |
| echo " claude login" | |
| echo "" | |
| echo " Codex (OpenAI):" | |
| echo " npm i -g @openai/codex" | |
| echo " codex" | |
| echo "" | |
| echo " 2. Create a workspace:" | |
| echo " mkdir -p ~/workspaces/my-project" | |
| echo "" | |
| echo " 3. Open https://${DOMAIN} and complete" | |
| echo " the onboarding wizard. When creating" | |
| echo " an agent, set Working Directory to:" | |
| echo " ${WORKSPACES}/my-project" | |
| echo "--------------------------------------------" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment