From 5189ca95efc9bb4f151333dd00b5cb13c629abd1 Mon Sep 17 00:00:00 2001 From: CodeGhost21 <164498022+CodeGhost21@users.noreply.github.com> Date: Thu, 7 May 2026 01:41:20 +0530 Subject: [PATCH] feat(deploy): one-click cloud deployment for OpenHuman Core (closes #1280) (#1304) --- .do/app.yaml | 56 +++++++++ .github/workflows/deploy-smoke.yml | 123 +++++++++++++++++++ Dockerfile | 18 ++- README.md | 2 +- docker-compose.yml | 48 ++++++++ docs/CLOUD_DEPLOY.md | 191 +++++++++++++++++++++++++++++ 6 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 .do/app.yaml create mode 100644 .github/workflows/deploy-smoke.yml create mode 100644 docker-compose.yml create mode 100644 docs/CLOUD_DEPLOY.md diff --git a/.do/app.yaml b/.do/app.yaml new file mode 100644 index 000000000..4a8835711 --- /dev/null +++ b/.do/app.yaml @@ -0,0 +1,56 @@ +# DigitalOcean App Platform spec for OpenHuman Core. +# +# Deploys the headless Rust core (`openhuman-core`) from the repo Dockerfile, +# exposing the JSON-RPC server on the public HTTP port. Used by the "Deploy +# to DigitalOcean" button (see docs/CLOUD_DEPLOY.md) and by `doctl apps create`. +# +# After deploy: +# - /health is publicly reachable (no auth — used for liveness) +# - /rpc requires Authorization: Bearer $OPENHUMAN_CORE_TOKEN +# +# Operators MUST set OPENHUMAN_CORE_TOKEN to a strong secret in App Platform's +# environment editor before any client calls /rpc. + +name: openhuman-core +region: nyc + +services: + - name: core + dockerfile_path: Dockerfile + source_dir: / + github: + repo: tinyhumansai/openhuman + branch: main + deploy_on_push: false + instance_count: 1 + instance_size_slug: basic-xs + http_port: 7788 + health_check: + http_path: /health + initial_delay_seconds: 20 + period_seconds: 30 + timeout_seconds: 5 + success_threshold: 1 + failure_threshold: 3 + envs: + - key: OPENHUMAN_CORE_HOST + scope: RUN_TIME + value: "0.0.0.0" + - key: OPENHUMAN_CORE_PORT + scope: RUN_TIME + value: "7788" + - key: OPENHUMAN_APP_ENV + scope: RUN_TIME + value: production + - key: BACKEND_URL + scope: RUN_TIME + value: https://api.tinyhumans.ai + - key: RUST_LOG + scope: RUN_TIME + value: info + # Required for clients to authenticate against /rpc. Set to a strong + # random value (e.g. `openssl rand -hex 32`) in the App Platform UI. + - key: OPENHUMAN_CORE_TOKEN + scope: RUN_TIME + type: SECRET + value: "CHANGE_ME_BEFORE_DEPLOY" diff --git a/.github/workflows/deploy-smoke.yml b/.github/workflows/deploy-smoke.yml new file mode 100644 index 000000000..fbeb2559e --- /dev/null +++ b/.github/workflows/deploy-smoke.yml @@ -0,0 +1,123 @@ +--- +name: Deploy Smoke +on: + push: + branches: [main] + paths: + - Dockerfile + - .dockerignore + - docker-compose.yml + - .do/app.yaml + - docs/CLOUD_DEPLOY.md + - .github/workflows/deploy-smoke.yml + - Cargo.toml + - Cargo.lock + - rust-toolchain.toml + - src/** + pull_request: + paths: + - Dockerfile + - .dockerignore + - docker-compose.yml + - .do/app.yaml + - docs/CLOUD_DEPLOY.md + - .github/workflows/deploy-smoke.yml + - Cargo.toml + - Cargo.lock + - rust-toolchain.toml + - src/** + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true +jobs: + docker-image: + name: Build & smoke-test core image + runs-on: ubuntu-22.04 + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build openhuman-core image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: false + load: true + tags: openhuman-core:smoke + cache-from: type=gha,scope=deploy-smoke + cache-to: type=gha,scope=deploy-smoke,mode=max + + - name: Run container + run: | + docker run -d \ + --name oh-smoke \ + -p 7788:7788 \ + -e OPENHUMAN_CORE_TOKEN=ci-smoke-token \ + -e OPENHUMAN_APP_ENV=staging \ + -e BACKEND_URL=https://staging-api.tinyhumans.ai \ + openhuman-core:smoke + + - name: Wait for /health + run: | + set -e + for i in $(seq 1 30); do + if curl -fsS http://localhost:7788/health > /tmp/health.json; then + echo "Healthy on attempt $i" + cat /tmp/health.json + exit 0 + fi + echo "attempt $i: not ready, sleeping..." + sleep 2 + done + echo "Container never became healthy. Logs:" + docker logs oh-smoke || true + exit 1 + + - name: Verify /rpc rejects without bearer token + run: | + set -e + status=$(curl -s -o /tmp/rpc.json -w "%{http_code}" \ + -X POST http://localhost:7788/rpc \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"openhuman.about_app_list","params":{}}') + if [ "$status" != "401" ]; then + echo "Expected 401 from /rpc without token, got $status" + cat /tmp/rpc.json + docker logs oh-smoke || true + exit 1 + fi + + - name: Verify /rpc accepts the configured bearer token + run: | + set -e + status=$(curl -s -o /tmp/rpc-ok.json -w "%{http_code}" \ + -X POST http://localhost:7788/rpc \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ci-smoke-token' \ + -d '{"jsonrpc":"2.0","id":1,"method":"openhuman.about_app_list","params":{}}') + if [ "$status" != "200" ]; then + echo "Expected 200 from authenticated /rpc, got $status" + cat /tmp/rpc-ok.json + docker logs oh-smoke || true + exit 1 + fi + cat /tmp/rpc-ok.json + + - name: Container logs (always) + if: always() + run: docker logs oh-smoke || true + + - name: Tear down + if: always() + run: docker rm -f oh-smoke || true diff --git a/Dockerfile b/Dockerfile index 2366ba12b..ab6ecbc65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,22 @@ FROM rust:1.93-bookworm AS builder ENV DEBIAN_FRONTEND=noninteractive -# System dependencies required for compilation +# System dependencies required for compilation. +# +# ALSA / X11 / input headers are needed because `cpal`, `enigo`, `arboard`, +# and `rdev` are unconditional dependencies of the core crate (used by the +# voice, autocomplete, and clipboard subsystems). They link against system +# libraries even when the corresponding features are disabled at runtime. RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ + cmake \ pkg-config \ libssl-dev \ + libasound2-dev \ + libxdo-dev \ + libxtst-dev \ + libx11-dev \ + libevdev-dev \ clang \ mold \ ca-certificates \ @@ -51,6 +62,11 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ libssl3 \ + libasound2 \ + libxdo3 \ + libxtst6 \ + libx11-6 \ + libevdev2 \ curl \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 33f31afed..8bb906cbe 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ OpenHuman is an open-source agentic assistant that is designed to integrate with - **Deep desktop integrations** — OpenHuman is a **native desktop** assistant, not a web-only chat: **memory-aware keyboard autocomplete**, **voice** (**STT** listening and **TTS** replies), **screen intelligence** that understands what is on screen and feeds your local context, plus windowing and OS-level permissions—so the agent meets you **on the machine**, not trapped in a browser tab. -Architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). Contributor orientation: [`CONTRIBUTING.md`](./CONTRIBUTING.md). Running from source: [`docs/install.md`](docs/install.md#running-from-source). +Architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). Contributor orientation: [`CONTRIBUTING.md`](./CONTRIBUTING.md). Running from source: [`docs/install.md`](docs/install.md#running-from-source). Cloud-hosting the headless core: [`docs/CLOUD_DEPLOY.md`](docs/CLOUD_DEPLOY.md). ## Highlights diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..43c5337f3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +# OpenHuman Core — Docker Compose for self-hosted cloud deploy. +# +# Brings up the headless Rust core (`openhuman-core`) on :7788, persists the +# workspace to a named volume, and reads secrets/config from a `.env` file +# next to this compose file. +# +# Usage: +# 1. cp .env.example .env (then edit values — at minimum BACKEND_URL and +# OPENHUMAN_CORE_TOKEN; the latter is required for any client that calls +# /rpc on this instance) +# 2. docker compose up -d +# 3. curl http://localhost:7788/health +# +# The image is built from the repo Dockerfile. To pin a published image +# instead of building, replace `build:` with `image: ghcr.io/.../openhuman-core:`. + +services: + openhuman-core: + build: + context: . + dockerfile: Dockerfile + image: openhuman-core:local + container_name: openhuman-core + restart: unless-stopped + ports: + - "${OPENHUMAN_CORE_PORT:-7788}:7788" + env_file: + - .env + environment: + # Bind to 0.0.0.0 inside the container so port-forwarding works regardless + # of what `.env` says. The Dockerfile already sets this default, but make + # it explicit so an inherited shell value cannot override it. + OPENHUMAN_CORE_HOST: 0.0.0.0 + OPENHUMAN_CORE_PORT: "7788" + OPENHUMAN_WORKSPACE: /home/openhuman/.openhuman + RUST_LOG: ${RUST_LOG:-info} + volumes: + - openhuman-workspace:/home/openhuman/.openhuman + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:7788/health"] + interval: 30s + timeout: 5s + start_period: 15s + retries: 3 + +volumes: + openhuman-workspace: + name: openhuman-workspace diff --git a/docs/CLOUD_DEPLOY.md b/docs/CLOUD_DEPLOY.md new file mode 100644 index 000000000..2f1bdfb85 --- /dev/null +++ b/docs/CLOUD_DEPLOY.md @@ -0,0 +1,191 @@ +# Cloud deployment + +OpenHuman is a desktop app, but its **Rust core** (`openhuman-core`) is a +headless JSON-RPC server that can be hosted in the cloud. Deploying the core +separately is useful for: + +- Multi-device access — point several desktop clients at the same hosted core +- Internal testers without local Rust toolchains +- Long-running cron jobs / webhooks that should outlive a laptop session + +This guide covers three deploy paths, easiest first: + +1. [DigitalOcean App Platform: one-click](#1-digitalocean-app-platform-one-click) +2. [DigitalOcean App Platform: manual via doctl](#2-digitalocean-app-platform-manual-via-doctl) +3. [Any VPS via Docker Compose](#3-any-vps-via-docker-compose) + +What gets deployed in every path: a single container running +`openhuman-core serve` on port `7788`, behind the provider's TLS. The desktop +app already knows how to talk to a remote core — set +`OPENHUMAN_CORE_RPC_URL=https://your-host/rpc` and `OPENHUMAN_CORE_TOKEN=...` +in `app/.env.local` and launch. + +--- + +## What you need before you start + +| Setting | Required | Notes | +|----------------------------|----------|-----------------------------------------------------------------------| +| `OPENHUMAN_CORE_TOKEN` | yes | Bearer token clients send to `/rpc`. Generate with `openssl rand -hex 32`. **Anyone with this token can drive the core.** | +| `BACKEND_URL` | yes | Tinyhumans backend the core talks to (`https://api.tinyhumans.ai` for prod). | +| `OPENHUMAN_APP_ENV` | no | `production` or `staging`. Defaults to `staging`. | +| `OPENHUMAN_CORE_HOST` | no | Defaults to `0.0.0.0` in the container. | +| `OPENHUMAN_CORE_PORT` | no | Defaults to `7788`. | +| `RUST_LOG` | no | `info` is fine; `debug` for triage. | + +Endpoints exposed by the running container: + +- `GET /health` — public liveness probe. Used by every deploy path's healthcheck. +- `POST /rpc` — bearer-protected JSON-RPC entrypoint. +- `GET /events`, `GET /ws/dictation` — public streaming channels. + +The `OPENHUMAN_WORKSPACE` directory (`/home/openhuman/.openhuman` inside the +container) holds the core's config, sqlite databases, and skill state. **Mount +it on a persistent volume** in every production deploy or you will lose data on +restart. + +--- + +## 1. DigitalOcean App Platform: one-click + +Click the button below to create a new App Platform application from this +repository's [`.do/app.yaml`](../.do/app.yaml): + +[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/tinyhumansai/openhuman/tree/main) + +Then, in the App Platform UI, **before the first deploy completes**: + +1. Open the **Settings → App-Level Environment Variables** tab. +2. Replace the placeholder `OPENHUMAN_CORE_TOKEN` value with a strong secret + (`openssl rand -hex 32`). Mark it encrypted. +3. If you are deploying staging, change `OPENHUMAN_APP_ENV` to `staging` and + `BACKEND_URL` to `https://staging-api.tinyhumans.ai`. +4. Hit **Save** — App Platform redeploys with the new secret. + +App Platform handles TLS, restart-on-crash, log streaming, and rolling +redeploys on `git push` (set `deploy_on_push: true` in `.do/app.yaml` to +opt-in). + +> **Persistence note:** App Platform Basic does not provide block storage. The +> core's workspace lives in the container's ephemeral filesystem and is lost +> on redeploy. For durable storage, attach a managed database or upgrade to a +> tier that supports volumes. See the [Compose path](#3-any-vps-via-docker-compose) +> for a self-host alternative with persistent volumes out of the box. + +--- + +## 2. DigitalOcean App Platform: manual via doctl + +If you'd rather not click through the UI: + +```bash +# One-time: install doctl and authenticate. +doctl auth init + +# Edit .do/app.yaml — set OPENHUMAN_CORE_TOKEN to a real value (or pass it in +# at create time via --spec with envsubst). Then: +doctl apps create --spec .do/app.yaml + +# Watch the build: +doctl apps list +doctl apps logs --type build --follow +``` + +Update an existing app after editing the spec: + +```bash +doctl apps update --spec .do/app.yaml +``` + +--- + +## 3. Any VPS via Docker Compose + +Works on any host with Docker Engine ≥ 24 and the Compose plugin — +DigitalOcean Droplet, Hetzner, Linode, EC2, a home server. + +```bash +# On the server: +git clone https://github.com/tinyhumansai/openhuman.git +cd openhuman + +# Configure secrets: +cp .env.example .env +# Edit .env — at minimum: +# BACKEND_URL=https://api.tinyhumans.ai +# OPENHUMAN_CORE_TOKEN= +# OPENHUMAN_APP_ENV=production + +# Build and start: +docker compose up -d + +# Verify: +docker compose ps +curl -fsS http://localhost:7788/health +``` + +The Compose file ([`docker-compose.yml`](../docker-compose.yml)) maps the core +on `:7788`, mounts a named volume `openhuman-workspace` for persistence, and +sets `restart: unless-stopped` so the core comes back after host reboots. + +### Updating + +```bash +git pull +docker compose build +docker compose up -d +``` + +### Logs + +```bash +docker compose logs -f openhuman-core +``` + +### Putting it behind TLS + +Use Caddy, nginx, or Traefik as a reverse proxy in front of `:7788`. A minimal +`Caddyfile`: + +```caddy +core.example.com { + reverse_proxy localhost:7788 +} +``` + +--- + +## Pointing the desktop app at a hosted core + +In the desktop app's environment file (`app/.env.local`): + +```bash +# Use the hosted core instead of spawning a local sidecar. +OPENHUMAN_CORE_RUN_MODE=external +OPENHUMAN_CORE_RPC_URL=https://core.example.com/rpc +OPENHUMAN_CORE_TOKEN= +``` + +Restart the desktop app. The provider chain in `App.tsx` will route all RPC +calls to the remote core; nothing else changes. + +--- + +## Smoke test + +The repo ships [`.github/workflows/deploy-smoke.yml`](../.github/workflows/deploy-smoke.yml), +which runs on every PR that touches the deploy artifacts. It builds the +Docker image, boots it, and polls `/health` — so a regression in the cloud +deploy path fails CI before it lands on `main`. + +To run the same check locally: + +```bash +docker build -t openhuman-core:smoke . +docker run -d --name oh-smoke -p 7788:7788 \ + -e OPENHUMAN_CORE_TOKEN=smoke-test-token \ + openhuman-core:smoke +# Wait ~15s for the binary to come up, then: +curl -fsS http://localhost:7788/health +docker rm -f oh-smoke +```