Deer WorkflowをVPSでセルフホストする方法
Deer WorkflowをUbuntu VPSにBunで導入し、バージョンを固定してTypeScriptのエージェントグラフをsystemdでヘッドレス実行します。認証情報とPATH、ログ保存の設定も確認できます。
構築するもの
Deer Workflow は、エージェントグラフ向けのコードファーストなランタイムです。制御フローはレビュー可能な TypeScript ファイルに記述し、判断が必要な処理だけをコーディングエージェントに実行させます。このガイドでは、1 台の Ubuntu VPS にインストールし、1 つのサンプルグラフを systemd でヘッドレス実行します。また、マシンで読み取り可能なイベントストリームをログファイルに書き込み、午前3時に実行が失敗した場合でも検索できるようにします。
構成要素は少数です。Bun が CLI を実行します。Codex または Claude Code のいずれか1つのコーディングエージェント CLI がモデル処理を担います。固定した1つの npm パッケージがランタイムを提供します。1つの TypeScript ファイルにグラフを記述します。systemd のサービスとタイマーで、スケジュールに従って実行します。本文の大半では、実際に問題になりやすい部分を扱います。systemd unit 内の PATH、ログインシェルのないセッションでのエージェント認証情報、そして2026年7月に初めて公開された依存関係のバージョン固定です。
ビジュアルビルダー、コード、またはエージェントへの直接プロンプト
モデルを使って作業を自動化するセルフホスターは、3 つの方式から選びます。それぞれ異なる問題が発生します。
ビジュアルビルダーでは、キャンバス、ノードライブラリ、プログラマーでない人でも開けるユーザーインターフェイスを利用できます。これは実際の利点です。また、選択肢が十分に多いため、セルフホスト可能な n8n の代替をまとめた調査もあります。一方、ロジックは UI が生成する JSON ドキュメントになります。その差分は把握しにくいため、変更をレビューするにはパッチを読むのではなく、キャンバスを開く必要があります。
エージェントに直接プロンプトを入力する方式が 2 つ目の形です。作業全体を段落で説明し、順序、リトライ、停止するタイミングをモデルに決めさせます。モデルの判断が変わる日までは機能します。成果物がないため差分もありません。計画は会話の中にあり、その会話が失われると確認できなくなります。
コードによるオーケストレーションが 3 つ目の形です。手順の順序、ファンアウト、リトライ、エラーハンドリングを、git で管理する通常の TypeScript として記述します。モデルは判断が必要な箇所でのみ呼び出します。代わりに、そのコードを記述して保守する担当者が必要です。また、TypeScript を書けない同僚は編集できません。
グラフランタイムで得られるものと、そのコスト
- レビュー可能な制御フロー。 グラフはファイルです。再試行ポリシーの変更は、移動したボックスではなく、3 行の変更として pull request に現れます。
- バージョン管理できる障害処理。 4 番目のステップが失敗した場合の動作を明文化し、テストし、インフラの他の要素とともにタグ付けできます。
- 交換可能なエージェント。 ランタイムには Codex、Claude Code、Pi 用のアダプターが付属します。ステップを実行するエージェントの変更は、import 1 つで済みます。
- 監視可能な実行。 フェーズとイベントは構造化データとしてランタイムから出力されるため、ヘッドレス実行でもクエリ可能な記録が残ります。
モデルが実行するループ自体を設計し、単一のプロンプトを磨き上げるのではない一般的な実践を ループエンジニアリング と呼びます。グラフランタイムは、そのための具体的な方法の 1 つです。コストはセットアップです。インストールするランタイム、認証が必要なエージェント CLI、プログラマー以外向けのインターフェースがないこと、そして継続的に注意を払う必要がある新しい依存関係があります。
プロジェクトは新しいため、バージョンを固定します
Deer Workflow は MIT ライセンスで提供されており、まだ新しいプロジェクトです。2026年8月19日時点で、リポジトリには main 上のコミットが 47 件あります。npm には公開済みバージョンが 3 つあります。2026年7月26日に 0.0.1 と 0.1.0 が公開され、その後、2026年7月27日に 0.2.0 が公開されました。それぞれに git tag があり、各バージョン間の変更内容は changelog で確認できます。Unreleased セクションではすでに deer-workflow agent コマンドが削除されているため、main と最新の公開バージョンでは、同じ CLI を利用できません。
これは、プロジェクトの利用を避ける理由にはなりません。正確なバージョンを 1 つインストールし、どのバージョンをインストールしたかを把握する理由になります。
- 正確なバージョンをインストールし、範囲指定は使用しません。
- そのバージョンを、グラフと同じリポジトリに記録します。
- アップグレード後は、タイマーが再度実行する前に、自分のグラフを手動で 1 回実行します。
Bun とエージェントランタイムをインストールする
以下の操作はすべて、sudo 権限を持つ通常ユーザーで実行します。root では実行しないでください。エージェント CLI は、サインインしたユーザーのホームディレクトリに認証情報を保存します。後で作成する systemd unit も、認証情報を見つけられるよう同じユーザーで実行する必要があります。
sudo apt update
sudo apt install -y curl unzip jq git nodejs npm
curl -fsSL https://bun.com/install | bashBun のインストーラーは zip アーカイブを展開するため、最初に unzip が必要です。インストーラーは PATH の設定行をシェルプロファイルに追加します。現在のシェルはそのファイルをすでに読み込んでいるため、新しいシェルを開くか、~/.bashrc に次の 2 行を自分で追加して再読み込みしてください。
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$HOME/.npm-global/bin:$PATH"bun --versionこれによりバージョン番号が表示されます。bun: command not found と表示される場合は、インストールに失敗したのではなく、現在のシェルに PATH の設定行がないことを示します。何も再インストールせず、先に ls ~/.bun/bin を実行してください。
次にエージェントランタイムを設定します。デフォルトは Codex CLI で、npm からインストールします。ユーザーレベルの npm prefix を設定すると、グローバルインストールで root 権限が不要になります。
npm config set prefix "$HOME/.npm-global"
npm install -g @openai/codex
command -v codex
codexcommand -v codex では $HOME/.npm-global/bin 配下のパスが表示されます。codex を単独で実行すると CLI が起動し、ChatGPT アカウントでサインインできます。画面を確認できる今のうちに、1 回実行してサインインしてください。
Claude Code は代替ランタイムとして使用でき、専用のインストーラーがあります。
curl -fsSL https://claude.ai/install.sh | bash
claude --versionインストールが正常に完了すると、2.1.211 (Claude Code) のようなバージョンが表示されます。claude を 1 回実行してログインしてください。これは、ホストする他のエージェントと同じ種類のプロセスであり、ファイルへのアクセス権も同じです。そのため、VPS でコーディングエージェントを実行する に記載したアカウントとセキュリティ強化に関する注意事項を、そのまま適用できます。
Deer Workflow をインストールし、正確なバージョンを固定する
bun install --global @deerwork-ai/deer-workflow@0.2.0
command -v deer-workflowcommand -v は絶対パスを出力します。通常は /home/<your user>/.bun/bin/deer-workflow です。パスをどこかにコピーしておきます。systemd の unit では単なる名前を使用できません。
インストールコマンドにはバージョンを指定したままにします。@0.2.0 を省略すると、実行日に最新のバージョンがインストールされます。47 件のコミットがあるプロジェクトでは、誰も監視していない timer の下で CLI が変わる可能性があります。
グラフを git リポジトリに格納する
mkdir -p ~/workflows/logs
cd ~/workflows
git initCodex は自身が git リポジトリ内で実行されているかどうかを確認します。そのため、CodexAgentConfig にはリポジトリを指定できない場合に備えて skipGitRepositoryCheck オプションがあります。自分の VPS ではリポジトリを指定できるため、指定してください。グラフはコードであり、コードをバージョン管理しないなら、オーケストレーションをコードとして記述する意義が失われます。ここで logs ディレクトリを作成してください。systemd はこのディレクトリを自動作成しません。
1 つのグラフを作成する
ワークフローは通常の TypeScript モジュールです。名前、説明、順序付きのフェーズ一覧を保持するオブジェクトを meta としてエクスポートし、ハンドラーを default または名前付きの run エクスポートとしてエクスポートします。ハンドラー内では、パッケージのヘルパーを呼び出します。phase() は実行中のステージを示し、log() は進行状況の行を書き込み、agent() は coding agent に 1 つのプロンプトを送り、parallel() はタスクの一覧を同時に実行し、pipeline() は項目の一覧を複数のステージに渡します。
これを ~/workflows/log-triage.ts として保存します。
import { agent, log, parallel, phase } from "@deerwork-ai/deer-workflow";
export const meta = {
name: "log-triage",
description: "Groups recent service errors and writes one short report.",
phases: [{ title: "Collect" }, { title: "Classify" }, { title: "Report" }],
exampleArgs: { service: "nginx", hours: 24 },
};
export default async function workflow(args: { service: string; hours: number }) {
if (!args?.service) throw new Error("input needs a service name");
phase("Collect");
log(`Reading ${args.hours}h of logs for ${args.service}`);
const found = await agent<{ patterns: string[] }>(
`Read the last ${args.hours} hours of journalctl -u ${args.service} and list the distinct error patterns.`,
{
sandbox: "read-only",
schema: {
type: "object",
properties: { patterns: { type: "array", items: { type: "string" } } },
required: ["patterns"],
additionalProperties: false,
},
},
);
phase("Classify");
log(`Classifying ${found.patterns.length} patterns`);
const notes = await parallel(
found.patterns.map((pattern) => () =>
agent(`Explain this error and its most likely cause: ${pattern}`, { sandbox: "read-only" }),
),
);
phase("Report");
return agent(`Write a short operations report from these notes: ${JSON.stringify(notes.filter(Boolean))}`);
}このファイルには、特に重要な点が 4 つあります。
agent()の呼び出しでschemaを指定すると、構造化された出力を要求できます。この呼び出しは、解析済みのオブジェクトを返します。found.patternsは、グラフの残りの処理で反復できる実際の配列です。スキーマがない場合、agent()は文字列を返すため、文章を自分で解析する必要があります。sandboxは、そのステップで操作できる対象を決めます。read-onlyは書き込みを禁止し、workspace-writeは保護付きの書き込みを許可し、danger-full-accessは保護を解除します。これは呼び出しごとに設定するため、グラフ全体では広範囲を読み取りつつ、1 か所だけに書き込む構成にできます。parallel()は promise ではなく関数を受け取ります。map((pattern) => () => agent(...))は thunk の一覧を作成するため、各処理をいつ開始するかはランタイムが決定します。agent(...)を直接渡すと、一覧の作成時点ですべての呼び出しが開始されます。parallel()内のタスクが失敗すると、nullになり、実行は続行されます。これは、設計上、部分的な完了を許可しているためです。そのためnotes.filter(Boolean)は単なる装飾ではありません。省略すると、失敗した分岐によってテキストnullが次のステップのプロンプトに入ります。
単純な agent() ヘルパーは、デフォルトのランタイムである Codex を使用します。代わりに 1 つのステップを Claude Code に送るには、agent クラスをインポートして直接呼び出します。
import { ClaudeAgent } from "@deerwork-ai/deer-workflow";
const claude = new ClaudeAgent({ sandbox: "read-only" });
const summary = await claude.run<string>("Summarise ./report.md in five lines.");これが、実際に agent を切り替える構成です。インポート 1 つとコンストラクター 1 つを追加するだけで、周囲のグラフは変更しません。CLI の --agent codex|claude|pi フラグは、説明からワークフローファイルを生成する deer-workflow create に属します。これは deer-workflow run が使用するランタイムを変更しません。
手動で 1 回実行してから、ヘッドレスで実行する
cd ~/workflows
deer-workflow run ./log-triage.ts --input '{"service":"nginx","hours":24}'対話的に実行すると、ターミナルインターフェースが表示されます。画面の一方には meta からのフェーズが、もう一方にはライブログが表示されます。自動化する前に、この方法で一連の実行を最後まで 1 回確認してください。agent がログインしていない場合や、入力が handler のシグネチャと一致しない場合も、翌週にログファイルで見つけるのではなく、数秒で確認できます。
自動化する場合は、入力をファイルに移します。~/workflows/input.json を保存します。
{ "service": "nginx", "hours": 24 }deer-workflow run ./log-triage.ts --input-file ./input.json --print >> logs/run.jsonl--print(短縮形は -p)を指定すると、インターフェースを無効にし、イベントストリームを 1 行 1 JSON オブジェクトの形式で stdout に書き込みます。このモードでは、それ以外の内容は stdout に出力されません。そのため、.jsonl ファイルに直接追記すれば、すべての行を解析できるファイルになります。
イベントストリームと、午前3時に grep する内容
各行には type、sequence、timestamp、workflowId、depth、scriptPath が含まれます。種類は workflow:start、workflow:meta、workflow:end、workflow:error、workflow:phase:start、workflow:phase:end、log です。フェーズイベントには phase、終了イベントには durationMs が含まれます。log イベントには message が含まれ、workflow:error イベントには error と name、message、通常は stack が含まれます。
これだけの構造があれば、午前3時に確認する2つの疑問に答えられます。処理は完了したか、どこで停止したかです。
grep workflow:error logs/run.jsonl
jq -r 'select(.type == "workflow:error") | .error.message' logs/run.jsonl
jq -r 'select(.type == "workflow:phase:end") | [.phase, .durationMs] | @tsv' logs/run.jsonl
jq -r 'select(.type == "log") | .message' logs/run.jsonl現在実行中の処理を監視するには、次のファイルを追跡します: tail -f logs/run.jsonl | jq -c 'select(.type == "log")'。1 回の実行で書き込まれる行数は少ないですが、ファイルは増え続けます。そのため、タイマーを数週間運用したら ~/workflows/logs/*.jsonl 用の logrotate ルールを追加してください。
systemd で実行する
長時間稼働する daemon ではなく、oneshot service と timer を使用します。グラフは起動して実行された後、終了します。deploy を自分のユーザー名に置き換えて、/etc/systemd/system/log-triage.service を作成します。
[Unit]
Description=Log triage workflow
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=deploy
WorkingDirectory=/home/deploy/workflows
Environment=HOME=/home/deploy
Environment=PATH=/home/deploy/.bun/bin:/home/deploy/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/deploy/.bun/bin/deer-workflow run ./log-triage.ts --input-file ./input.json --print
StandardOutput=append:/home/deploy/workflows/logs/run.jsonl
StandardError=journal
TimeoutStartSec=3600次に、/etc/systemd/system/log-triage.timer を実行します。
[Unit]
Description=Run the log triage workflow every night
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl start log-triage.service
systemctl status log-triage.service
sudo systemctl enable --now log-triage.timer
systemctl list-timers log-triage.timerまず手動で service を起動します。正常に実行されると unit は正常に非アクティブ化され、logs/run.jsonl には workflow:end で終わるイベントのまとまりが追加されます。その後で timer を有効にします。list-timers は次回のスケジュール実行を表示し、Persistent=true を指定すると、サーバーの停止中に実行されなかった処理が次回の boot 時に 1 回実行されます。StandardOutput=append: はイベントストリームをファイルに送り、それ以外のログは journal に残すため、journalctl -u log-triage.service を読みやすい状態に保てます。
シェルではグラフが動作するのに、systemd では失敗するのはなぜですか?
次の 4 点を、この順番で確認してください。
unit がバイナリを見つけられない。 systemd は ~/.bashrc を読み込まず、デフォルトの PATH に ~/.bun/bin も ~/.npm-global/bin も含まれていません。unit は 1 秒未満で失敗し、journalctl -u log-triage.service にはコマンド名の実行に失敗したことが示されます。そのため ExecStart では絶対パスを使用します。また、Environment=PATH= に両方のディレクトリを指定しているのも同じ理由です。エージェントのステップを開始するとき、ランタイム自身が codex または claude を見つける必要があります。
エージェントが認証情報を見つけられない。 エージェント CLI はホームディレクトリからログイン情報を読み込むため、User= と Environment=HOME= を明示的に設定し、ログインに使用したホームディレクトリを指定してください。workflow:start まで到達した後、自分のコードではなくエージェント CLI からのメッセージを持つ workflow:error が生成される場合、ほとんどはこれが原因です。
実行が 90 秒後に強制終了される。 Type=oneshot では、systemd がコマンド全体に起動タイムアウトを適用し、デフォルトは 90 秒です。エージェントグラフの実行には数分かかります。ジャーナルには Start operation timed out. Terminating. が記録され、unit は failed 状態になり、ログファイルには workflow:end のない実行途中までの内容が残ります。TimeoutStartSec=3600 を指定すると、タイムアウトを 1 時間にできます。時間による強制終了を避けたい場合は infinity を使用してください。
相対パスが別の場所を基準に解決される。 ./log-triage.ts と ./input.json は WorkingDirectory を基準にした相対パスです。この行を省略すると、systemd は / でプロセスを起動するため、どちらのファイルも見つかりません。
オーケストレーターに許可する操作
タイマーでエージェントのステップを実行するオーケストレーターは、誰も監視していない状態でサーバー上の処理を実行します。重要なのは、2 つの制御と 1 つの予算です。
1 つ目の制御は、各 agent() 呼び出しに設定するサンドボックスです。ログ、メトリクス、要約対象のリポジトリなど、読み取りだけを行うステップには read-only が適切なデフォルトです。書き込みが本当に必要な場合だけ、そのステップを workspace-write に変更し、danger-full-access を使うのではなく additionalWritableDirectories で書き込み可能な領域を小さく保ちます。
2 つ目の制御は人による承認です。メールの送信、金銭の移動、データの削除、本番環境の設定変更など、一部のステップは無人で実行してはいけません。コードファーストのグラフでは、ステップがコードの 1 行であるため、承認ゲートを簡単に配置できます。実行を停止し、実行予定の操作を記録して、人による回答を待ってから続行します。エージェントの操作の前に承認ゲートを配置する では、このパターンを詳しく説明しています。タイマーで開始するすべてのグラフに組み込むべきです。
予算とは費用です。agent() の呼び出しは毎回完全なエージェントセッションを実行し、parallel() は複数のセッションを同時に開始します。そのため、12 個の分岐に分岐するグラフは、誰もレポートを読まなくても毎晩 12 個のセッションを実行します。VPS 上の AI エージェントの費用を管理する にある測定方法と制限は、スケジュール実行するグラフにもそのまま適用できます。
ランタイムをアップグレードする前に、変更履歴を読み、新しい正確なバージョンをインストールし、--print を使ってグラフを手動で 1 回実行します。このプロジェクトはまだ初期段階のため、CLI の仕様は変化しています。Unreleased セクションでは、すでに 0.2.0 に存在するコマンドを削除しています。タイマーで実行するグラフの信頼性は、固定したバージョンと、実際に監視した最後の実行結果に左右されます。
FAQ
Bun が必要ですか。それとも Node.js で Deer Workflow を実行できますか
Bun をインストールしてください。公開パッケージは deer-workflow バイナリから src/cli.ts(TypeScript ソースファイル)を実行するよう指定しており、ドキュメントでも Bun を前提条件としています。Bun は TypeScript を直接実行するため、ビルド工程はありません。sudo apt install -y unzip に続けて curl -fsSL https://bun.com/install | bash を実行してインストールし、bun --version で確認してください。Codex CLI を npm からインストールする場合は、Node.js と npm も別途必要です。
ワークフローはターミナルでは実行できますが、systemd では失敗するのはなぜですか
ほとんどの場合、原因は PATH、HOME、または起動タイムアウトです。systemd はシェルのプロファイルを読み込まないため、ExecStart には deer-workflow への絶対パスを指定し、Environment=PATH= には codex または claude を含むディレクトリを指定する必要があります。エージェント CLI は $HOME から認証情報を読み込むため、User= と Environment=HOME= にはログインしたアカウントを設定してください。また、Type=oneshot の起動タイムアウトはデフォルトで 90 秒です。この時間を超えるとエージェントの実行が途中で終了し、journal に Start operation timed out. Terminating. が残ります。そのため、TimeoutStartSec=3600 を設定してください。
あるステップで Codex の代わりに Claude Code を使うにはどうすればよいですか
通常の agent() ヘルパーはデフォルトのランタイムである Codex を使用します。パッケージから ClaudeAgent をインポートしてインスタンスを作成し、Claude Code に処理させるステップで .run() を呼び出してください。--agent codex|claude|pi フラグは、説明からワークフローファイルを生成するコマンドである deer-workflow create に属します。このフラグは deer-workflow run には影響しません。使用するエージェントごとに CLI をインストールし、サービスの実行ユーザーと同じユーザーでログインしておく必要があります。
Deer Workflow はどのバージョンをインストールすべきですか
テストしたバージョンと完全に同じものをインストールしてください。2026 年 8 月 19 日時点で、公開済みの最新バージョンは 2026 年 7 月 27 日リリースの 0.2.0 で、リポジトリには 47 件のコミットがあります。@0.2.0、またはこれを読む時点での現行バージョンをインストールコマンドに指定し、その番号をグラフと一緒に git で管理してください。アップグレードのたびに、タイマーから再度実行される前にグラフを 1 つ手動で実行してください。