Self-host OpenHands
The OpenHands web UI and its Docker sandboxes on one VM. No API keys to paste.
Shelley handles the setup. About 15 minutes.
Prefer a terminal?
curl -fsSL https://exe.dev/deploy/openhands/prompt.txt | ssh exe.dev new --prompt=/dev/stdinOpenHands (once OpenDevin) is an open-source coding agent that writes code, runs commands and commits its work. Its self-hosted app, Agent Canvas, is a web UI for conversations, files, terminals and scheduled automations, backed by an Agent Server that runs the agent. OpenHands' own docs call a server in the cloud the most powerful way to run it. On an exe.dev VM the agents keep working when your laptop sleeps, the UI is reachable over HTTPS behind exe.dev's login, and each conversation gets its own Docker sandbox on the same VM.
Models come from exe.dev's LLM integration: Claude, GPT and open-weight models with no API key to paste, and usage billed to your exe.dev account.
What you get#
- Agent Canvas 1.40.0 running as a service on port 8000, back after a reboot
- Conversations in Docker sandboxes by default, one container per conversation, with only its project folder mounted
- The OpenHands agent on Claude Sonnet 5.5 through exe.dev's LLM integration, with no API key to paste
- A random backend API key in
~/.config/agent-canvas/env, readable only by you - A project at
~/projects/helloand a setup-check conversation that ran in a container and answered42
What's running, and where
https://my-openhands.exe.xyz/
Agent Canvas in your browser, behind exe.dev login and OpenHands' API key
ssh my-openhands.exe.xyz
your terminal, for the API key and upkeep
Deploy with Shelley#
Click Deploy and Shelley will set up OpenHands on a new VM, then check that it works. No config-file wrangling required.
Prefer a terminal?
curl -fsSL \
https://exe.dev/deploy/openhands/prompt.txt \
| ssh exe.dev new --prompt=/dev/stdinWhat Shelley is told
Set up OpenHands on this VM by following the exe.dev recipe at https://exe.dev/deploy/openhands.md from top to bottom.
The recipe's example VM name is my-openhands. This VM's name is the output of `hostname`: use it wherever the recipe writes my-openhands. Skip the step that creates the VM; you are on it.
You are done when all of these are true. Check each one yourself:
- Agent Canvas 1.40.0 running as a service on port 8000, back after a reboot
- Conversations in Docker sandboxes by default, one container per conversation, with only its project folder mounted
- The OpenHands agent on Claude Sonnet 5.5 through exe.dev's LLM integration, with no API key to paste
- A random backend API key in `~/.config/agent-canvas/env`, readable only by you
- A project at `~/projects/hello` and a setup-check conversation that ran in a container and answered `42`
Run the VM-side commands in "Install by hand" only, from "Install Agent
Canvas" on. Verify that `systemctl is-active agent-canvas` says active,
that `curl -s -o /dev/null -w '%{http_code}' localhost:8000/api/settings`
prints `401` (the API key is on), and that the setup check prints
`{"response":"42"}`. Never print the API key in your messages. Skip the
backup, update and troubleshooting sections and the FAQ. Finish by
giving the owner the browser URL and the command from "Open OpenHands"
that prints the API key, with this VM's name, and tell them to follow
"Open OpenHands", unticking the usage-data box if they prefer.
Finish with the URLs I should open.
Also at /deploy/openhands/prompt.txt. The recipe it follows is /deploy/openhands.md.
Install#
Deploy installs Agent Canvas, runs it as a service with Docker sandboxes and an API key, connects a model, and checks that a sandboxed conversation answers. To do it yourself:
Install by hand
On your computer, create a VM and connect. Pick any free name; if you already have a VM, skip the first command and use its name:
ssh exe.dev new --name=my-openhands
ssh my-openhands.exe.xyz
Install Agent Canvas#
On the VM, install Node.js 24 from NodeSource (Agent Canvas needs it; the
VM's own is older), then Agent Canvas from npm, with the version pinned.
Global npm packages go in ~/.local, so updates need no sudo:
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
npm config set prefix ~/.local
npm install -g @openhands/agent-canvas@1.40.0
export PATH="$HOME/.local/bin:$PATH"
agent-canvas --version
It prints 1.40.0. npm warns that a few packages have install scripts it
didn't run; that's fine. Agent Canvas fetches its Python services with
uv, which the VM already has.
Turn on Docker sandboxes#
Start Docker, and pull the sandbox image that matches this Agent Canvas release. It's 5.5 GB, so pulling it now spares your first conversation the wait:
sudo systemctl enable --now docker.service
docker pull ghcr.io/openhands/agent-server:1.62.0-python
Run it as a service#
Give Agent Canvas a random API key and turn on Docker sandboxes, in a file only you can read:
mkdir -p ~/.config/agent-canvas
install -m 600 /dev/null ~/.config/agent-canvas/env
cat > ~/.config/agent-canvas/env <<EOF
LOCAL_BACKEND_API_KEY=$(openssl rand -hex 32)
OH_CONVERSATION_RUNTIME=docker
OH_CONVERSATION_RUNTIME_SELECTABLE=1
OH_CONVERSATION_IMAGE=ghcr.io/openhands/agent-server:1.62.0-python
EOF
OH_CONVERSATION_RUNTIME_SELECTABLE=1 keeps the Local runtime on offer
next to Docker, conversation by conversation. Now run Agent Canvas as a
systemd service on port 8000, where exe.dev's
HTTPS proxy sends the VM's address. --public makes it
listen beyond the VM itself, which it only does with an API key:
sudo tee /etc/systemd/system/agent-canvas.service >/dev/null <<EOF
[Unit]
Description=OpenHands Agent Canvas
After=network-online.target docker.service
Wants=network-online.target docker.service
[Service]
User=$USER
WorkingDirectory=$HOME
EnvironmentFile=$HOME/.config/agent-canvas/env
Environment=PATH=$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=$HOME/.local/bin/agent-canvas --public --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now agent-canvas
The first start downloads the Agent Server and the automation service, which takes about half a minute. Check it:
until curl -sf -o /dev/null localhost:8000/; do sleep 2; done
systemctl is-active agent-canvas
curl -s -o /dev/null -w '%{http_code}\n' localhost:8000/api/settings
Expect active and 401: the API won't answer without the key.
Connect a model and check#
Save an LLM profile that points at exe.dev's LLM gateway, make it the
active one, and make Docker the default runtime. The gateway needs no real
key, so the profile holds a placeholder. The validate call sends the
model one token first, to catch a typo:
set -a; . ~/.config/agent-canvas/env; set +a
oh() { curl -fsS -H "X-Session-API-Key: $LOCAL_BACKEND_API_KEY" \
-H 'Content-Type: application/json' "localhost:8000/api/$1" "${@:2}"; echo; }
LLM='{"llm":{"model":"anthropic/claude-sonnet-5-5","base_url":"https://llm.int.exe.xyz","api_key":"exe-gateway"}}'
oh profiles/claude-sonnet-5-5/validate -d "$LLM"
oh profiles/claude-sonnet-5-5 -d "$LLM"
oh profiles/claude-sonnet-5-5/activate -X POST
oh settings -X PATCH -d '{"conversation_settings_diff":{"default_runtime":"docker"}}' >/dev/null
The first line prints {"valid":true,"error":null}. Then make a project
and run a one-line conversation in it:
mkdir -p ~/projects/hello && git -C ~/projects/hello init -q
agent=$(oh agent-profiles | jq -r .active_agent_profile_id)
id=$(oh conversations -d '{"agent_profile_id":"'"$agent"'",
"workspace":{"working_dir":"'"$HOME"'/projects/hello"},
"initial_message":{"role":"user","content":[{"type":"text",
"text":"Create hello.py that prints 6*7 and run it. Reply with its output only."}]}}' | jq -r .id)
until oh conversations/$id | jq -e '.execution_status == "finished"' >/dev/null; do sleep 5; done
oh conversations/$id/agent_final_response
docker ps --format '{{.Names}} {{.Status}}'
It prints {"response":"42"}, and docker ps lists an
agent-server-conversation-… container: the agent worked in a sandbox,
and ~/projects/hello/hello.py is on the VM.
Open OpenHands#
Port 8000 is private: only people with access to the VM get past exe.dev's login, and Agent Canvas asks for its API key on top. On your computer, print the key:
ssh my-openhands.exe.xyz "sed -n 's/^LOCAL_BACKEND_API_KEY=//p' .config/agent-canvas/env"
Open https://my-openhands.exe.xyz/ and log in to exe.dev. The page stays
blank for a few seconds the first time, then Agent Canvas asks you to
Add a backend. The host is already filled in. Give it a name, keep
Local (it means a backend you run, not OpenHands Cloud), paste the key,
and choose Next.
OpenHands then asks to Help improve OpenHands with Send anonymous usage data ticked; untick it if you'd rather not, and confirm. Keep OpenHands as the agent. Set up your LLM already shows the exe.dev profile from the install, so choose Next, then Close.
On the home screen, the chip under the message box says Docker. Choose
Open Workspace, then + Add Workspace, pick projects under
Favorites, and Add all subdirectories. Select hello, confirm, and
give it a task:
Add index.html: a one-page site for a bakery that sells only sourdough. Keep it short, then commit it.
The agent sees the folder as /workspace, inside its own container. The
panel on the right shows its files, changes, terminal and browser. Close
the tab and come back later: the conversation keeps going on the VM. On a
phone, open the same address, log in to exe.dev and add the backend the
same way.
A few things worth knowing:
- Pick a model from the menu under the message box, or add one under
Settings → LLM, Advanced tab. For exe.dev's gateway, use
anthropic/claude-opus-5-5with base URLhttps://llm.int.exe.xyz,openai/gpt-5.5withhttps://llm.int.exe.xyz/v1, orfireworks_ai/accounts/fireworks/models/glm-5p3withhttps://llm.int.exe.xyz/fireworks/inference/v1, and any text as the API key. - Switch runtimes with the Docker chip, per conversation, or set the default under Settings → Runtime, which also sets the sandbox's memory and CPU limits. Worktree runs on the VM in a fresh Git worktree.
- Preview what it builds from a Local conversation: ask the agent
to serve the app on
0.0.0.0:3000, then openhttps://my-openhands.exe.xyz:3000/. It's private until you share it. A Docker sandbox's ports stay inside its container. - Set the commit author per project, since sandboxes start without your
Git identity:
git -C ~/projects/hello config user.name "Your Name"anduser.emailon the VM. The repository's own config is inside the mounted folder, so every runtime uses it.
Warning
Anyone with the API key who gets past exe.dev's login can run any command on the VM: the Local runtime is the VM. Keep the VM private, and only share it with people you'd give your SSH key to.
Sandboxes and the Docker socket#
Agent Canvas runs as you, and you are in the docker group, so the Agent
Server can start containers. Membership in that group is as good as root on
the VM. The sandboxes don't get the socket: each one mounts only its
project folder, its conversation's state and a small home directory, runs
as your user ID with no capabilities, and can't gain privileges. Don't add
/var/run/docker.sock to a sandbox, and don't run Agent Canvas itself in a
container with the socket mounted: either hands the agent the whole VM.
The sandbox limits what the agent can touch on disk, not what it can reach: containers share the VM's network, so they can reach exe.dev's LLM gateway and every integration attached to the VM. That's what lets a sandboxed agent push through a GitHub integration, and it's why each integration should cover only the repositories you want agents to have.
Bring your repositories#
There are two ways to give OpenHands GitHub, and they suit different jobs.
exe.dev's GitHub integration, for cloning, committing, pushing and the
gh CLI. No GitHub token sits on the VM: exe.dev adds the credentials as
requests leave it. Connect your GitHub account,
then on your computer add a repository to the VM:
ssh exe.dev integrations add github --name your-repo --repository you/your-repo --attach vm:my-openhands
On the VM, clone it into ~/projects and add it as a workspace:
git clone https://github.int.exe.xyz/you/your-repo.git ~/projects/your-repo
The remote points at the integration, so the agent's pushes go through it,
from a sandbox too. For gh, tell the agent to set
GH_HOST=github.int.exe.xyz. Add --readonly to the integration for an
agent that reads but never pushes, and --act-as-user to have pushes show
as you instead of the exe.dev bot.
A GitHub token, for OpenHands' own GitHub features: the GitHub code
review and GitHub issue to PR automations and the GitHub MCP server
call GitHub's API with a token they read from Settings → Secrets. Make
a fine-grained token for only the repositories they need, with an expiry,
and save it there as the automation's setup asks. The trade-off: secrets
are exported into every conversation's environment, so any agent run can
read the token, and it's stored on the VM, encrypted with a key that sits
next to it in ~/.openhands. Use the integration for everyday Git work,
and a token only for the automations that need one.
Drive it from scripts#
The Agent Server behind Agent Canvas has a REST API, described at
/openapi.json. On the VM, with the oh helper from
Connect a model and check, start a
conversation, wait for it, and read its answer as in the setup check. To
ask from your computer instead, run the same commands over
ssh my-openhands.exe.xyz; port 8000 is behind exe.dev's login, so
scripts elsewhere can't reach it.
For work on a schedule, Automate in the sidebar runs a prompt or a script on a cron schedule, on the VM, with the same model and sandboxes.
Back up and restore#
Commit and push your code; that's the copy that matters most. For a copy of the whole VM, run this on your computer (choose a free backup name):
ssh exe.dev cp my-openhands my-openhands-backup
To keep OpenHands' settings and conversations on their own, archive them on the VM. The archive holds the API key and the key that decrypts saved secrets, so keep it private:
mkdir -p ~/backups
sudo systemctl stop agent-canvas
tar -C ~ -czf ~/backups/openhands-$(date +%F).tar.gz .openhands .config/agent-canvas projects
chmod 600 ~/backups/openhands-*.tar.gz
sudo systemctl start agent-canvas
Conversations started without a workspace keep their files in
~/workspace; add it to the tar command if you have it. To restore, on a
VM installed as above, replace 2026-11-13 with the archive's date:
sudo systemctl stop agent-canvas
rm -rf ~/.openhands
tar -C ~ -xzf ~/backups/openhands-2026-11-13.tar.gz
sudo systemctl start agent-canvas
Your conversations are listed again, and browsers that know the old API key keep working.
Update#
On the VM, update Agent Canvas, then point the sandboxes at the Agent Server image it now expects:
npm install -g @openhands/agent-canvas@latest
v=$(agent-canvas --info | awk '$1 == "agent-server:" {print $2; exit}')
docker pull ghcr.io/openhands/agent-server:$v-python
sed -i "s|^OH_CONVERSATION_IMAGE=.*|OH_CONVERSATION_IMAGE=ghcr.io/openhands/agent-server:$v-python|" ~/.config/agent-canvas/env
sudo systemctl restart agent-canvas
Restarting interrupts running conversations, so pick a quiet moment.
OpenHands releases every few days; read the
release notes first.
Each old sandbox image takes 5.5 GB: list them with docker image ls and
remove the ones you're done with with docker image rm.
Troubleshooting#
- The page stays blank. Give it ten seconds the first time. If it
still doesn't load, on the VM run
systemctl status agent-canvasandjournalctl -u agent-canvas -n 80 --no-pager;ss -ltn | grep 8000should show0.0.0.0:8000. - Adding the backend fails. The key is wrong or has a stray space:
print it again with the command from "Open OpenHands". The host must be
https://my-openhands.exe.xyz, with no port. - "Creating conversation…" never ends. The sandbox image is missing or
still downloading:
docker image lsshould list the tag in~/.config/agent-canvas/env.docker ps -aanddocker logs <container>show a sandbox that started and stopped. - A model fails. Run the
validatecall from Connect a model and check with that model's name and base URL; its error says what's wrong. - The disk fills up.
docker system dfshows what Docker holds. Remove old sandbox images, or grow the disk withssh exe.dev resize my-openhands --disk=50GBon your computer, thensudo resize2fs $(findmnt -no SOURCE /)on the VM. - Sandboxes are slow or get killed. Each may use 4 GB and 2 CPUs. Lower the limits under Settings → Runtime, or resize the VM.
agent-canvas: command not found. Open a new shell, or use~/.local/bin/agent-canvas.
For everything else, see OpenHands' Docker runtime guide and VM guide.
Frequently asked questions#
Why does OpenHands want an API key when exe.dev already asks me to log in?
They guard different things. exe.dev's login decides who reaches the VM's web port: you, and anyone you share the VM with. The API key decides who gets the Agent Server, which can run commands, read secrets and start containers. OpenHands' own VM guide asks for both a strong key and access control in front of it. Keep the VM private, and the browser remembers the key after the first visit.
Docker or Local runtime?
Docker, unless the agent needs the whole VM. A Docker conversation runs in its own container as your user, with every Linux capability dropped, 4 GB and 2 CPUs at most, and only its project folder mounted. A Local conversation runs as you on the VM itself, with sudo, Docker and every port: pick it for work like serving a dev server you open at https://my-openhands.exe.xyz:3000/. Either way the agent has the VM's network, including exe.dev's LLM gateway and any integration you attach.
Can I use Claude Code, Codex or OpenCode instead of the OpenHands agent?
Agent Canvas can drive them as ACP agents: pick one under Choose your agent, or later in Customize. exe.dev VMs come with Claude Code, Codex and OpenCode installed, and each can use exe.dev's LLM gateway on its own; see Claude Code and OpenCode. We tested the OpenHands agent, not the ACP ones.
What about the OpenHands CLI or the old docker run command?
This page installs Agent Canvas, which is what the OpenHands repository ships today, with Docker sandboxes per conversation. The terminal CLI (openhands on PyPI) is a separate project whose last release was in May 2026; we didn't test it. To script OpenHands on this VM, use the Agent Server's API, as in Drive it from scripts.
How big a VM does it need?
A new VM (2 vCPUs, 8 GB, 25 GB of disk) ran this page. The sandbox image takes 5.5 GB of disk and each running sandbox may use up to 4 GB of memory, so for several conversations at once, give the VM more with ssh exe.dev resize my-openhands --cpu=4 --memory=16GB. VMs share your plan's capacity; see Personal plan.
How much does it cost?
OpenHands is free and MIT-licensed. You pay for the VM as part of your exe.dev plan, and model usage through the LLM integration is billed to your exe.dev account. exe.dev keeps a list of models and prices.
Something's not working. Where do I look?
On the VM, systemctl status agent-canvas says whether it's running, journalctl -u agent-canvas -n 80 --no-pager shows why it stopped, and docker ps lists the sandboxes. Troubleshooting covers the usual problems.
Updated Nov 13, 2026 · tested end to end Nov 13, 2026 · Markdown · manifest.json · Something wrong? Tell us