Skip to main content

Otto Deployment Runbook

Owner: Rex Status: active Audience: engineering / operations Last tested: 2026-04-08 (RateMatch droplet)

Purpose

Step-by-step guide to deploy an Otto instance (Open WebUI + Hermes Agent + Paperclip) on a fresh Ubuntu droplet. This is the same stack that powers otto.nucleotto.com — replicated for client or internal environments.

Architecture

Browser → Open WebUI (Docker, port 8080)

Hermes API Server (port 8642, OpenAI-compatible)

Claude API (via ~/.claude/.credentials.json OAuth token)

Paperclip (Docker, port 3000) — agent orchestration
ComponentRuns asPortData
PaperclipDocker container3000 (localhost)/opt/cleo/paperclip
Hermes Agentsystemd service (gateway mode)8642 (0.0.0.0)~/.hermes/
Open WebUIDocker container8080 (localhost)/opt/cleo/open-webui
NginxSystem service80/443/etc/nginx/sites-enabled/

Prerequisites

  • Ubuntu 24.04 droplet (minimum 2 vCPU / 8 GB RAM / 40 GB disk)
  • Docker installed
  • Nginx installed
  • Certbot installed
  • A user account (e.g. cleo) with:
    • GitHub CLI (gh) authenticated with access to Nucleotto/hermes-agent
    • Claude CLI credentials at ~/.claude/.credentials.json (run claude and log in)
  • Paperclip already running in Docker
  • DNS A record pointing to the droplet IP

Reference deployment

InstanceDomainDroplet IP
Nucleottootto.nucleotto.com
RateMatchotto.ratematch.nucleotto.com134.199.159.113

Step 1 — System prerequisites

apt update && apt install -y python3-pip python3-venv git sqlite3

Step 2 — Clone and install Hermes (as deploy user)

su - cleo
mkdir -p ~/.hermes
cd ~/.hermes
git clone https://github.com/Nucleotto/hermes-agent.git
cd hermes-agent
python3 -m venv venv
source venv/bin/activate
pip install -e .

Verify install:

python -m hermes_cli.main --help

Step 3 — Configure Hermes

.env file

cat > ~/.hermes/.env << 'EOF'
# Otto — Hermes Agent config
# Auth: uses Claude credentials from ~/.claude/.credentials.json

# API Server (OpenAI-compatible endpoint for Open WebUI)
API_SERVER_ENABLED=true
API_SERVER_PORT=8642
API_SERVER_HOST=0.0.0.0
API_SERVER_KEY=<generate-a-secure-key>
API_SERVER_MODEL_ID=otto
EOF
caution

API_SERVER_HOST must be 0.0.0.0 — not 127.0.0.1. Docker containers reach the host via the Docker bridge IP (172.17.0.1) and cannot connect to localhost-only listeners.

config.yaml file

cat > ~/.hermes/config.yaml << 'EOF'
model:
default: "claude-opus-4-6"
provider: "anthropic"

display:
tool_progress: true
skin: "default"
EOF

Step 4 — Verify Claude credentials

cat ~/.claude/.credentials.json | head -5

You should see an OAuth token with subscriptionType. If missing, install the Claude CLI and run claude to trigger the OAuth login flow.

Step 5 — Test Hermes API server

cd ~/.hermes/hermes-agent
source venv/bin/activate
nohup python -m hermes_cli.main gateway run > ~/.hermes/logs/gateway.log 2>&1 &
sleep 3

# Verify port is listening
ss -tlnp | grep 8642

# Test with API key
curl -s -H "Authorization: Bearer <your-api-key>" http://127.0.0.1:8642/v1/models

Expected response:

{"object": "list", "data": [{"id": "otto", "object": "model", ...}]}

Kill the test process before proceeding to systemd:

kill $(pgrep -f "hermes_cli.main gateway")

Step 6 — Create systemd service for Hermes

# As root
cat > /etc/systemd/system/hermes-gateway.service << 'EOF'
[Unit]
Description=Hermes Agent Gateway (Otto API Server)
After=network.target docker.service

[Service]
Type=simple
User=cleo
Group=cleo
WorkingDirectory=/home/cleo/.hermes/hermes-agent
ExecStart=/home/cleo/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main gateway run --replace
Restart=always
RestartSec=5
Environment=HOME=/home/cleo
Environment=PATH=/home/cleo/.hermes/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable hermes-gateway
systemctl start hermes-gateway
systemctl status hermes-gateway

Step 7 — UFW firewall rule

Docker containers communicate with the host via the docker0 bridge. UFW blocks this by default.

ufw allow in on docker0 to any port 8642
ufw reload

Step 8 — Deploy Open WebUI

docker run -d \
--name open-webui \
--restart always \
-p 127.0.0.1:8080:8080 \
-v /opt/cleo/open-webui:/app/backend/data \
-e OPENAI_API_BASE_URL=http://172.17.0.1:8642/v1 \
-e OPENAI_API_KEY=<same-api-key-from-step-3> \
ghcr.io/open-webui/open-webui:main
caution

Use 172.17.0.1 (Docker bridge IP) — not localhost or host.docker.internal. The --add-host=host.docker.internal:host-gateway flag does not work reliably on all kernel/Docker combinations.

Verify connectivity from inside the container:

docker exec open-webui curl -s -H "Authorization: Bearer <your-api-key>" http://172.17.0.1:8642/v1/models

Step 9 — Nginx reverse proxy

cat > /etc/nginx/sites-enabled/otto << 'EOF'
server {
server_name otto.<domain>.nucleotto.com;
client_max_body_size 25m;

location / {
proxy_pass http://127.0.0.1:8080;
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 otto.<domain>.nucleotto.com \
--non-interactive --agree-tos -m admin@nucleotto.com

Step 11 — First login

  1. Navigate to https://otto.<domain>.nucleotto.com
  2. The first account registered becomes admin — create it immediately
  3. Select "otto" from the model picker
  4. Send a test message to verify the full chain works

Pitfalls & lessons learned

host.docker.internal doesn't work

On Ubuntu 24.04 with Docker 29.x, --add-host=host.docker.internal:host-gateway silently fails on some kernels. Always use the Docker bridge IP (172.17.0.1) instead. Find it with:

ip addr show docker0 | grep inet

Hermes must listen on 0.0.0.0

If API_SERVER_HOST=127.0.0.1, the Docker container cannot reach the API server even via the bridge IP. Set it to 0.0.0.0.

UFW blocks Docker bridge traffic

Even though the host can reach 172.17.0.1:8642, containers on the bridge network are blocked by UFW unless explicitly allowed:

ufw allow in on docker0 to any port 8642

Gateway PID lock blocks systemd start

If you tested Hermes manually with nohup before setting up systemd, the old process holds a PID lock. The systemd service will crash-loop with exit-code 1 and no useful error in journalctl. Fix: use the --replace flag in ExecStart (already included in Step 6 above), or kill the stale process first:

pkill -f "hermes_cli.main gateway"

Piping gateway output kills the process

python -m hermes_cli.main gateway run 2>&1 | head -40 — the head closes the pipe after 40 lines, sending SIGPIPE and killing the gateway. Use nohup ... & or systemd for persistent runs.

Claude credentials vs API keys

The Hermes agent uses Claude CLI OAuth tokens stored at ~/.claude/.credentials.json — not traditional API keys. This means:

  • No per-token billing — it runs on the Claude Pro/Max subscription
  • Tokens expire and may need refresh (re-run claude CLI to re-auth)
  • The .credentials.json must be readable by the user running Hermes

Open WebUI data persists across container recreations

The SQLite database at /opt/cleo/open-webui/webui.db survives docker rm as long as the volume mount (-v /opt/cleo/open-webui:/app/backend/data) stays the same. User accounts, chat history, and settings are all in this file.


Maintenance

Restart Hermes

systemctl restart hermes-gateway

View Hermes logs

journalctl -u hermes-gateway -f
# or
tail -f /home/cleo/.hermes/logs/gateway.log

Update Hermes

su - cleo
cd ~/.hermes/hermes-agent
source venv/bin/activate
git pull
pip install -e .
exit
systemctl restart hermes-gateway

Update Open WebUI

docker pull ghcr.io/open-webui/open-webui:main
docker stop open-webui && docker rm open-webui
# Re-run the docker run command from Step 8

Reset Open WebUI admin password

sqlite3 /opt/cleo/open-webui/webui.db "SELECT email FROM user;"
# Then use the Open WebUI admin API or recreate the container with WEBUI_SECRET_KEY

Check all services

systemctl status hermes-gateway
docker ps
ss -tlnp | grep -E ':(3000|8080|8642|443|80) '