Skip to content

Latest commit

 

History

History
294 lines (220 loc) · 8.76 KB

File metadata and controls

294 lines (220 loc) · 8.76 KB

Deployment Guide

This guide covers deploying hamroh to a VPS (Contabo, Hetzner, DigitalOcean, etc.) using Docker, and setting up a continuous deployment workflow.

Prerequisites

  • A VPS with SSH access
  • A GitHub repo with your hamroh code
  • A Telegram bot token (from @BotFather)
  • A Claude Code account (for API authentication)

Initial server setup (one-time)

# SSH into your server
ssh root@your-server-ip

# Install Docker
curl -fsSL https://get.docker.com | sh

# Install Node.js + Claude Code CLI and authenticate
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y nodejs
npm install -g @anthropic-ai/claude-code
claude   # interactive login — creates ~/.claude/

# Clone your private repo (SSH auth — add server's public key to GitHub first)
#   On server: ssh-keygen -t ed25519 (if no key exists)
#   Copy ~/.ssh/id_ed25519.pub → GitHub Settings → SSH keys
git clone git@github.com:your-user/hamroh.git ~/hamroh
cd ~/hamroh

# Configure
cp .env.example .env
vim .env   # set TELEGRAM_BOT_TOKEN, HAMROH_OWNER_ID, etc.
cp prompts/project.md.example prompts/project.md
vim prompts/project.md   # customize identity, integrations, team info

# Build and start
docker compose up -d --build

# Verify it's running
docker compose ps
docker compose logs -f   # should see "hamroh is live"

DM your bot on Telegram to confirm it replies.

Enabling capabilities

The bot ships with a tight default surface — Telegram messaging, memory tools, reminders, and read-only web access only. Shell, code-editing, subagents, and any external MCPs are all off by default.

Toggles live in plugins.json at the repo root. Copy the shipped template once on first setup:

cp plugins.json.example plugins.json

Then edit:

{
  "tool_groups": { "bash": true, "code": true, "subagents": false },
  "mcps":  [ /* sample Jira / GitLab / GitHub entries — keep, edit, or delete */ ],
  "skills_disabled": [],
  "builtin_tools_disabled": []
}

plugins.json is gitignored, so different deployments can carry different toggles without fighting over the file. External MCPs are declared in plugins.json but their credentials live in .env, referenced as ${VAR}. An MCP whose ${VAR} references aren't satisfied is silently skipped at boot. The shipped example carries sample Jira / GitLab / GitHub entries to copy from — they're not first-class, just convenient starting points.

For the per-tool list, the schema, "How to add a new MCP", and how to disable individual built-in tools (e.g. telegram_create_poll, render_latex) or skills, see tools.md. Restart the container after editing either file: docker compose up -d --force-recreate.

Update workflow

Manual (SSH)

Every time you push changes to GitHub:

ssh root@your-server-ip 'cd ~/hamroh && git pull && docker compose up -d --build'

Or step by step:

ssh root@your-server-ip
cd ~/hamroh
git pull
docker compose up -d --build
docker compose logs -f   # verify it started correctly

Automatic (GitHub Actions)

Create .github/workflows/deploy.yml in your repo:

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_IP }}
          username: root
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/hamroh
            git pull
            docker compose up -d --build

Then add these secrets to your GitHub repo (Settings → Secrets and variables → Actions):

Secret Value
SERVER_IP Your VPS IP address
SSH_PRIVATE_KEY Contents of ~/.ssh/id_ed25519 (generate with ssh-keygen -t ed25519 and add the public key to the server's ~/.ssh/authorized_keys)

Every push to main will automatically deploy to your server.

Note: Since the repo is private, the server needs SSH access to GitHub for git pull to work. Make sure the server's SSH key (~/.ssh/id_ed25519.pub) is added as either:

  • A deploy key on the repo (Settings → Deploy keys) — scoped to this repo only, recommended
  • Or an SSH key on your GitHub account (Settings → SSH keys) — grants access to all your repos

The data/ directory

The data/ directory is created automatically on first run. It contains:

  • data/memories/ — bot's runtime memory files (the only part of data/ worth migrating between deployments). Durable memories you want to keep permanently can instead live in the git-tracked memories/ folder at the repo root, which travels with the repo and needs no migration — see below.
  • data/hamroh.db — SQLite database (messages, users, reminders, tool call logs) — starts fresh on new servers
  • data/session_id — Claude Code session ID for conversation continuity
  • data/attachments/ — inbound photos/docs the dispatcher saved
  • data/renders/ — outbound PNGs from render_html
  • data/cc_logs/ — raw Claude Code subprocess logs

Headless Chromium for render_html is pre-installed in the Docker image (playwright install --with-deps chromium) — no per-host provisioning step needed.

First deployment: nothing to do — the bot creates everything.

Migrating from another server: only copy memories:

scp -r old-server:~/hamroh/data root@new-server:~/hamroh/data

Don't copy session_id or hamroh.db to a new server — stale session IDs cause CC subprocess crashes, and the database will rebuild naturally from new messages.

Syncing memories and config

The gitignored runtime store (data/memories/) only lives on the server, so use the included sync script to keep it and your project config in sync between your local machine and the server:

# Pull latest memories from server
./scripts/sync-memories.sh pull root@your-server-ip

# Push updated project.md to server
./scripts/sync-memories.sh push root@your-server-ip

# Both (pull memories, then push project.md)
./scripts/sync-memories.sh sync root@your-server-ip

The committed memories/ folder (repo root) needs none of this — it's tracked in git, so git pull / git push move it like any other code. Use it for durable memories you want versioned and backed up; let the bot's ephemeral day-to-day notes stay in the runtime store.

After pushing project.md, restart for changes to take effect:

ssh root@your-server-ip 'cd ~/hamroh && docker compose restart'

Common operations

# View live logs
ssh root@your-server-ip 'cd ~/hamroh && docker compose logs -f'

# Shell into the container
ssh root@your-server-ip 'cd ~/hamroh && docker compose exec hamroh bash'

# Restart without rebuilding
ssh root@your-server-ip 'cd ~/hamroh && docker compose restart'

# Stop the bot
ssh root@your-server-ip 'cd ~/hamroh && docker compose down'

# Check status
ssh root@your-server-ip 'cd ~/hamroh && docker compose ps'

Troubleshooting

Telegram conflict error

Conflict: terminated by other getUpdates request

Another instance is polling the same bot token. Make sure only one is running — check both local (pkill -f 'python -m hamroh') and Docker (docker compose down).

CC subprocess crashes (rc=1, empty stderr)

Common causes:

  • Stale session ID — delete data/session_id and restart. This happens after renaming the project folder or moving to a new server.
  • MCP server not reachable — the --strict-mcp-config flag makes Claude exit if any MCP server in the config fails to connect. Check that uvx and npx are available inside the container.

Claude Code auth expired

SSH into the server and re-authenticate:

ssh root@your-server-ip
claude   # follow the login flow
cd ~/hamroh && docker compose restart

macOS Docker credentials

If you run hamroh in Docker on macOS and see Not logged in · Please run /login even though claude login succeeded on the host, that's because macOS stores the OAuth token in the Keychain — the container's bind mount of ~/.claude doesn't carry it. (Linux hosts write ~/.claude/.credentials.json natively, so they don't hit this.)

Easiest fix: don't use Docker on macOS. Run with uv instead — the subprocess inherits your shell's Keychain access:

uv sync --extra dev
uv run python -m hamroh

If you really need Docker on macOS: export the Keychain entry to a file once, then bring up the stack:

security find-generic-password -s "Claude Code-credentials" -w \
  > ~/.claude/.credentials.json
chmod 600 ~/.claude/.credentials.json
docker compose up -d

The OAuth token rotates periodically. When it does, the file goes stale and the same error returns — re-run the export to refresh. There is no automated refresh, which is why the uv path is recommended for day-to-day macOS use.