For the complete documentation index, see llms.txt. This page is also available as Markdown.

Cloud Deploy

Hosting the headless openhuman-core in the cloud - DigitalOcean App Platform, Fly.io, or Docker Compose on any VPS.

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 four deploy paths, easiest first:

What gets deployed in every path: a single container running openhuman-core serve on port 7788. Public hosts should sit behind the provider's TLS, for example https://core.example.com/rpc. Private-only hosts on localhost, RFC1918 networks, or tailnets such as Tailscale can use plain HTTP, for example http://100.x.x.x:7788/rpc, when the core is not reachable from the public internet. The desktop app already knows how to talk to a remote core; set OPENHUMAN_CORE_RPC_URL and OPENHUMAN_CORE_TOKEN=... in app/.env.local and launch.


Remote UI choices

OpenHuman's supported remote deployment is core remote, UI local: run openhuman-core on a Linux server and point a desktop client at that RPC URL. The deployed core does not serve the full React/Tauri UI as a production web app yet. Desktop-only features still need the Tauri shell, including tray controls, native deep links, CEF account scanners, OS keychain integration, and window/screen affordances.

For a browser-accessible UI on a private server today, use the Vite web build as a development/preview surface against the remote core:

# On the server, run the core with an explicit token.
export OPENHUMAN_CORE_HOST=0.0.0.0
export OPENHUMAN_CORE_PORT=7788
export OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)"
openhuman-core serve

# In another shell on the server, serve the UI only on loopback.
pnpm --dir app dev -- --host 127.0.0.1 --port 1420

Then tunnel both ports from your workstation:

Open http://127.0.0.1:1420, choose the remote/core option on the first-run screen, and enter http://127.0.0.1:7788/rpc plus the OPENHUMAN_CORE_TOKEN value from the server.

If you serve the browser UI from a non-loopback origin, add that exact origin to the core's CORS allowlist:

Loopback Vite origins such as http://127.0.0.1:1420 and http://localhost:1420 are allowed automatically. Public http:// origins are not recommended because every RPC call carries the bearer token.


Single source of truth for the bearer token

Every /rpc call carries Authorization: Bearer <token>. The core has two ways to load that token at startup (src/core/auth.rs):

  1. OPENHUMAN_CORE_TOKEN environment variable: pre-seeded by the caller (Tauri shell, Docker, App Platform, systemd unit, โ€ฆ). The core uses this value as-is and never writes a file.

  2. {workspace}/core.token file: generated by the core on first boot only when OPENHUMAN_CORE_TOKEN is unset. Standalone openhuman core run uses this so CLI clients can cat the file.

Rule of thumb for any remote / dockerized deploy: always set OPENHUMAN_CORE_TOKEN. Do not rely on core.token in a container. Ephemeral filesystems lose it on redeploy, and any client trying to read the file from outside the container will get a stale or empty value. The two paths are deliberately mutually exclusive at startup; mixing them is the most common reason behind "the dashboard gets 401 after I redeployed".

To check what the running core is using, run scripts/print-core-token.sh on the host (or inside the container with docker compose exec):

The desktop app's first-run picker also exposes a Test connection button next to the Core RPC URL + token fields, which fires core.ping against the URL with the typed token and reports Connected โœ“ / Auth failed / Unreachable inline before persisting the configuration.


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 production.

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:

Deploy to DO

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 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:

Update an existing app after editing the spec:


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.

Each production release publishes a multi-tagged image to GHCR:

The image is linux/amd64. arm64 hosts pull the standalone tarball attached to the same GitHub Release (openhuman-core-<version>-aarch64-unknown-linux-gnu.tar.gz) or build the image from source on an arm64 builder.

Quick run with a published image:

Or use the in-repo Compose file (still builds the image locally from Dockerfile; switch the image: field to ghcr.io/tinyhumansai/openhuman-core:latest in docker-compose.yml to consume the published image instead):

Headless install without Docker

If you can't run Docker on the host, grab the standalone CLI tarball attached to the latest GitHub Release:

Then run openhuman-core serve under your service manager of choice (systemd, supervisord, โ€ฆ) with the same environment variables documented above.

Headless self-update contract

Headless deployments should treat openhuman.update_apply as the safe primitive: it downloads the release asset, writes it atomically next to the current binary, and returns. Nothing exits automatically.

openhuman.update_run follows config.update.restart_strategy:

  • self_replace (default): stage the binary, publish an in-process restart request, and let the running core respawn itself.

  • supervisor: stage the binary and return restart_requested=false. Your outer service manager must restart the process.

For long-running Linux services, set:

or the equivalent env vars:

Recommended systemd stance:

Operator flow:

  1. Call openhuman.update_check to discover a release.

  2. Configure restart_strategy = "supervisor" in your update.toml (or set OPENHUMAN_AUTO_UPDATE_RESTART_STRATEGY=supervisor) so the core stages the new binary without trying to re-exec itself, then call openhuman.update_apply or openhuman.update_run. restart_strategy is a configuration setting, not an RPC parameter.

  3. Restart the unit explicitly: systemctl restart openhuman.

If download or staging fails, the running binary is left in place and no restart is requested. If a staged binary proves bad after restart, roll back by restoring the previous binary from your package manager, image tag, or release artifact and restarting the supervisor again.

The Compose file (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

For RPC-exposed production deployments, prefer leaving mutating update RPCs disabled (OPENHUMAN_AUTO_UPDATE_RPC_MUTATIONS_ENABLED=false) and perform rollouts through your existing image tag or package-management flow instead.

Logs

Rotating the bearer token

OPENHUMAN_CORE_TOKEN is the only thing standing between the public internet and full RPC access. Rotate it on a schedule and after any suspected leak:

For App Platform, do the same in Settings โ†’ App-Level Environment Variables: edit the OPENHUMAN_CORE_TOKEN secret and let App Platform redeploy. There is no separate token file to delete; the env var is the only state.

Putting it behind TLS

Use Caddy, nginx, or Traefik as a reverse proxy in front of :7788. A minimal Caddyfile:


Pointing the desktop app at a hosted core

In the desktop app's environment file (app/.env.local):

For a private tailnet-only VM with no public IP, use the tailnet URL instead:

Restart the desktop app. The provider chain in App.tsx will route all RPC calls to the remote core; nothing else changes. Public http:// hosts are rejected by the app picker; use HTTPS for any publicly reachable core.

Troubleshooting: sign-in fails after OAuth on a cloud runtime

Symptom: OAuth in the browser completes ("close this and return to the app"), but the desktop then shows a sign-in error โ€” while the same account signs in fine on the local (embedded) runtime (issue #3025).

Root cause is almost always the remote core, not the desktop. auth_store_session makes the core validate the fresh session token against the backend (GET /auth/me) before persisting it; on a cloud runtime that call runs on your server, so it fails if the remote core can't reach/authenticate the backend:

  • BACKEND_URL unset or wrong. It is required (see the env table above) and must be https://api.tinyhumans.ai for prod (or the staging URL). A missing value is the most common cause โ€” one reporter's failure was exactly this.

  • Backend unreachable from the server (egress firewall, DNS, TLS interception) โ†’ the core sees a gateway/timeout on /auth/me.

  • Outdated core. Older cores predate allowPendingBackendValidation and validate synchronously with no grace; update the server to a current release.

  • Wrong RPC token. A token mismatch surfaces as HTTP 401 on /rpc; re-paste the token (see "Rotating the bearer token").

Check the remote core logs for Session validation failed (GET /auth/me) and the status/reason. The desktop now reports a cloud-specific, actionable message for these instead of a generic "try again" (issue #3025).


Named-volume ownership and the Docker entrypoint

Docker creates named volumes owned root:root by default. Because the core runs as the non-root openhuman user (UID 10001), the first write after the banner (init_rpc_token โ†’ write_token_file into $OPENHUMAN_WORKSPACE) would raise Permission denied (os error 13) if nothing fixes the ownership first.

The image ships a dedicated entrypoint at /usr/local/bin/docker-entrypoint-core.sh that:

  1. Starts as root.

  2. Runs mkdir -p + chown openhuman:openhuman on both $OPENHUMAN_WORKSPACE and $HOME/.openhuman (the directory core.token is written to when OPENHUMAN_CORE_TOKEN is unset).

  3. Calls exec gosu openhuman openhuman-core "$@" to drop privileges and hand off to the binary.

This is idempotent: on a freshly-created volume the chown heals the root-owned directory; on a volume that was already healed the chown is a no-op. No manual docker volume rm is required when upgrading from images predating this fix.

The entrypoint is named docker-entrypoint-core.sh and wired only into the root Dockerfile. The E2E image (e2e/docker-entrypoint.sh) is unaffected.


4. Fly.io

Fly.io is a good fit for openhuman-core: it handles TLS automatically, supports persistent volumes on all tiers, and can auto-stop idle machines to cut costs.

Prerequisites

  • flyctl installed and authenticated (fly auth login)

  • A Fly.io account

Step 1: Launch the app

Fly.io detects the Dockerfile automatically. Choose a region close to your users and skip the first deploy when prompted. This generates a config file.

Step 2: Configure .fly/fly.toml

The repo ships a template at .fly/fly.toml. Fill in <your-app-name> and <your-region> with the values you chose during fly launch:

Step 3: Create a persistent volume

Mount the workspace on a persistent volume or data is lost on every redeploy.

Step 4: Set secrets

Save the value of OPENHUMAN_CORE_TOKEN. You will need it to connect the desktop app later. Anyone with this token can drive the core; treat it like a password and rotate it with fly secrets set OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)" after any suspected leak.

Step 5: Deploy

Verify the core is healthy:

Step 6: Point the desktop app at the hosted core

In app/.env.local:

Or use the first-run picker in the desktop app (Core RPC URL + token fields with a Test connection button) to configure without editing files.

Continuous deployment

To redeploy automatically on every push to main, add a workflow file at .github/workflows/fly-deploy.yml:

Generate a deploy token with fly tokens create deploy and add it as a repository secret named FLY_API_TOKEN.

Updating

For version-pinned deployments, update the image tag in .fly/fly.toml and redeploy:

Logs

Known gotcha: UID mismatch on volumes

If you switch between building from Dockerfile (which creates the openhuman user at UID 10001) and pulling the pre-built GHCR image (which uses UID 1000), files already written to the persistent volume will be owned by the old UID and produce Permission denied (os error 13) on startup.

Fix by SSH-ing in and re-owning the workspace:


Smoke test

Two failure modes guard the cloud deploy path:

  • docker-image: sets OPENHUMAN_CORE_TOKEN and mounts no volume. Protects the DigitalOcean App Platform path (.do/app.yaml) where the token is always pre-set and no persistent volume is used.

  • docker-volume-permissions: omits OPENHUMAN_CORE_TOKEN and mounts a fresh anonymous volume at /home/openhuman/.openhuman. Reproduces the exact failure mode of issue #2065 and asserts that /health returns 200 and that Permission denied (os error 13) is absent from the logs.

Run the smoke check locally:

Last updated