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...

Written by Muhammed Musthafa

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, workspace, and hourly database backups need room. Without backup pruning, expect ENOSPC errors within weeks. See Section 10.)

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
StandardInput=null
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.

Node version drift: The PATH above hardcodes v24.14.0. When you update Node via nvm, the version directory changes and the service will silently fail with "command not found" errors. After any Node update, run which node to get the new path and update the PATH line in the service file accordingly. Then sudo systemctl daemon-reload && sudo systemctl restart paperclip.

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.


9a. Diagnostics Cheat Sheet

Quick commands to check what's happening on your Paperclip instance:

journalctl -u paperclip -f              # live service logs (Ctrl+C to stop)
curl localhost:3100/api/health           # health check (should return 200)
df -h                                    # disk usage — watch for full disks
du -sh ~/.paperclip/instances/default/data/backups/   # backup folder size

Stale paperclip.log warning: After systemd restarts the service, journalctl is the source of truth for logs. If you have a paperclip.log file from an earlier tmux session, it will still contain old output and can be misleading. Always check journalctl -u paperclip for current logs.


9b. Updating to a New Release

To update Paperclip to a newer tagged release:

cd ~/paperclip
git fetch --tags
git checkout <tag>           # e.g. git checkout v0.4.2
pnpm install
sudo systemctl restart paperclip
curl localhost:3100/api/health           # verify it's running

For the full walkthrough with troubleshooting steps, see the standalone update guide.


10. Backup Retention

Paperclip creates hourly database backups in ~/.paperclip/instances/default/data/backups/. These can grow to ~3 GB/day and will silently fill your disk if you don't clean them up.

Built-in retention: Paperclip now includes automatic backup retention — the server reports DB Backup enabled (every 60m, keep 30d) at startup. This handles the common case. The cron job below is still a useful safety net if you want a shorter retention window (e.g. 7 days) or want extra assurance that old backups are pruned.

Follow the steps below to check how much space backups are using, safely remove old ones, and set up automatic cleanup so you never have to think about it again.

Step 1: Check your current backup size

This command shows you how much disk space your backups are using. It adds up every file in the backups folder and prints a single human-readable number (e.g. "1.2G"):

du -sh ~/.paperclip/instances/default/data/backups/

If the number is small (under a few hundred MB), you're fine for now. If it's multiple gigabytes, move on to Step 2.

Step 2: Do a dry run to see what would be deleted

Before deleting anything, preview which files are older than 7 days. The find command searches through a folder for files matching certain criteria. Here, -type f means "only files" (not folders), and -mtime +7 means "last modified more than 7 days ago." The -print flag tells it to just list the files without changing anything:

find ~/.paperclip/instances/default/data/backups/ -type f -mtime +7 -print

Look at the output. You should see a list of .sqlite or similar backup files with dates in their names. If the list looks reasonable, proceed to delete them by swapping -print for -delete:

find ~/.paperclip/instances/default/data/backups/ -type f -mtime +7 -delete

Run the du command from Step 1 again to confirm the space was freed.

Step 3: Set up automatic cleanup

Rather than remembering to clean up manually, you can schedule it to run every day using cron — a built-in Linux tool that runs commands on a schedule.

Open your cron schedule for editing:

crontab -e

This opens a text file in your terminal editor. Scroll to the bottom and add this line:

0 5 * * * find /home/ec2-user/.paperclip/instances/default/data/backups/ -type f -mtime +7 -delete

Here's what each part means:

  • 0 5 * * * — run at 5:00 AM UTC every day (the five fields are: minute, hour, day-of-month, month, day-of-week, where * means "every")
  • The rest is the same find and -delete command from Step 2

Save and close the file. Cron will now automatically delete backups older than 7 days, every day at 5 AM UTC.

To verify your cron job was saved, run:

crontab -l

You should see your new line in the output.

macOS users: launchd is the native scheduler on macOS, but running the find command from Step 2 manually on a periodic basis works fine too.


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.