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 1420Then 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):
OPENHUMAN_CORE_TOKENenvironment 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.{workspace}/core.tokenfile: generated by the core on first boot only whenOPENHUMAN_CORE_TOKENis unset. Standaloneopenhuman core runuses this so CLI clients cancatthe 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
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:
Then, in the App Platform UI, before the first deploy completes:
Open the Settings โ App-Level Environment Variables tab.
Replace the placeholder
OPENHUMAN_CORE_TOKENvalue with a strong secret (openssl rand -hex 32). Mark it encrypted.If you are deploying staging, change
OPENHUMAN_APP_ENVtostagingandBACKEND_URLtohttps://staging-api.tinyhumans.ai.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 returnrestart_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:
Call
openhuman.update_checkto discover a release.Configure
restart_strategy = "supervisor"in yourupdate.toml(or setOPENHUMAN_AUTO_UPDATE_RESTART_STRATEGY=supervisor) so the core stages the new binary without trying to re-exec itself, then callopenhuman.update_applyoropenhuman.update_run.restart_strategyis a configuration setting, not an RPC parameter.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_URLunset or wrong. It is required (see the env table above) and must behttps://api.tinyhumans.aifor 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
allowPendingBackendValidationand 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:
Starts as
root.Runs
mkdir -p+chown openhuman:openhumanon both$OPENHUMAN_WORKSPACEand$HOME/.openhuman(the directorycore.tokenis written to whenOPENHUMAN_CORE_TOKENis unset).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
.fly/fly.tomlThe 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: setsOPENHUMAN_CORE_TOKENand 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: omitsOPENHUMAN_CORE_TOKENand mounts a fresh anonymous volume at/home/openhuman/.openhuman. Reproduces the exact failure mode of issue #2065 and asserts that/healthreturns 200 and thatPermission denied (os error 13)is absent from the logs.
Run the smoke check locally:
Last updated