SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

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 | bash

Bun のインストーラーは 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
codex

command -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-workflow

command -v は絶対パスを出力します。通常は /home/<your user>/.bun/bin/deer-workflow です。パスをどこかにコピーしておきます。systemd の unit では単なる名前を使用できません。

インストールコマンドにはバージョンを指定したままにします。@0.2.0 を省略すると、実行日に最新のバージョンがインストールされます。47 件のコミットがあるプロジェクトでは、誰も監視していない timer の下で CLI が変わる可能性があります。

グラフを git リポジトリに格納する

mkdir -p ~/workflows/logs
cd ~/workflows
git init

Codex は自身が 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.target
sudo 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= では両方のディレクトリを引き続き指定しています。ランタイム自体が agent step の開始時に codex または claude を見つける必要があるためです。

agent が認証情報を見つけられない。 agent CLI はホームディレクトリからログイン情報を読み取るため、User= と Environment=HOME= を明示的に設定し、ログインに使用したホームディレクトリを指定してください。workflow:start まで到達した後、独自コードではなく agent CLI が出力元となるメッセージを持つ workflow:error が生成される場合、ほとんどはこれが原因です。

実行がハングするか、設定した制限で停止する。 Type=oneshot は、systemd がデフォルトで開始タイムアウトを無効にする唯一の service type です。そのため TimeoutStartSec= 行がないと、実行は停止しません。agent step がハングすると、unit は activating に無期限で留まります。また、timer はその横で 2 回目の実行を起動しないため、夜間ジョブがひそかに実行されなくなります。上記の unit にある TimeoutStartSec=3600 は、回避すべきデフォルト値ではなく、指定した上限値です。コマンド全体に適用されます。実行がこの上限に達すると、journal に start operation timed out. Terminating. が記録され、unit は failed 状態で終了します。ログファイルには実行途中までの内容が残り、workflow:end は記録されません。グラフの処理に 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 で確認してください。npm から Codex CLI をインストールする場合は、別途 Node.js と npm も必要です。

ワークフローは端末では実行できますが、systemd では失敗します。なぜですか。

ほとんどの場合、原因は PATH、HOME、または起動タイムアウトです。systemd はシェルのプロファイルを読み込まないため、ExecStart には deer-workflow への絶対パスが必要です。また、Environment=PATH= には codex または claude を格納しているディレクトリが必要です。エージェント CLI は $HOME から認証情報を読み込むため、User= と Environment=HOME= にはログインしたアカウントを設定してください。Type=oneshot にはデフォルトの起動タイムアウトがないため、1 回の実行を制限するのは TimeoutStartSec= だけです。ジャーナルに start operation timed out. Terminating. と表示された場合は、実行が設定した値に達しています。その場合は値を大きくしてください。何も設定しないと、停止したエージェントの手順によって unit が activating のままになり、後続の実行が開始されません。そのため、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 日時点で、公開されている最新バージョンは 0.2.0 です。これは 2026 年 7 月 27 日に公開され、リポジトリには 47 件のコミットがあります。@0.2.0、またはこの記事を読む時点での現行バージョンをインストールコマンドに指定してください。その番号をグラフとともに git で管理し、アップグレード後は timer が再び実行する前に、毎回 1 つのグラフを手動で実行してください。