---
title: Run agent-desktop in a Docker container
description: Build an image from the released Linux binary, start a headless Linux desktop inside a container you manage, and run agent-desktop commands against it.
url: "https://agent-browser.dev/desktop/guides/docker-container"
docs_index: /desktop/llms.txt
lastUpdated: 2026-11-11
navTitle: "Docker container"
---

> For an index of all documentation, see [/desktop/llms.txt](/desktop/llms.txt).

A container gives agent-desktop its own Linux desktop, so your commands act on apps inside the container instead of the desktop you are using. This guide builds that image from the released Linux binary and drives a text editor inside it. You run the container with Docker; the `sandbox` commands do not manage it, because the shipped provider is the [macOS VM](/desktop/sandboxes/macos).

## Requirements

- Docker installed and running.
- `curl`, and `shasum` or `sha256sum` to check the download.

## 1. Download the Linux binary

Run these commands in the directory that becomes the build context. The archive must match the architecture of the Docker daemon: `uname -m` gives the host's, and `docker info --format '{{.Architecture}}'` confirms what the daemon runs. Docker Desktop on Apple Silicon reports `aarch64`.

```bash
VERSION=1.0.2                                              # the agent-desktop release to run
case $(uname -m) in                                        # arm64 on macOS, aarch64 on Linux
  arm64 | aarch64) ARCH=aarch64 ;;
  *) ARCH=x86_64 ;;
esac
RELEASE="https://github.com/vercel-labs/agent-desktop/releases/download/v$VERSION"
TARBALL="agent-desktop-v$VERSION-$ARCH-unknown-linux-gnu.tar.gz"

curl -fsSLO "$RELEASE/$TARBALL"
curl -fsSLO "$RELEASE/checksums.txt"
grep " $TARBALL\$" checksums.txt | shasum -a 256 -c -      # sha256sum -c - where shasum is absent
tar -xzf "$TARBALL"
```

The checksum line confirms the download, and the archive adds `agent-desktop` and `agent-desktop-linux-clipboard-owner` to the directory. The binary looks for the clipboard helper beside itself.

```text
agent-desktop-v1.0.2-aarch64-unknown-linux-gnu.tar.gz: OK
```

## 2. Write the wrapper and the Dockerfile

The wrapper is not optional. A container has no logged-in desktop: no X server, no D-Bus session bus, and no accessibility bus. `agent-desktop` on its own has nothing to talk to; it fails with `PERM_DENIED` and tells you `No D-Bus session bus is reachable`. The wrapper starts the desktop on the first command and runs every desktop command inside that session.

Put both files in the same directory as the two binaries, and name the second one `agent-desktop-run`.

```dockerfile
FROM debian:bookworm-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
      xvfb openbox dbus x11-utils at-spi2-core libatk-bridge2.0-0 mousepad \
 && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home --shell /bin/bash agent \
 && mkdir -p /tmp/.X11-unix \
 && chmod 1777 /tmp/.X11-unix

COPY agent-desktop agent-desktop-linux-clipboard-owner agent-desktop-run /usr/local/bin/
RUN chmod 0755 /usr/local/bin/agent-desktop-run

USER agent
CMD ["sleep", "infinity"]
```

```sh
#!/bin/sh
set -eu

state_dir="${AGENT_DESKTOP_SESSION_DIR:-$HOME/.agent-desktop-session}"
env_file="$state_dir/env"
lock_dir="$state_dir/starting"

mkdir -p "$state_dir"
chmod 700 "$state_dir"

display_live() {
  [ -s "$env_file" ] || return 1
  . "$env_file"
  xdpyinfo >/dev/null 2>&1
}

bus_live() {
  [ -s "$env_file" ] || return 1
  . "$env_file"
  dbus-send --session --dest=org.freedesktop.DBus --type=method_call \
    --print-reply / org.freedesktop.DBus.GetId >/dev/null 2>&1
}

desktop_ready() {
  display_live && bus_live
}

start_desktop() {
  display_live || {
    rm -f /tmp/.X99-lock
    export DISPLAY=:99
    Xvfb :99 -screen 0 1280x800x24 -nolisten tcp >/dev/null 2>&1 &
    i=0
    until xdpyinfo >/dev/null 2>&1; do
      i=$((i + 1))
      [ "$i" -le 50 ] || { echo "agent-desktop-run: Xvfb did not start" >&2; exit 1; }
      sleep 0.2
    done
    openbox >/dev/null 2>&1 &
  }
  bus_live || {
    DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --session --fork --print-address)
    export DBUS_SESSION_BUS_ADDRESS
  }
  printf "export DISPLAY=%s\nexport DBUS_SESSION_BUS_ADDRESS='%s'\n" \
    "$DISPLAY" "$DBUS_SESSION_BUS_ADDRESS" > "$env_file"
}

if ! desktop_ready; then
  if mkdir "$lock_dir" 2>/dev/null; then
    trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM
    start_desktop
    rmdir "$lock_dir"
    trap - EXIT INT TERM
  else
    i=0
    until desktop_ready; do
      i=$((i + 1))
      [ "$i" -le 150 ] || {
        echo "agent-desktop-run: the desktop session did not start; remove $lock_dir and run again" >&2
        exit 1
      }
      sleep 0.2
    done
  fi
fi

. "$env_file"
exec agent-desktop "$@"
```

The image installs Xvfb, openbox, D-Bus, `x11-utils` for `xdpyinfo`, and `at-spi2-core`, which provides the accessibility bus agent-desktop reads. On the first command, `agent-desktop-run` starts a 1280x800 display on `:99`, a session bus, and the window manager, then records both addresses in `~/.agent-desktop-session/env`. Later commands reuse them. When the container restarts or the session dies, the next command starts a fresh one. Plain `agent-desktop` never does any of this, so every command that reads or acts on the desktop must go through `agent-desktop-run`.

## 3. Build and start the container

```bash
docker build -t agent-desktop-container:1.0.2 .
docker run -d --name agent-desktop-container agent-desktop-container:1.0.2
```

## 4. Run agent-desktop inside the container

There is no separate startup step and no environment sourcing. The first command starts the desktop, and every later command reuses it.

```bash
docker exec agent-desktop-container agent-desktop-run launch mousepad
docker exec agent-desktop-container agent-desktop-run snapshot --app mousepad -i
```

`snapshot` answers with the accessibility tree it read:

```json
{
  "version": "2.5",
  "ok": true,
  "command": "snapshot",
  "data": {
    "app": "mousepad",
    "complete": true,
    "ref_count": 271,
    "snapshot_id": "s1814103cz5xc6",
    "window": { "title": "Untitled 1 - Mousepad" },
    "tree": {
      "role": "window",
      "name": "Untitled 1 - Mousepad",
      "children": [
        { "ref_id": "@s1814103cz5xc6:e268", "role": "scrollarea", "available_actions": ["SetFocus", "Scroll"] }
      ]
    }
  }
}
```

The tree and the window are trimmed here. `ref_count` counts every element that received a ref, `complete` says the walk reached the whole window, and `e268` is the scroll container of the text area. Continue with the ref as you would on a host desktop; the [observe, act, verify loop](/desktop/guides/observe-act-verify) covers the rest.

## 5. Remove the container

```bash
docker rm -f agent-desktop-container
docker image rm agent-desktop-container:1.0.2
```

## Limits

- The desktop is headless: 1280x800 on display `:99`, and nothing appears on your screen.
- The image runs as the user `agent`, and everything agent-desktop stores lives in that user's `~/.agent-desktop`. Removing the container removes that state.
- The desktop session's addresses live in `~/.agent-desktop-session` inside the container. A command run as plain `agent-desktop` has no session and fails; use `agent-desktop-run`.
- The binary must match the container's architecture. Build on the machine that runs the container, or pass `--platform` to `docker run` and download the matching archive.
- `sandbox` commands are not available in the container, because the `macos` provider needs macOS. Use the container's desktop directly, as above.
- Running the container on a remote host is a manual setup today. A remote option is coming.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)