Guides

How to Run Paperclip on EC2 with Tailscale HTTPS

Run Paperclip on an EC2 instance (or a Mac) and access it over Tailscale with real HTTPS — no port forwarding, no self-signed certs, no public internet expos...

How to Run Paperclip on EC2 with Tailscale HTTPS

Run Paperclip on an EC2 instance (or a Mac) and access it over Tailscale with real HTTPS — no port forwarding, no self-signed certs, no public internet exposure. Linux and macOS instructions included throughout.

1. EC2 Instance

  • AMI: Amazon Linux 2023
  • Type: t4g.large (ARM, cheaper)
  • Storage: 100 GB (40 GB will run out — Paperclip's toolchain and workspace need room)

Connect via EC2 Instance Connect.

If you need to expand storage later: after resizing the EBS volume in the AWS console, the filesystem won't grow automatically. SSH in and run:

lsblk                              # confirm disk sees new size
sudo growpart /dev/nvme0n1 1       # grow the partition
sudo xfs_growfs /                  # extend the filesystem
df -h                              # confirm

If growpart fails with a stale GPT backup header (common on second+ resize), fix it with:

sudo gdisk /dev/nvme0n1            # type 'w', then 'Y' twice to rewrite headers
sudo sfdisk /dev/nvme0n1 --move-data --no-reread -N 1 <<< ", +"
sudo partprobe /dev/nvme0n1
sudo xfs_growfs /

2. Tailscale

In the Tailscale admin console, turn on MagicDNS and Serve.

Install Tailscale: https://tailscale.com/docs/install/linux

Authenticate and join your tailnet, then run:

sudo tailscale set --hostname paperclip
sudo tailscale set --ssh
sudo tailscale serve --bg 3100
sudo hostnamectl set-hostname paperclip   # optional, sets the Linux hostname too

This tells Tailscale to proxy HTTPS traffic on your *.ts.net hostname to localhost:3100. Tailscale provisions a real TLS certificate via Let's Encrypt automatically — any device on your tailnet can reach the app at https://paperclip.your-tailnet.ts.net with no certificate warnings.

This is tailnet-only by default. No --funnel flag means no public internet exposure.

To check what's being served: tailscale serve status. To tear it down: tailscale serve --https=443 off.

The Tailscale hostname can revert to the default AWS IP-based name after a reboot. The tailscale set --hostname command is persistent, but running hostnamectl as well keeps things consistent.

Serving HTTPS from a Mac instead

If you're running Paperclip on a MacBook, the same tailscale serve command works:

tailscale serve --https=443 3100

Your Mac's Tailscale hostname gets a real TLS cert and proxies to your local port. Any phone, laptop, or server on your tailnet hits it over HTTPS with zero config.

Keep-awake note: macOS drops the Tailscale connection when the machine sleeps. If you need it always available, disable sleep via System Settings → Energy, run caffeinate -s, or use a utility like Amphetamine. Closed-lid setups require clamshell mode or a keep-awake dongle.


3. tmux and Node

Linux (EC2):

sudo dnf update -y
sudo dnf install tmux -y

macOS:

brew install tmux

Install Node via nvm and pnpm (same on both):

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
source ~/.bashrc   # or source ~/.zshrc on macOS
nvm install --lts
npm install -g pnpm@latest-10

3a. Additional CLI Tools

GitHub CLI and git config:

Linux (EC2):

sudo dnf install -y gh

macOS:

brew install gh

Then on both:

gh auth login
git config --global user.name "Your Name"
git config --global user.email "your@email.com"

Cloudflare Wrangler (same on both):

pnpm install -g wrangler

The Wrangler auth token gets passed through the CTO agent config — no need to run wrangler login on the server.


4. Claude Code (Headless)

curl -fsSL https://claude.ai/install.sh | bash

On your local machine (with a browser):

claude setup-token

Get your account info:

cat ~/.claude.json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(json.dumps(d.get('oauthAccount'), indent=2))"

Back on the EC2 instance, add your token (replace with the real token):

echo 'export CLAUDE_CODE_OAUTH_TOKEN="sk-ant-oat01-your-token-here"' >> ~/.bashrc

Create ~/.claude.json on the EC2 instance with your account details from the step above:

{
  "hasCompletedOnboarding": true,
  "lastOnboardingVersion": "2.1.76",
  "oauthAccount": {
    "accountUuid": "your-account-uuid",
    "emailAddress": "your@email.com",
    "organizationUuid": "your-organization-uuid"
  }
}

Test:

source ~/.bashrc
claude

If it works, type /quit or /exit and continue.


5. Paperclip — Clone and Onboard

git clone https://github.com/paperclipai/paperclip.git
cd paperclip
pnpm install
pnpm paperclipai onboard --yes

When onboarding finishes, stop the process (Ctrl+C).


6. Paperclip Config for Tailscale

Edit the config:

nano ~/.paperclip/instances/default/config.json

Set the server section so it listens on all interfaces and allows your Tailscale hostname. Get the full hostname from Tailscale admin → your machine → full domain (e.g. paperclip.kingfisher-halibut.ts.net):

"server": {
  "deploymentMode": "authenticated",
  "exposure": "private",
  "host": "0.0.0.0",
  "port": 3100,
  "allowedHostnames": ["paperclip.kingfisher-halibut.ts.net"],
  "serveUi": true
}

Save and exit (Ctrl+O, Enter, Ctrl+X).


7. Claim the Board

From the paperclip repo:

cd ~/paperclip
pnpm paperclipai run

In the logs, find a board-claim URL like:

http://localhost:3100/board-claim/581087dd...?code=70b9a720...

Replace localhost with your Tailscale hostname:

https://paperclip.kingfisher-halibut.ts.net/board-claim/581087dd...?code=70b9a720...

Since Tailscale Serve is handling HTTPS, use https:// without a port number. Open this URL in a browser on a device that's on the same tailnet, create an account, and claim the board. Then stop Paperclip (Ctrl+C).


8. Run Paperclip as a Service

tmux works for quick testing but doesn't survive reboots. Use a system service instead.

Linux (EC2) — systemd

sudo tee /etc/systemd/system/paperclip.service << 'EOF'
[Unit]
Description=Paperclip AI
After=network.target tailscaled.service

[Service]
Type=simple
User=ec2-user
WorkingDirectory=/home/ec2-user/paperclip
ExecStart=/home/ec2-user/.local/share/pnpm/pnpm paperclipai run
Restart=always
RestartSec=10
Environment=HOME=/home/ec2-user
Environment=PATH=/home/ec2-user/.nvm/versions/node/v24.14.0/bin:/home/ec2-user/.local/share/pnpm:/home/ec2-user/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=CLAUDE_CODE_OAUTH_TOKEN=your-token-here

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable paperclip
sudo systemctl start paperclip

Important: systemd doesn't source .bashrc, so the PATH must explicitly include every directory containing tools Paperclip calls — node, pnpm, claude, gh, wrangler. Same for the Claude OAuth token. Without this, Paperclip runs but gets "command not found" errors.

Better: use a systemd override for secrets. Instead of putting your token directly in the main unit file, use an override:

sudo systemctl edit paperclip

This opens an editor. Add:

[Service]
Environment=CLAUDE_CODE_OAUTH_TOKEN=your-actual-token

Then remove the CLAUDE_CODE_OAUTH_TOKEN line from the main unit file. The override lives in /etc/systemd/system/paperclip.service.d/override.conf and won't be clobbered if you recreate the base service file.

sudo systemctl daemon-reload
sudo systemctl restart paperclip

Check status: sudo systemctl status paperclip View logs: journalctl -u paperclip -f

macOS — launchd

Create a plist:

cat > ~/Library/LaunchAgents/com.paperclip.ai.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.paperclip.ai</string>
  <key>WorkingDirectory</key>
  <string>/Users/YOUR_USER/paperclip</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/YOUR_USER/.local/share/pnpm/pnpm</string>
    <string>paperclipai</string>
    <string>run</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>HOME</key>
    <string>/Users/YOUR_USER</string>
    <key>PATH</key>
    <string>/Users/YOUR_USER/.nvm/versions/node/v24.14.0/bin:/Users/YOUR_USER/.local/share/pnpm:/Users/YOUR_USER/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    <key>CLAUDE_CODE_OAUTH_TOKEN</key>
    <string>your-token-here</string>
  </dict>
  <key>RunAtLoad</key>
  <true/>
  <key>KeepAlive</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/tmp/paperclip.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/paperclip.err</string>
</dict>
</plist>
EOF

Replace YOUR_USER with your macOS username, then load it:

launchctl load ~/Library/LaunchAgents/com.paperclip.ai.plist

Same rule as systemd: launchd doesn't source your shell profile, so the PATH must list every tool directory explicitly.

Check status: launchctl list | grep paperclip View logs: tail -f /tmp/paperclip.log Stop: launchctl unload ~/Library/LaunchAgents/com.paperclip.ai.plist

Quick Testing with tmux

cd ~/paperclip
tmux new
pnpm paperclipai run

Detach: Ctrl+B, then d. Reattach: tmux attach. Just know it won't survive a reboot.


9. Workspace and CEO Bootstrap

mkdir ~/paperclip-workspace

In the Paperclip dashboard, log in and bootstrap CEO.


Key Paths Reference

These are the default install locations on an Amazon Linux 2023 EC2 instance. Use this table when building the PATH for systemd or launchd — every directory containing a tool Paperclip calls must be listed.

Tool Linux (EC2) macOS (Homebrew)
node ~/.nvm/versions/node/v24.14.0/bin/node ~/.nvm/versions/node/v24.14.0/bin/node
pnpm ~/.local/share/pnpm/pnpm ~/.local/share/pnpm/pnpm
claude ~/.local/bin/claude ~/.local/bin/claude
wrangler ~/.local/share/pnpm/wrangler ~/.local/share/pnpm/wrangler
gh /usr/bin/gh /opt/homebrew/bin/gh

Node version will vary — check with which node and adjust accordingly.

Ready to build?

Go from idea to launched product in a week with AI-assisted development.