TL;DR
-
リモートの headless Linux(GUI のない Linux)で、通常の Orca Desktop(Electron 製の GUI アプリ)を仮想ディスプレイ Xvfb 上に常駐させました。これにより、Claude Code・Codex CLI・Antigravity CLI(agy)の3つを、Android の Orca Mobile からまとめて操作できるようになりました。
-
画面操作が必要になるのは、初回サインインと再サインインのときだけです。そのときだけ x11vnc を起動し、SSH トンネル経由で VNC 接続します。それ以外のときは SSH も VNC も閉じておき、スマホからは Orca Relay(Orca の中継サービス) 経由で接続します。
-
コンテナ環境だったため、次の3点でつまずきました。
- AppImage の起動に FUSE が必要
- Electron(Chromium)の sandbox
- サインイン用ブラウザ(WebKitGTK)の bubblewrap sandbox
後ろの2つは sandbox を無効化して回避 しています。セキュリティ上のトレードオフがあるので、セキュリティの節を必ず読んでください。
-
この環境では、Orca のプロセスを再起動するたびに Orca アカウントへの再サインインを求められました。原因は特定できていません。そこで、Orca は基本的に止めずに動かし続け、落ちたときだけ VNC で再サインインする運用にしています。
本記事の手順は Orca 1.4.204 で検証しました。その後、1.4.221(2026-10-05 リリース)でも、同じ手順で起動・サインイン・Relay 経由での Android からの接続までできることを確認しています。執筆時点(2026-10-10)の最新版は 1.4.224 ですが、こちらでは確認していません。Orca は頻繁に更新されるため、今後のバージョンでは workaround の一部が不要になったり、挙動が変わったりしている可能性があります。
はじめに
リモートの coding agent をスマホから操作したい
開発環境と計算資源を置いたリモートの Linux サーバーで、Claude Code、Codex CLI、Antigravity CLI(agy)といった CLI の coding agent を並行して動かしています。時間のかかるタスクを任せたまま席を離れることが多いので、進み具合の確認や追加の指示をスマホから出せるようにしたいと考えました。
スマホからの SSH + tmux は操作が煩わしい
一番手軽なのは、スマホの SSH クライアントから tmux のセッションに attach する方法です。これでも操作はできますが、日常的に使うと煩わしい点がいくつもあります。
- 日本語入力: SSH の通信そのものは、UTF-8 の日本語をそのまま通します。問題になるのは入力の手前の部分です。スマホの IME(Input Method Editor: かな漢字変換などの入力システム)、ターミナルアプリ、TUI(Text User Interface: 端末上で動く対話型の画面)の組み合わせによっては、変換中の文字列の扱いや、Enter キーが「変換の確定」なのか「送信」なのかの解釈が食い違うことがあります。Codex でも、IME の変換確定と送信がぶつかる issue が報告されています(openai/codex#23440。VS Code 拡張での事例)。
- 修飾キー: Ctrl・Shift・Esc・矢印キーなど、agent の TUI で使うキーを、スマホのソフトウェアキーボードで正確に入力するのは手間がかかります。
- 画面の狭さ: agent ごとに作りの違う TUI を、スマホの小さな画面で読みながら操作することになります。
各社の公式リモート操作機能では、操作する場所が分かれる
最近は、各 agent にもスマホから操作するための公式機能が用意されるようになりました。2026-10-10 時点で公式ドキュメントを確認した結果は次のとおりです。
| agent | 公式のスマホ操作手段 | headless Linux 上の CLI セッションとの関係 |
|---|---|---|
| Claude Code |
/remote-control または claude remote-control で、Claude のモバイルアプリや Web からローカルのセッションを操作できる |
CLI セッションそのものをスマホから操作する導線がある |
| Codex | ChatGPT Remote。スマホから ChatGPT の desktop app を操作する | host(操作される側)は macOS / Windows の ChatGPT desktop app に限られる。Codex CLI や IDE 拡張からは setup できない。headless Linux 上で直接起動した Codex CLI セッションにスマホから入る公式の導線は、ドキュメントには見当たらなかった |
| Antigravity(agy) |
/remote-control、agy --remote-control、agy remote-control start で CLI セッションの URL が発行され、スマホやブラウザから操作できる |
CLI セッションそのものに対応している。常駐デーモンは OS のサービスとして登録され、Linux では systemd を使う |
各列の意味は次のとおりです。
- 「公式のスマホ操作手段」: 各社の公式ドキュメントに書かれている機能
- 「headless Linux 上の CLI セッションとの関係」: 本記事の構成(リモート Linux 上で CLI を直接起動する構成)にそのまま使えるかどうか
出典は次のとおりです。
- Claude Code: Claude Code power user tips
- Codex: ChatGPT Remote connections
- Antigravity: Antigravity Remote Control
つまり、「Codex や Antigravity はスマホに対応していない」というわけではありません。問題は、Claude Code は Claude のアプリ、Codex は ChatGPT desktop 経由、agy は Antigravity のページと、操作する場所が agent ごとに分かれてしまうことです。特に Codex は、今回のように headless Linux 上で CLI を直接動かす使い方だと、公式の導線を使えません。
Orca で操作画面をまとめる
そこで、Orca を共通の操作画面(frontend)として使うことにしました。
Orca(stablyai/orca)は、複数の CLI coding agent を1つの workspace で並べて扱える OSS(オープンソースソフトウェア)のデスクトップアプリです。iOS / Android 向けの Mobile Companion(以下 Orca Mobile)があり、デスクトップアプリとペアリングすると、スマホから各 agent の様子を見たり指示を出したりできます。さらに Orca Relay を使えば、スマホとデスクトップが同じ LAN(Local Area Network)や VPN(Virtual Private Network)の中になくても接続できます。ただし Relay の告知では、この点に「一般的なケースでは」という限定が付いています(stablyai/orca#6722)。
実際に使ってみて、次の2点を確認しました。
- Orca Mobile から、日本語入力も含めて3つの agent すべてを普段どおり操作できる
- Ctrl や Shift などの修飾キーを、Orca Mobile のボタンで送信できる
これで、SSH + tmux で煩わしかった点はほぼ解消しました。
残る課題は、動かしたいサーバーが GUI のない Linux コンテナだったことです。以下では、そこに通常の Orca Desktop を常駐させるまでの手順と、途中でつまずいた点をまとめます。
完成構成
- Xvfb・Openbox・Orca は tmux のセッション内で動かし続けます。SSH を切断してもプロセスは止まりません。
- 普段は SSH も VNC も使いません。スマホの Orca Mobile は Relay を経由して接続します。
- GUI の操作が必要なとき(初回サインインと再サインイン)だけ
x11vncを起動し、Windows PC から SSH トンネル経由で UltraVNC を使って接続します。
検証環境
Orca: 1.4.204(AppImage を展開して実行)。1.4.221 でも起動・サインイン・Relay 接続を確認
OS: Ubuntu 24.04.5 LTS ベースのコンテナ
PID 1: systemd ではない(コンテナのエントリポイント)
FUSE: 使用不可(/dev/fuse なし、modprobe 不可)
ディスプレイ: Xvfb :99(1920x1080x24)
ウィンドウマネージャ: Openbox
VNC サーバー: x11vnc(localhost のみで待ち受け)
VNC クライアント: UltraVNC Viewer(Windows)
VNC への経路: SSH ローカルポートフォワード
スマホ: Android 版 Orca Mobile + Orca Relay
コンテナの管理には Coder を使っていますが、Coder は Docker のホストとして使っているだけで、本記事の内容には関係しません。後で述べるつまずきのうち、次の4点は特権を与えられていないコンテナで起きる制約です。VM(仮想マシン)や物理マシンの Ubuntu では当てはまらない可能性があります。
- FUSE が使えない
- Chromium の sandbox を無効化する必要がある
- WebKitGTK の sandbox を無効化する必要がある
- systemd がない
なぜ orca serve ではなく Orca Desktop なのか
Orca には、headless Linux 向けの公式の実行方法として orca serve があります。公式ドキュメントの Remote Orca Servers でも、headless Linux や VM では orca serve(Linux では orca-ide serve)を使うよう案内されています。headless で Orca を動かすなら、まずこちらを検討するのが順当です。
ただし、同じページにあるスマホとの接続手順("Mobile from a headless server")を見ると、--mobile-pairing を付けて QR コードを表示し、スマホも同じ tailnet(Tailscale のネットワーク)に参加させることが前提になっています。serve モードで Orca Relay を使えるかどうかは、このページには書かれていません。
また、2026-10-07 まで GitHub リポジトリにあった Headless Linux Server ガイド(削除前の版)には、次の記述がありました。
Background push notifications to a paired phone do not fire from a headless server: agent-completion detection runs in the desktop renderer, which is not started in serve mode, so nothing reaches the push gateway even though the phone registers successfully.
serve モードでは desktop の renderer が起動しないため、renderer に依存する機能(agent の完了を検知してスマホへ push 通知を送る機能など)は動作しない、という内容です。現行のドキュメントにこの記述が残っているかは確認できていません。
一方、Relay の設定は Desktop アプリの Settings → Mobile にあり、Relay を経由すれば、端末同士が同じ LAN や VPN の中になくても接続できます。また、Remote Orca Servers のページは、サーバー機で Desktop アプリを動かす構成も正式な選択肢の一つとして挙げています(想定されているのは、ノート PC や Mac mini など画面のあるマシンです)。
今回は Tailscale を導入せずに、Desktop のアカウント、Settings → Mobile、Orca Relay という、Desktop で確実に動く組み合わせを使いたいと考えました。そのため、通常の Desktop アプリをそのまま仮想ディスプレイ上で動かす構成を選んでいます。orca serve を使うべきでないという意味ではありません。Tailscale を使える環境で、serve モードで目的を果たせるなら、そちらのほうが素直な構成です。
構築手順
1. 必要なパッケージ
sudo apt-get update
sudo apt-get install -y \
tmux \
xvfb \
x11vnc \
openbox \
dbus-x11 \
x11-utils \
xdg-utils
# Orca のサインイン(OAuth)で開くブラウザとして使う
sudo apt-get install -y epiphany-browser
各パッケージの用途は次のとおりです。
-
x11-utils: 動作確認に使うxdpyinfoとxwininfoが含まれる -
xdg-utils: 外部ブラウザを開くためのコマンド群 - Epiphany(GNOME Web): WebKitGTK ベースのブラウザ。Orca のサインイン時に開かれる外部ブラウザとして使う
2. Orca AppImage の展開
AppImage は通常、FUSE(Filesystem in Userspace)を使って自分自身をマウントしてから起動します。今回のコンテナには FUSE がなく、そのまま実行すると次のエラーで止まりました。
Error: No suitable fusermount binary found on the $PATH
fuse: device not found, try 'modprobe fuse' first
Cannot mount AppImage
そこで、AppImage を展開してから使います。この方法は、Orca の Headless Linux Server ガイド(削除前の版)でも案内されていました。同ガイドには、「Docker には FUSE デバイスがないことが多いが、--appimage-extract か --appimage-extract-and-run を使えば特権コンテナは不要」という趣旨の記述もあります。
cd /opt/orca
sudo ./orca-linux.AppImage --appimage-extract
sudo chmod -R a+rX /opt/orca/squashfs-root
展開すると、/opt/orca/squashfs-root/orca-ide が Orca の実行ファイルになります。
コンテナの起動時に --cap-add SYS_ADMIN --device /dev/fuse などを付けて FUSE を使えるようにする方法も、ネット上でよく見かけます。しかし AppImage の公式ドキュメントは、この方法を**安全ではない(insecure)**としています(AppImage docs: FUSE)。コンテナに権限を追加するより、AppImage を展開して使うほうが影響範囲を小さくできます。なお、筆者は今回のコンテナの権限設定を変更できる立場になく、権限を追加する方法は試していません。
3. Xvfb による仮想ディスプレイの起動
Xvfb :99 -screen 0 1920x1080x24 -nolisten tcp
別のターミナルで次のコマンドを実行し、ディスプレイが使える状態か確認します。
DISPLAY=:99 xdpyinfo >/dev/null && echo "X display OK"
4. Openbox の起動
Xvfb は画面を用意するだけなので、ウィンドウを管理するために、軽量なウィンドウマネージャの Openbox を起動します。
DISPLAY=:99 openbox
仮想ディスプレイ上のウィンドウの一覧は、次のコマンドで確認できます。
DISPLAY=:99 xwininfo -root -tree
5. x11vnc の準備(必要なときだけ使う VNC サーバー)
VNC(Virtual Network Computing)のパスワードファイルを、最初に一度だけ作成します。
mkdir -p ~/.vnc
x11vnc -storepasswd ~/.vnc/passwd
x11vnc の起動コマンドは次のとおりです。
x11vnc \
-display :99 \
-localhost \
-forever \
-shared \
-rfbport 5900 \
-rfbauth ~/.vnc/passwd
-localhost は必ず指定してください。VNC のポートを外部のネットワークに直接公開しないためです。外部からは、次の手順の SSH トンネルを経由してだけ接続します(詳しくは セキュリティ を参照)。
6. Windows PC からの SSH トンネルと UltraVNC 接続
Windows PC の PowerShell で次のコマンドを実行し、SSH トンネルを張ります。my-devbox は ~/.ssh/config に登録したサーバーのホスト名です。
ssh -N -L 5900:127.0.0.1:5900 my-devbox
-N を付けるとリモートでコマンドを実行しないので、ターミナルは止まったように見えます。これはポート転送が動いている正常な状態です。
続いて UltraVNC Viewer を起動し、接続先に次のように入力します。
127.0.0.1::5900
UltraVNC では、ポート番号を直接指定するときに host::port(コロン2つ)と書きます。
VNC 経由でもテキストのコピー&ペーストができるので、サインイン時に URL やコードを受け渡すのに便利です。
7. Orca Desktop の起動
試行錯誤の末、GUI が表示され、アカウントのサインインまでできた起動コマンドは次のとおりです。
dbus-run-session -- env \
-u ELECTRON_RUN_AS_NODE \
-u WAYLAND_DISPLAY \
DISPLAY=:99 \
XDG_SESSION_TYPE=x11 \
GDK_BACKEND=x11 \
LIBGL_ALWAYS_SOFTWARE=1 \
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 \
WEBKIT_DISABLE_COMPOSITING_MODE=1 \
/opt/orca/squashfs-root/orca-ide \
--no-sandbox \
--disable-gpu \
2>&1 | tee /tmp/orca-gui.log
--no-sandbox と WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 は、それぞれ Chromium と WebKit の sandbox(プロセスを隔離する仕組み)を無効化します(詳しくは セキュリティ を参照)。
各項目の意図は下の表のとおりです。この組み合わせで動くことは確認しましたが、項目を1つずつ外して、それぞれが本当に必要かを確かめたわけではありません。 「根拠」列は、その項目を入れた理由の種類を表し、次の2通りがあります。
- 「ログで確認」: 外した状態でエラーログが出ており、追加するとエラーが消えた
- 「予防的」: headless 環境で問題になりやすいと考えて入れた(必要かどうかは未検証)
| 項目 | 意図 | 根拠 |
|---|---|---|
dbus-run-session |
headless 環境には session D-Bus(アプリ間通信のバス)がないので、Orca 用に1つ起動する | 予防的 |
-u ELECTRON_RUN_AS_NODE |
この変数が残っていると、Electron が GUI アプリではなく Node.js として動いてしまうので、確実に外す | 予防的 |
-u WAYLAND_DISPLAY, XDG_SESSION_TYPE=x11, GDK_BACKEND=x11
|
Wayland ではなく X11(Xvfb)を使わせる | 予防的 |
LIBGL_ALWAYS_SOFTWARE=1, --disable-gpu
|
GPU のない Xvfb 上で、描画をソフトウェアで行わせる | 予防的 |
--no-sandbox |
Chromium の sandbox を無効化する。コンテナ内で sandbox の初期化に失敗する問題を避ける | 予防的(エラーログは残していない) |
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 |
サインイン時に開くブラウザ(Epiphany / WebKitGTK)の sandbox を無効化する |
ログで確認(後述の bwrap エラーが消え、サインインに成功した) |
WEBKIT_DISABLE_COMPOSITING_MODE=1 |
WebKit の compositing(GPU を使った画面合成)を無効化する | 予防的(上の項目と同時に追加した) |
なお、AppImage に同梱の起動スクリプト AppRun から起動したときは、GUI が表示されませんでした(詳しくは 詰まった点 を参照)。上のコマンドのどの要素がこの問題を解消したのかは、切り分けていません。
8. サインインと Relay でのペアリング
- VNC で Orca Desktop の画面を開き、Sign in を押します。Epiphany が開くので、Orca アカウントで認証します。
- Orca Desktop で Settings → Mobile を開き、Orca Relay を設定します。
- 画面に表示されるペアリング情報を使って、Android の Orca Mobile とペアリングします。
- ペアリングが済んだら、次の3つを順に閉じます。
- UltraVNC Viewer
- SSH トンネル
- SSH 接続
- その状態で、Android の Orca Mobile からリモートの Orca に接続でき、各 agent を操作できることを確認します。
ペアリング用の URL(orca://pair?code=...)や QR コードは、それだけで接続できてしまう認証情報です。スクリーンショットやブログ記事に実際の値を載せないよう注意してください。
常駐化: tmux + orca-headless スクリプト
ここまでの起動手順を毎回手で実行するのは手間がかかります。そこで、tmux のセッション orca の中で、Xvfb・Openbox・Orca・x11vnc をそれぞれ別の window として起動するスクリプトを用意しました。tmux の中で動かしていれば、SSH を切断しても各プロセスは動き続けます。
このスクリプトは、起動時に tmux、Xvfb、openbox、dbus-run-session があるかを確認します。ほかに次のコマンドも使うので、インストールされているか確認してください。
-
xdpyinfo(x11-utils) -
pgrep(procps) -
ss(iproute2)
~/.local/bin/orca-headless(全文)
#!/usr/bin/env bash
set -euo pipefail
SESSION="orca"
DISPLAY_NUM="99"
DISPLAY=":${DISPLAY_NUM}"
SCREEN_RESOLUTION="1920x1080x24"
ORCA_BIN="/opt/orca/squashfs-root/orca-ide"
VNC_PORT="5900"
VNC_PASSWD="${HOME}/.vnc/passwd"
# 案内メッセージに表示する、手元PCから見たサーバーのSSHホスト名
SSH_HOST="my-devbox"
ORCA_LOG="/tmp/orca-gui.log"
OPENBOX_LOG="/tmp/orca-openbox.log"
VNC_LOG="/tmp/orca-vnc.log"
usage() {
cat <<EOF
Usage:
$0 start [--vnc]
$0 stop
$0 restart [--vnc]
$0 status
$0 vnc-on
$0 vnc-off
$0 logs
$0 attach
Examples:
$0 start
$0 start --vnc
$0 vnc-on
$0 vnc-off
$0 status
Windows VNC tunnel:
ssh -N -L 5900:127.0.0.1:5900 ${SSH_HOST}
UltraVNC Viewer:
127.0.0.1::5900
EOF
}
need_command() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: required command not found: $1" >&2
exit 1
fi
}
check_dependencies() {
need_command tmux
need_command Xvfb
need_command openbox
need_command dbus-run-session
if [[ ! -x "${ORCA_BIN}" ]]; then
echo "ERROR: Orca binary not found: ${ORCA_BIN}" >&2
exit 1
fi
}
session_exists() {
tmux has-session -t "${SESSION}" 2>/dev/null
}
window_exists() {
local name="$1"
session_exists || return 1
tmux list-windows -t "${SESSION}" -F '#{window_name}' |
grep -Fxq "${name}"
}
wait_for_x() {
local tries=30
for ((i=1; i<=tries; i++)); do
if DISPLAY="${DISPLAY}" xdpyinfo >/dev/null 2>&1; then
return 0
fi
sleep 0.2
done
echo "ERROR: Xvfb did not become ready on DISPLAY=${DISPLAY}" >&2
return 1
}
start_display() {
if window_exists display; then
echo "Xvfb: already running in tmux"
return
fi
echo "Starting Xvfb on ${DISPLAY}..."
if ! session_exists; then
tmux new-session -d -s "${SESSION}" -n display
else
tmux new-window -d -t "${SESSION}" -n display
fi
tmux send-keys -t "${SESSION}:display" \
"exec Xvfb ${DISPLAY} -screen 0 ${SCREEN_RESOLUTION} -nolisten tcp" \
Enter
wait_for_x
echo "Xvfb: ready"
}
start_openbox() {
if window_exists openbox; then
echo "Openbox: already running in tmux"
return
fi
echo "Starting Openbox..."
tmux new-window -d -t "${SESSION}" -n openbox
tmux send-keys -t "${SESSION}:openbox" \
"export DISPLAY=${DISPLAY}; exec openbox >${OPENBOX_LOG} 2>&1" \
Enter
sleep 0.5
}
start_orca() {
if window_exists desktop; then
echo "Orca: already running in tmux"
return
fi
echo "Starting Orca Desktop..."
tmux new-window -d -t "${SESSION}" -n desktop
local cmd
cmd=$(cat <<EOF
exec dbus-run-session -- env \
-u ELECTRON_RUN_AS_NODE \
-u WAYLAND_DISPLAY \
DISPLAY=${DISPLAY} \
XDG_SESSION_TYPE=x11 \
GDK_BACKEND=x11 \
LIBGL_ALWAYS_SOFTWARE=1 \
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 \
WEBKIT_DISABLE_COMPOSITING_MODE=1 \
${ORCA_BIN} \
--no-sandbox \
--disable-gpu \
>>${ORCA_LOG} 2>&1
EOF
)
tmux send-keys -t "${SESSION}:desktop" "${cmd}" Enter
sleep 2
if pgrep -f "${ORCA_BIN}" >/dev/null 2>&1; then
echo "Orca: started"
else
echo "WARNING: Orca process not detected."
echo "Check:"
echo " tail -n 100 ${ORCA_LOG}"
fi
}
start_vnc() {
need_command x11vnc
if window_exists vnc; then
echo "VNC: already running"
return
fi
if [[ ! -f "${VNC_PASSWD}" ]]; then
echo "ERROR: VNC password file does not exist:"
echo " ${VNC_PASSWD}"
echo
echo "Create it once with:"
echo " mkdir -p ~/.vnc"
echo " x11vnc -storepasswd ~/.vnc/passwd"
exit 1
fi
start_display
echo "Starting x11vnc on localhost:${VNC_PORT}..."
tmux new-window -d -t "${SESSION}" -n vnc
tmux send-keys -t "${SESSION}:vnc" \
"exec x11vnc \
-display ${DISPLAY} \
-localhost \
-forever \
-shared \
-rfbport ${VNC_PORT} \
-rfbauth ${VNC_PASSWD} \
>>${VNC_LOG} 2>&1" \
Enter
sleep 0.5
echo
echo "VNC enabled."
echo
echo "On Windows:"
echo " ssh -N -L ${VNC_PORT}:127.0.0.1:${VNC_PORT} ${SSH_HOST}"
echo
echo "Then UltraVNC Viewer:"
echo " 127.0.0.1::${VNC_PORT}"
}
stop_vnc() {
if window_exists vnc; then
echo "Stopping VNC..."
tmux kill-window -t "${SESSION}:vnc"
else
echo "VNC: not running"
fi
}
start_all() {
local enable_vnc="${1:-false}"
check_dependencies
need_command xdpyinfo
start_display
start_openbox
start_orca
if [[ "${enable_vnc}" == "true" ]]; then
start_vnc
fi
echo
status
}
stop_all() {
if session_exists; then
echo "Stopping Orca tmux session..."
tmux kill-session -t "${SESSION}"
else
echo "Orca tmux session is not running."
fi
}
status_item() {
local label="$1"
local pattern="$2"
if pgrep -f "${pattern}" >/dev/null 2>&1; then
printf "%-12s RUNNING\n" "${label}"
else
printf "%-12s STOPPED\n" "${label}"
fi
}
status() {
echo "===== Orca headless status ====="
if session_exists; then
echo "tmux RUNNING"
echo
tmux list-windows -t "${SESSION}" \
-F ' #{window_index}: #{window_name} #{?window_active,[active],}'
else
echo "tmux STOPPED"
fi
echo
status_item "Xvfb" "Xvfb ${DISPLAY}"
status_item "Openbox" "openbox"
status_item "Orca" "${ORCA_BIN}"
status_item "x11vnc" "x11vnc.*-display ${DISPLAY}"
echo
echo "DISPLAY=${DISPLAY}"
echo "Orca log: ${ORCA_LOG}"
if ss -ltn 2>/dev/null | grep -q "127.0.0.1:${VNC_PORT}"; then
echo "VNC port: 127.0.0.1:${VNC_PORT} LISTENING"
else
echo "VNC port: not listening"
fi
}
show_logs() {
touch "${ORCA_LOG}"
tail -f "${ORCA_LOG}"
}
attach_tmux() {
if ! session_exists; then
echo "ERROR: tmux session '${SESSION}' does not exist." >&2
exit 1
fi
exec tmux attach -t "${SESSION}"
}
COMMAND="${1:-}"
shift || true
case "${COMMAND}" in
start)
ENABLE_VNC=false
while [[ $# -gt 0 ]]; do
case "$1" in
--vnc)
ENABLE_VNC=true
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
shift
done
start_all "${ENABLE_VNC}"
;;
restart)
ENABLE_VNC=false
while [[ $# -gt 0 ]]; do
case "$1" in
--vnc)
ENABLE_VNC=true
;;
*)
echo "Unknown option: $1" >&2
usage
exit 1
;;
esac
shift
done
stop_all
start_all "${ENABLE_VNC}"
;;
stop)
stop_all
;;
status)
status
;;
vnc-on)
check_dependencies
start_vnc
;;
vnc-off)
stop_vnc
;;
logs)
show_logs
;;
attach)
attach_tmux
;;
*)
usage
exit 1
;;
esac
スクリプトの設置:
mkdir -p ~/.local/bin
# 上のスクリプトを ~/.local/bin/orca-headless に保存してから
chmod +x ~/.local/bin/orca-headless
普段使うコマンド:
orca-headless start # Xvfb + Openbox + Orca を起動
orca-headless start --vnc # 上に加えて x11vnc も起動(初回サインイン時)
orca-headless vnc-on # 再サインインが必要なときに VNC だけ起動
orca-headless vnc-off # Relay の接続が戻ったら VNC を止める
orca-headless status # 各プロセスと VNC ポートの状態
orca-headless logs # Orca のログを tail -f
orca-headless attach # tmux セッションに attach
orca-headless stop と restart は Orca のプロセスを終了させます。後で述べるとおり、この環境では再起動するたびに再サインインが必要になります。
運用上の制約
Orca を再起動すると再サインインが必要になる
Orca のプロセスが動いている間は、問題なく使えます。
VNC を閉じる / SSH トンネルを閉じる / SSH を切断する
↓
Orca のプロセスは生きたまま → Relay も生きたまま → Android から再接続できる
一方、Orca のプロセスを終了して起動し直すと、毎回 Orca アカウントへの再サインインを求められます。サインインが済むまでは、Relay 経由での接続もできません。
この環境では、プロセスを再起動するとアカウントのセッションが復元されませんでした。次の2つを試しましたが、どちらでも解消しませんでした。
-
GNOME Keyring: Epiphany の起動時に
org.freedesktop.secretsがないという警告が出ていたので、gnome-keyring-daemon --start --components=secretsを起動しました。D-Bus にorg.freedesktop.secretsが登録されるところまでは確認できましたが、headless でのキーリングのロック解除や、secret-toolでの確認が安定せず、実用的な解決にはなりませんでした。 -
Electron の
--password-store=basic: このオプションを付けて起動しましたが、再起動後に再サインインを求められる状態は変わりませんでした。
原因は特定できていません。そこで、Orca は基本的に止めずに動かし続け、落ちたときだけ vnc-on で VNC に入って再サインインする運用にしました。再サインインの手間は許容範囲と判断しています。
コンテナを再起動すると tmux ごと消える
tmux の中で動かしていれば SSH を切断してもプロセスは残りますが、コンテナ自体を停止・再起動すると、中のプロセスはすべて消えます。コンテナ側で自動起動を設定するかどうかは環境によるので、本記事では扱いません。筆者はコンテナの再起動後に、手動で orca-headless start --vnc を実行して再サインインしています。
Orca の更新が速い
冒頭の注記のとおり、Orca は短い間隔で新しいバージョンが出ます。バージョンを上げるには Orca の再起動が必要なので、そのたびに再サインインも必要になります。
詰まった点
ここからは、上の手順にたどり着くまでにつまずいた点と、そのときのログをまとめます。
AppRun では GUI が出ない
最初は、展開した AppImage に含まれる AppRun から起動しました。
/opt/orca/squashfs-root/AppRun
しかし GUI は表示されませんでした。起動後に残っていたプロセスは、主に次の2つだけです。
chrome_crashpad_handler
orca-ide ... daemon-entry.js
DISPLAY=:99 xwininfo -root -tree でウィンドウの一覧を見ても、Openbox のウィンドウしかなく、Orca のウィンドウは作られていませんでした。バックグラウンドの daemon だけが残り、GUI の本体(Desktop の main プロセスと renderer)は起動していない状態です。
そこで AppRun を使わず、手順 7 のように dbus-run-session と X11 用の環境変数を付けて orca-ide を直接起動したところ、GUI が表示されました。
Sign in を押すと spinner が回り続ける
GUI は表示されるようになりましたが、Sign in を押すと spinner が回ったまま先に進みません。最初は「headless なので外部ブラウザを開けないのではないか」と考え、インストールした Epiphany を単体で起動してみました。
DISPLAY=:99 epiphany https://example.com
結果は次のとおりです。
libEGL warning: DRI3 error: Could not get DRI3 device
bwrap: Can't mount proc on /newroot/proc: Operation not permitted
ERROR: Failed to fully launch dbus-proxy: Child process exited with code 1
trace trap (core dumped)
1行目の EGL(グラフィックス API の一つ)の warning は、Xvfb に GPU がないために出るもので、今回の問題の主な原因ではありません。原因は次の行です。
bwrap: Can't mount proc on /newroot/proc: Operation not permitted
WebKitGTK は、Web ページを描画する子プロセスを bubblewrap(bwrap)による sandbox の中で起動します。このコンテナ内では、その sandbox が /proc をマウントできずに失敗していました。特権のないコンテナで bubblewrap がこのエラーを出す事例は、ほかにも報告されています。ただし、このコンテナのどの制限(seccomp や capability など)が原因になっているのかまでは特定していません。
さらに、tail -f /tmp/orca-gui.log でログを見ながら Orca の Sign in を押すと、Orca が実際に Epiphany を起動していたことが分かりました。つまり、失敗は次の順で起きていました。
Orca の Sign in
↓
外部ブラウザとして Epiphany を起動
↓
WebKitGTK の bubblewrap sandbox がコンテナ内で失敗
↓
ブラウザがクラッシュ → Orca はブラウザからの認証結果を待ち続ける(spinner)
そこで、Orca を起動するときの環境変数に WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 と WEBKIT_DISABLE_COMPOSITING_MODE=1 を加えました。Orca から起動される Epiphany にも、この環境変数が引き継がれます。これで、Sign in → Epiphany での認証 → Orca へのサインイン完了 → Relay でのペアリング、まで通るようになりました。
2つの環境変数は、どちらも WebKitGTK が実際に参照するものです。
-
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS: WebKitGTK/Debugging に記載がある -
WEBKIT_DISABLE_COMPOSITING_MODE: WebKit Bug 154147 に、この変数を読み取る実装がある
認証サービスの障害ではないことの確認
サインインで止まっている間に、原因が手元の環境にあるのか、Orca の認証サービス側の障害なのかを切り分けるため、認証サービスのエンドポイントに直接アクセスしてみました。
curl -v --max-time 10 https://auth.onorca.dev/ -o /dev/null
# → TLS handshake 成功、HTTP/2 307、location: /login
curl -v --max-time 10 https://login.onorca.dev/health -o /dev/null
# → HTTP/2 200
どちらも正常に応答していたので、原因はサービス側ではなく、手元のブラウザの sandbox だと判断しました。
トラブルシューティング表
表の各列の意味は次のとおりです。
- 「症状」: 観測された現象
- 「原因・切り分け」: 今回判明した原因、または確認すべき箇所
- 「対応」: 今回の構成で取った対処
| 症状 | 原因・切り分け | 対応 |
|---|---|---|
| AppImage が起動しない | FUSE が使えない |
--appimage-extract で展開して使う |
| VNC の画面が真っ黒 | Xvfb だけが動いていて、ウィンドウがない | Openbox と Orca のプロセスが動いているか確認する |
| Orca のプロセスはあるのに GUI が出ない | daemon だけが残り、Desktop の main プロセスと renderer が起動していない |
orca-ide を直接、dbus-run-session と X11 用の環境変数を付けて起動する |
xwininfo に Orca のウィンドウがない |
GUI のウィンドウが作られていない |
/tmp/orca-gui.log を確認する |
| EGL / DRI3 の warning | Xvfb に GPU デバイスがない | ソフトウェア描画にする。warning 自体は主な原因ではない |
| Epiphany がすぐに落ちる | WebKitGTK の bubblewrap sandbox がコンテナ内で /proc をマウントできない |
WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 を付ける |
| Sign in で spinner が回り続ける | ブラウザのクラッシュ、認証サービスへの接続、コールバックのいずれか |
/tmp/orca-gui.log、curl での接続確認、ブラウザのプロセスを確認する |
| Orca を落とすまでは Relay が使える | 再起動するとアカウントのセッションが復元されない(原因は特定できていない) | Orca を常駐させ、再起動したときは VNC で再サインインする |
| SSH を切ると Orca が止まる | tmux の外でプロセスを起動している | tmux の中で起動する |
| コンテナを再起動すると全部消える | tmux はコンテナのライフサイクルを越えられない |
orca-headless start で起動し直す |
セキュリティ
この構成には sandbox の無効化が含まれます。信頼できるリモート開発環境で、自分だけが使うことを前提にしてください。
Chromium / Electron: --no-sandbox
Orca(Electron)に含まれる Chromium の sandbox を無効化しています。sandbox は、renderer プロセスが攻撃を受けたときに、被害をそのプロセスの中に閉じ込めるための仕組みです。無効化すると、この保護がなくなります。今回はコンテナ内での回避策として使いました。VM や物理マシンなど、--no-sandbox なしで起動できる環境では外してください。
WebKitGTK: WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1
名前のとおり、WebKit の子プロセスの sandbox を無効化します。この環境変数は Orca から起動される Epiphany に引き継がれるので、その Epiphany は Orca のサインイン(OAuth: 外部サービスで認証・認可を行う仕組み)専用にしてください。この状態の Epiphany で、普段の Web ブラウジングはしないようにします。
VNC は localhost 限定 + SSH トンネル
x11vnc -localhost で、VNC にはサーバーの内部からしか接続できないようにしています。外部からは SSH トンネル(ssh -N -L 5900:127.0.0.1:5900 ...)を経由してだけ接続します。VNC のパスワードは x11vnc -storepasswd で設定し、普段は vnc-off で VNC 自体を止めておきます。
ペアリング情報
orca://pair?code=... やペアリング用の QR コードは、それだけで接続できてしまう認証情報です。スクリーンショットや記事に実際の値を載せないでください。
まとめ
- headless Linux でも、通常の Orca Desktop を Xvfb 上で常駐させれば、複数の CLI coding agent を Orca にまとめて扱えます。VNC を使うのは初期設定と再サインインのときだけで、普段は Orca Relay 経由でスマホの Orca Mobile から操作できます。
- Orca Mobile では日本語入力も修飾キーも問題なく使えたので、スマホからの SSH + tmux で煩わしかった点はほぼ解消しました。
- ただし、sandbox を弱める workaround と、Orca を再起動するたびの再サインインという制約があります。信頼できるリモート開発環境で常駐させる前提の構成として扱ってください。
参考文献
- Orca
- Claude Code
- Codex / ChatGPT
- Antigravity
- AppImage / WebKitGTK