---
title: "Run Autohand Code on a VPS, Docker, and Cloud Hosts"
source: https://docs.autohand.ai/tutorials/run-autohand-on-a-vps
---

# Run Autohand Code on a VPS, Docker, and Cloud Hosts

Install Autohand Code on an always-on Linux host so you can run headless automation from SSH, cron, webhooks, Docker, AWS, DigitalOcean, or Cloudflare.

Intermediate 30 min

autohand -p "Review the latest changes, run tests, and summarize what needs attention" --output-format stream-json

## What this covers

-   Provision a small Linux host for always-on Autohand Code headless runs
-   Install Node.js, Git, Autohand Code, and provider credentials safely
-   Run one-off, scheduled, and webhook-triggered Autohand Code jobs
-   Expose a remote runner with SSH, Docker, or Cloudflare Tunnel
-   Choose between VPS, AWS EC2, DigitalOcean Droplets, Cloudflare Workers, and Cloudflare Containers

## Provider-specific tutorials

Use this page to choose the right runner pattern, then follow the dedicated provider guide for exact setup steps.

[AWS EC2](/docs/tutorials/run-autohand-on-aws-ec2.html) - Launch a Linux instance, configure security groups, and run scheduled Autohand Code jobs.

[DigitalOcean](/docs/tutorials/run-autohand-on-digitalocean.html) - Create a Droplet, add SSH access, and run a simple remote Autohand Code runner.

[Docker](/docs/tutorials/run-autohand-in-docker.html) - Build a repeatable runner image with bind mounts and runtime secrets.

[Cloudflare Tunnel](/docs/tutorials/expose-autohand-with-cloudflare-tunnel.html) - Expose a VPS or Docker runner without opening inbound ports.

[Cloudflare Worker](/docs/tutorials/trigger-autohand-from-cloudflare-worker.html) - Use an edge Worker as a secure webhook trigger for an Autohand Code runner.

[Cloudflare Containers](/docs/tutorials/run-autohand-on-cloudflare-containers.html) - Run a Worker-controlled containerized Autohand Code runner.

## Choose a runtime

Use a real Linux host when Autohand Code needs a repository checkout, Git, package managers, build tools, or a long-running shell. Use Cloudflare Workers as a secure HTTP trigger or gateway, not as the process that runs the CLI directly.

| Runtime | Use it when | Notes |
|---|---|---|
| VPS | You want the simplest always-on remote terminal | Works with Hetzner, Linode, Vultr, DigitalOcean, AWS Lightsail, or any Ubuntu host |
| AWS EC2 | You need VPC access, IAM, CloudWatch, or enterprise controls | Prefer a small Ubuntu LTS instance and restrict SSH ingress |
| DigitalOcean Droplet | You want a straightforward developer VPS | Attach an SSH key during creation and use snapshots before major automation changes |
| Docker | You want repeatable local or remote Autohand Code runners | Mount the target repository and pass credentials as environment variables or secrets |
| Cloudflare Tunnel | You want private remote access without opening inbound ports | Run Autohand Code on a VPS or container, then expose SSH or an HTTP trigger through cloudflared |
| Cloudflare Worker | You need an edge webhook that validates requests and dispatches jobs | Workers can call APIs or a runner endpoint; they are not a replacement for a full Linux CLI environment |
| Cloudflare Containers | You want a Worker-controlled containerized runtime | Use when you need a Linux-like filesystem or existing container image controlled from Worker code |

## Prerequisites

-   A Linux host running Ubuntu 22.04 LTS or newer
-   SSH key access to the host; avoid password SSH
-   Node.js 20 or newer, Git, and a package manager for your project
-   An Autohand Code-supported model provider key, such as OpenRouter, OpenAI, Anthropic-compatible gateways, AWS Bedrock, GCP Vertex AI, Ollama, MLX, or llama.cpp
-   A repository URL and a dedicated system user for automation

**Security note:** Do not run unattended automation as `root`. Create a dedicated user, keep secrets out of shell history, and prefer read-only deploy keys until you intentionally enable write access.

## Step 1: Provision a VPS

Create a small Ubuntu host with your SSH public key attached. A basic shared CPU instance is enough for most headless code review, docs, lint, and issue-triage jobs. Scale up when your project builds require more CPU or memory.

``` bash
# Local machine: generate a dedicated key for this runner
ssh-keygen -t ed25519 -C "autohand-runner" -f ~/.ssh/autohand-runner

# Connect after the provider gives you an IP address
ssh -i ~/.ssh/autohand-runner ubuntu@203.0.113.10
```

For AWS EC2, create an Ubuntu AMI instance, attach a security group that allows SSH only from trusted IPs, and use Systems Manager Session Manager if your organization forbids public SSH. For DigitalOcean, create a Droplet with your SSH key selected during provisioning.

## Step 2: Install Autohand Code on the host

Install the base runtime and Autohand Code CLI as the automation user.

``` bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl git build-essential

# Install Node.js from your preferred trusted source, then verify:
node --version
npm --version

# Install Autohand Code
npm install -g autohand-cli
autohand --version
```

If your team uses Homebrew on Linux, you can install Autohand Code with Homebrew instead. Keep the install method consistent across hosts so upgrades are predictable.

## Step 3: Configure credentials

Use environment variables or a config file owned by the runner user. Keep the file readable only by that user.

``` bash
mkdir -p ~/.autohand
chmod 700 ~/.autohand

cat > ~/.autohand/env <<'EOF'
export OPENROUTER_API_KEY="sk-or-v1-..."
export AUTOHAND_MODEL="anthropic/claude-sonnet-4"
export AUTOHAND_OUTPUT_FORMAT="stream-json"
EOF
chmod 600 ~/.autohand/env

source ~/.autohand/env
```

If you store provider configuration in `~/.autohand/config.json`, keep that file out of repository checkouts and backups that are shared broadly.

## Step 4: Clone the project

Use a deploy key or machine user token. Prefer least privilege: read-only for review jobs, write access only for jobs that push branches or commits.

``` bash
mkdir -p ~/work
cd ~/work
git clone git@github.com:your-org/your-repo.git
cd your-repo

# Optional: let Autohand Code learn project conventions
autohand -p "Read this repo and create or update AGENTS.md with build, test, and style guidance" --restricted
```

## Step 5: Run Autohand Code headlessly

Start with read-only analysis, then enable edits after the runner is configured correctly.

``` bash
# Read-only review
autohand -p "Review this repository for failing tests, security risks, and obvious maintenance issues" --restricted

# Stream progress for remote logs
autohand -p "Run the test suite and fix straightforward failures" --output-format stream-json

# Allow approved automation in a trusted runner
autohand -p "Update docs for the latest CLI changes, run tests, and commit the result" --yes --auto-commit
```

Use `--restricted` for review-only jobs, `--dry-run` for previews, and `--yes` only on runners where repository write access and shell access are intentionally scoped.

## Step 6: Schedule jobs

For simple recurring automation, use cron or systemd timers. Write logs to a directory owned by the runner user.

``` bash
mkdir -p ~/logs
crontab -e
```

``` bash
# Every weekday at 08:00 UTC: pull, review, and write JSON logs
0 8 * * 1-5 . $HOME/.autohand/env && cd $HOME/work/your-repo && git pull --ff-only && autohand -p "Review changes from the last 24 hours and summarize issues" --restricted --output-format json >> $HOME/logs/daily-review.jsonl 2>&1
```

## Run Autohand Code in Docker

Use Docker when you want a repeatable runner image or you need to keep host dependencies isolated. Mount the repository into the container and pass provider credentials at runtime.

``` dockerfile
FROM node:22-bookworm

RUN apt-get update \
  && apt-get install -y --no-install-recommends git openssh-client ca-certificates build-essential \
  && rm -rf /var/lib/apt/lists/*

RUN npm install -g autohand-cli

WORKDIR /workspace
ENTRYPOINT ["autohand"]
```

``` bash
docker build -t autohand-runner .

docker run --rm -it \
  -e OPENROUTER_API_KEY \
  -e AUTOHAND_MODEL="anthropic/claude-sonnet-4" \
  -v "$PWD:/workspace" \
  autohand-runner \
  -p "Review this repository and suggest the highest-risk fixes" --restricted
```

For a remote Docker host, clone the repository on the host, run the container there, and use SSH or Cloudflare Tunnel for access. Avoid baking API keys into the image.

## Add a webhook runner

A minimal HTTP trigger lets GitHub Actions, Slack, Linear, or a Cloudflare Worker dispatch jobs to the VPS without giving every service SSH access.

``` js
// server.mjs
import { createServer } from "node:http";
import { spawn } from "node:child_process";

const token = process.env.AUTOHAND_RUNNER_TOKEN;
const repoDir = process.env.AUTOHAND_REPO_DIR || "/home/autohand/work/your-repo";

createServer((req, res) => {
  if (req.method !== "POST" || req.headers.authorization !== `Bearer ${token}`) {
    res.writeHead(401);
    res.end("unauthorized");
    return;
  }

  const job = spawn("autohand", [
    "-p",
    "Review the latest repository state and summarize action items",
    "--restricted",
    "--output-format",
    "stream-json"
  ], { cwd: repoDir, env: process.env });

  res.writeHead(202, { "Content-Type": "text/plain" });
  job.stdout.on("data", chunk => process.stdout.write(chunk));
  job.stderr.on("data", chunk => process.stderr.write(chunk));
  job.on("exit", code => console.log(`autohand exited ${code}`));
  res.end("queued\n");
}).listen(8787, "127.0.0.1");
```

``` bash
AUTOHAND_RUNNER_TOKEN="$(openssl rand -hex 32)" node server.mjs
```

Keep the listener bound to `127.0.0.1` unless it is behind a reverse proxy, VPN, or Cloudflare Tunnel with authentication.

## Expose remote access with Cloudflare Tunnel

Cloudflare Tunnel is a good fit when the runner should not expose inbound ports. Install `cloudflared` on the VPS and publish either SSH or the webhook runner through an outbound tunnel.

``` bash
# On the VPS
cloudflared tunnel login
cloudflared tunnel create autohand-runner

# Route a private webhook hostname to the local runner
cloudflared tunnel route dns autohand-runner autohand-runner.example.com
```

``` yaml
# ~/.cloudflared/config.yml
tunnel: autohand-runner
credentials-file: /home/autohand/.cloudflared/autohand-runner.json

ingress:
  - hostname: autohand-runner.example.com
    service: http://127.0.0.1:8787
  - service: http_status:404
```

``` bash
cloudflared tunnel run autohand-runner
```

Put Cloudflare Access in front of the hostname for human-triggered workflows. For machine-triggered webhooks, validate HMAC signatures or bearer tokens in the runner before starting Autohand Code.

## Use a Cloudflare Worker as a trigger

A Worker can validate an incoming webhook, apply rate limits, hide the runner origin, and dispatch a request to your VPS or Cloudflare Container. It should not be treated as a general Linux shell for the Autohand Code CLI.

``` js
export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("method not allowed", { status: 405 });
    }

    const auth = request.headers.get("authorization");
    if (auth !== `Bearer ${env.RUNNER_TRIGGER_TOKEN}`) {
      return new Response("unauthorized", { status: 401 });
    }

    const response = await fetch(env.RUNNER_URL, {
      method: "POST",
      headers: {
        authorization: `Bearer ${env.RUNNER_BACKEND_TOKEN}`,
        "content-type": "application/json"
      },
      body: await request.text()
    });

    return new Response(await response.text(), { status: response.status });
  }
};
```

``` bash
wrangler secret put RUNNER_TRIGGER_TOKEN
wrangler secret put RUNNER_BACKEND_TOKEN
wrangler secret put RUNNER_URL
wrangler deploy
```

## Use Cloudflare Containers when you need a container runtime

If the runner must execute inside Cloudflare instead of a VPS, use Cloudflare Containers rather than a plain Worker. Containers are controlled from Worker code and are intended for workloads that need a full filesystem, a specific runtime, or an existing container image.

``` js
import { Container, getContainer } from "@cloudflare/containers";

export class AutohandCodeContainer extends Container {
  defaultPort = 8787;
  sleepAfter = "10m";
}

export default {
  async fetch(request, env) {
    const id = request.headers.get("x-repo") || "default";
    const instance = getContainer(env.AUTOHAND_CONTAINER, id);
    return instance.fetch(request);
  }
};
```

``` toml
name = "autohand-container-runner"
main = "src/index.js"
compatibility_date = "2026-06-18"

[[containers]]
class_name = "AutohandCodeContainer"
image = "./Dockerfile"
max_instances = 5

[[durable_objects.bindings]]
class_name = "AutohandCodeContainer"
name = "AUTOHAND_CONTAINER"

[[migrations]]
new_sqlite_classes = [ "AutohandCodeContainer" ]
tag = "v1"
```

Start with the VPS or Docker path first. Move to Cloudflare Containers when you specifically need Worker-controlled routing, on-demand containers, or Cloudflare-native deployment.

## Operations checklist

-   **Identity:** Use a dedicated Unix user, Git deploy key, and model provider key for the runner.
-   **Network:** Restrict SSH by IP, VPN, or Cloudflare Tunnel. Do not expose raw runner ports to the internet.
-   **Permissions:** Default scheduled jobs to `--restricted`. Enable `--yes` only for trusted repos and narrow tasks.
-   **Logs:** Prefer `--output-format stream-json` for long jobs and ship logs to CloudWatch, journald, or your normal log pipeline.
-   **Updates:** Pin the CLI version for production runners, test upgrades in a disposable clone, then roll forward.
-   **Recovery:** Keep snapshots, use git branches for write jobs, and make sure `/undo` or git revert can recover from unwanted changes.

## Next steps

[Headless Mode](/docs/working-with-autohand-code/headless-mode.html) - Learn all headless flags, output formats, and session controls.

[CI/CD Automation](/docs/guides/ci-cd-automation.html) - Wire Autohand Code into pipeline jobs and release workflows.

[CLI Reference](/docs/working-with-autohand-code/cli-reference.html) - Review commands, flags, tools, and slash commands.