AIエージェントをVPSで24時間動かす
systemdで常駐させるだけなら30分。難しいのは「止まっているのに緑」を見抜くこと。常駐エージェントを運用して分かった監視と安全の設計。
エージェントを常駐させること自体は、30分もあれば終わります。手間がかかるのはその先で、数週間たったころに「プロセスは生きているのに、何も処理していない」状態を見抜けるかどうかです。この記事では、最小の構成を作ったあと、編集部の常駐 Bot で実際に踏んだ罠と、いま使っている監視の設計を順に説明します。
先に結論
- 重い推論は LLM の API に任せる。VPS は「指示を出して、結果を書き込むだけ」の係にする。メモリ 2GB あれば足ります。
- LLM には判断だけをさせる。実際の操作は、事前に許可したスクリプトだけが行う。
- 監視は別のホストに置く。異常がなくても毎日「OK」を送らせる。
1. VPSを選ぶ
推論を API に任せるなら、CPU よりも「メモリ」「リージョン」「料金の払い方」のほうが効きます。2026年10月16日時点の、各社公式ページの表示です(税込・税抜は各社の表記のまま)。
| サービス | 目安のプラン | 料金 | メモ |
|---|---|---|---|
| ConoHa VPS | 2GB / 3コア / 100GB SSD | 時間課金で月上限 2,033円。長期契約の割引あり | 東京リージョン、時間課金があるので検証しやすい |
| Hostinger | KVM 1(4GB / 1vCPU / 50GB NVMe) | $6.49/月(24か月前払い)、更新後 $11.99/月 | 更新後の料金で比べること |
| DigitalOcean | Basic 2GB / 1vCPU / 50GB | $12/月(秒課金、月の上限あり) | 作っては壊す検証に向く |
PR:上の表のサービスには、当サイトが参加する(予定の)アフィリエイトプログラムのものがあります。選んだ基準は料金とリージョンだけです。
おすすめVPS(国内):東京リージョンで始めるなら、時間課金で試しやすい ConoHa VPS
か、メモリ量で選べる XServer VPS
が候補です。料金とプランは変わるので、契約前に公式ページで確認してください。
2. 最小の構成
ユーザーと SSH
sudo apt update && sudo apt -y upgrade
sudo adduser --disabled-password agent
sudo ufw allow OpenSSH && sudo ufw enable
# /etc/ssh/sshd_config: PasswordAuthentication no / PermitRootLogin no
sudo systemctl reload ssh
systemd で常駐させる
# /etc/systemd/system/agent.service
[Unit]
Description=AI agent
After=network-online.target
Wants=network-online.target
[Service]
User=agent
WorkingDirectory=/home/agent/app
EnvironmentFile=/home/agent/app/.env
ExecStart=/usr/bin/node dist/main.js
Restart=on-failure
RestartSec=5
WatchdogSec=120
[Install]
WantedBy=multi-user.target
.env は chmod 600 にして、Git には入れません。WatchdogSec は、アプリから sd_notify("WATCHDOG=1") を送っていないと意味がありません。後で説明する「生きているのに動いていない」状態への最初の備えです。
3. 「判断は LLM、実行はスクリプト」
編集部では、監視役のエージェントに10分ごとにログを読ませて、状況を診断させています。ただし、修復のコマンドを直接打たせることはしません。打てるのは、名前を指定した数本のスクリプトだけです。
各スクリプトの中身は同じ型です。
- 対象のファイルをバックアップする
- 変更を tmp に書き出す
- 検証する(構文チェック、起動テスト)
mvで原子的に置き換える- 失敗したら、バックアップから戻す
最初の版では、通知に使う Bot のトークンをシステムプロンプトに入れていました。いまは通知専用のスクリプトに分けてあり、LLM が知っているのは notify.sh という名前だけです。ログには外部から来た文字列が混ざるので、鍵と信頼できない入力を同じコンテキストに置かないようにしています。同じ理由で、無人の cron から LLM に本番への投稿をさせることもやめ、本番への書き込みは人が端末から起動しています。
本番を読ませても書かせない設定は「AIエージェントの権限設計」に書きました。
4. 緑なのに止まっている
運用を始めて一番時間を取られたのは、この種の障害でした。
systemctl is-activeも/healthも、親プロセスが生きているかしか見ていません。 処理ループが空回りしていても active です。いまは、CPU 使用率の外形監視、処理ループが一定間隔で打つ tick の watchdog、依存先まで含めた readiness の3つを重ねています。- 「再起動していない」ことは健全性の指標になりません。 数日おきに勝手に復活しているように見えた件は、調べたら unattended-upgrades による再起動でした。つまり、その間ずっと壊れていたことになります。
- journal に出ていないことは、発火していないことを意味しません。 ルートのログレベルが WARNING になっていて、info が丸ごと捨てられていたことがあります。
- 監視が自分自身を止めることがあります。 stall を検知した瞬間にログを出そうとすると、ロギングのロックで監視スレッドごと固まります。検知の経路では、標準エラーに直接
os.write(2, …)するだけにしました。
5. 監視の4つの不変条件
最後に、監視の一番外側(日次チェック)が黙って止まっていて、2日間の空回りに誰も気づかなかったことがあります。それ以来、次の4つは崩さないことにしています。
- 監視は、監視対象と別のホストに置く。 同じ VPS に置くと、一緒に落ちます。
- 異常がなくても、毎日 OK を送る。 OK が届かないこと自体を、異常の合図にします。
- 判定と通知を分ける。 判定のコードが通知の失敗に巻き込まれないようにします。
- 「意図的に止めている」状態を、正常とも故障とも別に扱う。 ok / error のほかに inert というラベルを用意します。
アラートの経路は、本番が健全なときには自然には通りません。そこで ?simulate=fail のようなフラグで、強制的に失敗を流せるようにしています。
# 別ホストのcronから。OKが来なければ、それ自体が通知になる
curl -fsS --config - <<< "url = \"$HEALTH_URL?simulate=${SIMULATE:-0}\""
4つの条件の先で踏んだ落とし穴は「常駐Bot監視の落とし穴」にまとめています。
6. 秘密情報で踏んだこと
- トークンを
curlの引数に書かない。 共有のマシンではpsから見えます。curl --config -で標準入力から渡します(上の例のとおり)。 - 空の値が「Success」で登録される。 空の標準入力を
wrangler secret putに流したら、空の secret が成功扱いで登録されていました。値をいったん変数に取り、空でないことを確かめてから流します。s="$(cat token.txt)"; [ -n "$s" ] && printf %s "$s" | npx wrangler secret put API_TOKEN - エージェントのメモリや設定ファイルに、キーそのものを書かない。 メモリは長く残り、会話ログにも載ります。書くのは「どこに、何という名前で置いてあるか」だけにします。
権限の deny を書いても、grep -r やスクリプト経由で .env が読まれる経路は残ります。ダミーの鍵で確かめた漏れ道と塞ぎ方は「Claude Codeに秘密を渡さない」にまとめました。
7. 「止めたはず」を確かめる
止めたつもりの自動実行が、何か月も動き続けていたことがあります。そのときにやっていたのは、出力を見えなくすることと、たまったファイルを消すことだけで、生成元は生きていました。いまは停止を宣言する前に、次の4つを実際に確認しています。
systemctl list-timers --all
crontab -l; sudo ls /etc/cron.d
systemctl list-unit-files --state=enabled | grep -v '^systemd'
find /var/log /home/agent -newermt '-1 hour' -type f # 出力がまだ増えていないか
止めたつもり、試したつもりの処理が本番で動いていた、という静かな事故は「デプロイとマイグレーションの事故集」にまとめました。
チェックリスト
-
.envは 600、Git の管理外 - 修復はスクリプトだけが行う。LLM は名前しか知らない
- tick watchdog と readiness を入れている
- 監視は別ホスト、日次の OK、
simulate=failを用意 - API の利用額に上限アラートを設定