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
| Component | Runs as | Port | Data |
|---|---|---|---|
| Paperclip | Docker container | 3000 (localhost) | /opt/cleo/paperclip |
| Hermes Agent | systemd service (gateway mode) | 8642 (0.0.0.0) | ~/.hermes/ |
| Open WebUI | Docker container | 8080 (localhost) | /opt/cleo/open-webui |
| Nginx | System service | 80/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 toNucleotto/hermes-agent - Claude CLI credentials at
~/.claude/.credentials.json(runclaudeand log in)
- GitHub CLI (
- Paperclip already running in Docker
- DNS A record pointing to the droplet IP
Reference deployment
| Instance | Domain | Droplet IP |
|---|---|---|
| Nucleotto | otto.nucleotto.com | — |
| RateMatch | otto.ratematch.nucleotto.com | 134.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
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
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
- Navigate to
https://otto.<domain>.nucleotto.com - The first account registered becomes admin — create it immediately
- Select "otto" from the model picker
- 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
claudeCLI to re-auth) - The
.credentials.jsonmust 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) '