Paperclip Deployment Runbook
Owner: Operations Status: active Audience: engineering / operations Last tested: 2026-04-08
Purpose
Step-by-step guide to deploy Paperclip (the agent orchestrator) on a fresh or existing Ubuntu droplet. This covers both greenfield deployments and replacing an existing service (e.g. replacing OpenClaw with Paperclip).
Architecture
Browser → Nginx (443/80) → Paperclip (Docker, port 3000)
↓
Embedded PostgreSQL
Claude CLI / Codex / OpenCode (agent tools)
Project workspaces at /opt/cleo/paperclip/
| Component | Runs as | Port | Data |
|---|---|---|---|
| Paperclip | Docker container | 3000 (localhost) | /opt/cleo/paperclip |
| Nginx | System service | 80/443 | /etc/nginx/sites-enabled/ |
| Certbot | One-shot + cron | — | /etc/letsencrypt/ |
Prerequisites
- Ubuntu 24.04 droplet (minimum 2 vCPU / 4 GB RAM / 40 GB disk)
- Docker and Docker Compose plugin installed
- Nginx installed
- Certbot installed
- DNS A record pointing to the droplet IP
- Access to
ghcr.io/paperclip-ai/paperclip:latest(or a locally built image)
Reference deployments
| Instance | Domain | Droplet IP | Image |
|---|---|---|---|
| RateMatch | cleo.ratematch.nucleotto.com | 134.199.159.113 | local/paperclip:latest |
| TalentSupply | TBD | TBD | TBD |
Part A — New droplet (greenfield)
Step 1 — System prerequisites
apt update && apt install -y docker.io docker-compose-plugin nginx certbot python3-certbot-nginx git curl ufw
systemctl enable docker nginx
Step 2 — Create the deploy user
adduser --disabled-password --gecos "" cleo
usermod -aG docker cleo
Step 3 — Configure UFW firewall
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
Step 4 — Create directory structure
mkdir -p /opt/cleo/paperclip /opt/cleo/logs/paperclip /opt/cleo/config
chown -R cleo:cleo /opt/cleo
Step 5 — Generate secrets
APP_SECRET=$(openssl rand -hex 32)
SESSION_SECRET=$(openssl rand -hex 32)
AUTH_SECRET=$(openssl rand -hex 32)
echo "Generated secrets — save these:"
echo "APP_SECRET=$APP_SECRET"
echo "SESSION_SECRET=$SESSION_SECRET"
echo "AUTH_SECRET=$AUTH_SECRET"
Step 6 — Create Paperclip environment file
Replace <DOMAIN> with the full domain (e.g. cleo.talentsupply.nucleotto.com).
cat > /opt/cleo/config/paperclip.env << EOF
NODE_ENV=production
PORT=3000
APP_URL=https://<DOMAIN>
APP_SECRET='$APP_SECRET'
SESSION_SECRET='$SESSION_SECRET'
BETTER_AUTH_SECRET='$AUTH_SECRET'
BETTER_AUTH_BASE_URL='https://<DOMAIN>'
TRUSTED_ORIGINS='https://<DOMAIN>'
EOF
Step 7 — Pull and run Paperclip
Option A — Pre-built image (recommended):
docker pull ghcr.io/paperclip-ai/paperclip:latest
docker run -d \
--name paperclip \
--restart always \
-p 127.0.0.1:3000:3000 \
-v /opt/cleo/paperclip:/paperclip \
-v /opt/cleo/logs/paperclip:/app/logs \
--env-file /opt/cleo/config/paperclip.env \
-e HOST=0.0.0.0 \
-e SERVE_UI=true \
-e PAPERCLIP_HOME=/paperclip \
-e PAPERCLIP_INSTANCE_ID=default \
-e USER_UID=$(id -u cleo) \
-e USER_GID=$(id -g cleo) \
-e PAPERCLIP_CONFIG=/paperclip/instances/default/config.json \
-e PAPERCLIP_DEPLOYMENT_MODE=authenticated \
-e PAPERCLIP_DEPLOYMENT_EXPOSURE=private \
-e OPENCODE_ALLOW_ALL_MODELS=true \
ghcr.io/paperclip-ai/paperclip:latest
Option B — Local build:
If you have the Paperclip source (e.g. from nucleotto/paperclip-custom):
cd /opt/cleo/paperclip-src
docker build -t local/paperclip:latest .
# Then use the same docker run command above, replacing the image name
Step 8 — Verify Paperclip is running
sleep 10
docker ps | grep paperclip
curl -s http://127.0.0.1:3000/api/health
Expected response:
{"status":"ok","version":"0.3.1","deploymentMode":"authenticated",...}
Step 9 — Nginx reverse proxy
cat > /etc/nginx/sites-enabled/cleo << 'EOF'
server {
server_name <DOMAIN>;
client_max_body_size 25m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
proxy_send_timeout 300;
}
listen 80;
listen [::]:80;
}
EOF
nginx -t && systemctl reload nginx
Step 10 — SSL certificate
certbot --nginx -d <DOMAIN> --non-interactive --agree-tos -m admin@nucleotto.com
Step 11 — Set up Claude CLI credentials (for agent tools)
su - cleo
# Install Node.js (needed for Claude CLI)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
# Install Claude CLI
npm install -g @anthropic-ai/claude-code
# Log in (this creates ~/.claude/.credentials.json)
claude
# Also install inside the container
docker exec -it paperclip npm install -g @anthropic-ai/claude-code
Step 12 — Verify the full stack
# From the host
curl -s https://<DOMAIN>/api/health
# Check logs
docker logs paperclip --tail 20
Navigate to https://<DOMAIN> and create your admin account.
Part B — Replacing OpenClaw on an existing droplet
Use this when a droplet already has OpenClaw running and you want to replace it with Paperclip.
Step 1 — Document what's running
# Record current state
docker ps
ss -tlnp | grep -E ':(3000|3100|8080|443|80) '
cat /etc/nginx/sites-enabled/*
Step 2 — Back up OpenClaw data (optional)
mkdir -p /opt/cleo/backups/$(date +%Y%m%d)
# If OpenClaw has a data volume
docker inspect openclaw --format '{{json .Mounts}}' | python3 -m json.tool
# Copy any mounted data directories
cp -r /opt/cleo/openclaw /opt/cleo/backups/$(date +%Y%m%d)/openclaw 2>/dev/null
Step 3 — Stop and remove OpenClaw
docker stop openclaw
docker rm openclaw
# Remove the OpenClaw image (free disk)
docker rmi $(docker images | grep openclaw | awk '{print $3}') 2>/dev/null
Step 4 — Clean up OpenClaw nginx config (if separate)
If OpenClaw had its own nginx site config, remove or repurpose it:
# Check if openclaw has a dedicated config
ls /etc/nginx/sites-enabled/
# If it was sharing the same domain/config as Paperclip, no change needed
# If it had its own, remove it:
rm /etc/nginx/sites-enabled/openclaw 2>/dev/null
nginx -t && systemctl reload nginx
Step 5 — Deploy Paperclip
Follow Part A, Steps 4–12 above. The existing nginx, certbot, Docker, and user account can be reused.
Step 6 — Verify and clean up
# Confirm Paperclip is healthy
curl -s http://127.0.0.1:3000/api/health
# Confirm OpenClaw is fully removed
docker ps -a | grep openclaw # Should return nothing
docker images | grep openclaw # Should return nothing
# Kill zombie processes from old services
ps aux | grep defunct | head -10
# If many zombies, reboot:
# reboot
Pitfalls & lessons learned
Port 3000 vs 3100
Paperclip's default internal port is 3100 (in the Dockerfile), but the RateMatch instance maps it as 3000:3000 with PORT=3000 env var. Be consistent — pick one and set PORT accordingly.
Secrets must be pre-generated
Paperclip requires APP_SECRET, SESSION_SECRET, and BETTER_AUTH_SECRET in the environment. If these are missing, the container will start but auth will fail silently.
TRUSTED_ORIGINS must match the domain exactly
TRUSTED_ORIGINS and BETTER_AUTH_BASE_URL must be https://<exact-domain> — no trailing slash, no port number. Mismatches cause CORS and auth failures.
Container user UID/GID
The Paperclip container runs as a node user whose UID/GID is dynamically adjusted via USER_UID and USER_GID env vars to match the host user. Set these to $(id -u cleo) and $(id -g cleo) so file permissions on the bind-mounted volumes work correctly.
Zombie processes after replacing services
Replacing Docker containers (especially ones that spawn child processes) can leave zombie processes. The RateMatch droplet had 159 zombies after initial setup. A reboot clears them:
# Check for zombies
ps aux | awk '{if ($8=="Z") print}' | wc -l
# Reboot if many (safe — Docker restart policies will bring containers back)
reboot
Claude CLI credentials inside the container
If Paperclip's agents need Claude CLI access, the credentials must exist both on the host (/home/cleo/.claude/.credentials.json) and inside the container. The bind mount at /paperclip maps to /opt/cleo/paperclip on the host — Claude looks for credentials at $HOME/.claude/, which inside the container is /paperclip/.claude/.
Maintenance
Restart Paperclip
docker restart paperclip
View logs
docker logs paperclip --tail 50
# or
tail -50 /opt/cleo/logs/paperclip/*.log
Update Paperclip
docker pull ghcr.io/paperclip-ai/paperclip:latest
docker stop paperclip && docker rm paperclip
# Re-run the docker run command from Step 7
Check health
curl -s http://127.0.0.1:3000/api/health | python3 -m json.tool
docker ps | grep paperclip
Full stack check (Paperclip + Otto)
docker ps
systemctl status hermes-gateway 2>/dev/null
ss -tlnp | grep -E ':(3000|8080|8642|443|80) '
curl -s http://127.0.0.1:3000/api/health
curl -s http://127.0.0.1:8642/v1/models -H "Authorization: Bearer <API_KEY>" 2>/dev/null