<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://reotech736.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://reotech736.com/" rel="alternate" type="text/html" /><updated>2026-08-09T15:18:39+09:00</updated><id>https://reotech736.com/feed.xml</id><title type="html">Reo’s Tech Blog</title><subtitle>エンジニアの学習記録 兼 備忘録</subtitle><author><name>Reo Komatsubara</name></author><entry><title type="html">Jekyllブログの記事をQiita CLIで自動ミラーする</title><link href="https://reotech736.com/2026/08/08/qiita-cli-blog-mirroring.html" rel="alternate" type="text/html" title="Jekyllブログの記事をQiita CLIで自動ミラーする" /><published>2026-08-08T00:00:00+09:00</published><updated>2026-08-08T00:00:00+09:00</updated><id>https://reotech736.com/2026/08/08/qiita-cli-blog-mirroring</id><content type="html" xml:base="https://reotech736.com/2026/08/08/qiita-cli-blog-mirroring.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>個人ブログの記事をQiitaにも掲載したいと考えました。しかし、同じ本文を2か所で編集すると、修正の反映漏れやリンク切れが起きやすくなります。</p>

<p>そこで、Jekyllの<code class="language-plaintext highlighter-rouge">_posts/</code>を正本にして、<a href="/terms/qiita-cli/">Qiita CLI</a>用Markdownを生成する仕組みを追加しました。mainブランチへマージした記事だけを<a href="/terms/github-actions/">GitHub Actions</a>からQiitaへ投稿します。</p>

<h2 id="作った仕組み">作った仕組み</h2>

<p>記事の編集元は、これまでどおり<code class="language-plaintext highlighter-rouge">_posts/</code>だけです。</p>

<pre><code class="language-mermaid">flowchart LR
  P[Jekyll _posts] --&gt; E[export-qiita.rb]
  E --&gt; Q[qiita/public]
  Q --&gt; R[Pull Requestで確認]
  R --&gt; M[mainへマージ]
  M --&gt; A[GitHub Actions]
  A --&gt; C[Qiita CLI]
  C --&gt; I[Qiitaの記事]
  C --&gt; D[記事IDと更新日時]
  D --&gt; Q
</code></pre>

<p>変換スクリプトは、次の処理を行います。</p>

<ul>
  <li>Qiita用Front Matterへ変換</li>
  <li>ブログ内の相対URLを絶対URLへ変換</li>
  <li>掲載元ブログの案内を末尾へ追加</li>
  <li>Qiitaの記事IDと更新日時を維持</li>
</ul>

<p>生成先は<code class="language-plaintext highlighter-rouge">qiita/public/</code>です。生成物もGit管理し、Pull Requestで実際の投稿内容を確認できるようにしました。</p>

<h2 id="qiita-apiではなくqiita-cliを選んだ理由">Qiita APIではなくQiita CLIを選んだ理由</h2>

<p>Qiita APIを直接使う方法もありますが、今回は公式CLIを選びました。</p>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>今回の判断</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Qiita API</td>
      <td>記事ID、リクエスト、エラー処理、プレビューを自前実装する必要がある</td>
    </tr>
    <tr>
      <td>Qiita CLI</td>
      <td>プレビュー、投稿、更新、記事ID管理を利用できる</td>
    </tr>
  </tbody>
</table>

<p>独自実装はJekyllからQiita形式への変換だけに絞り、投稿処理は公式ツールへ任せます。</p>

<p>使用したQiita CLIは<code class="language-plaintext highlighter-rouge">1.10.0</code>です。Node.jsは<code class="language-plaintext highlighter-rouge">22.22.1</code>以上を指定し、開発環境とCIではNode.js 24を使います。</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"engines"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&gt;=22.22.1"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@qiita/qiita-cli"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.10.0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install
</span>npx qiita version
</code></pre></div></div>

<h2 id="ミラーする記事を明示する">ミラーする記事を明示する</h2>

<p>Qiitaへ出したい記事だけに、次の設定を追加します。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">qiita</span><span class="pi">:</span>
  <span class="na">publish</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">tags</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">Qiita</span>
    <span class="pi">-</span> <span class="s">QiitaCLI</span>
    <span class="pi">-</span> <span class="s">GitHubActions</span>
    <span class="pi">-</span> <span class="s">Jekyll</span>
</code></pre></div></div>

<p>運用ルールは次のとおりです。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">qiita.publish: true</code>を公開の意思表示とする</li>
  <li>Qiita用タグは1〜5個とし、空文字と重複を拒否する</li>
  <li><code class="language-plaintext highlighter-rouge">false</code>へ変更した場合は、Qiitaの記事を削除せず同期だけを止める</li>
  <li>記事の削除と公開範囲の変更は自動化しない</li>
</ul>

<h2 id="jekyll固有のリンクを変換する">Jekyll固有のリンクを変換する</h2>

<p>ブログ内のルート相対URLは、そのままQiitaへ投稿するとリンク切れになります。</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nv">Qiita CLI</span><span class="p">](</span><span class="sx">/terms/qiita-cli/</span><span class="p">)</span>
<span class="p">![</span><span class="nv">設定画面</span><span class="p">](</span><span class="sx">/assets/images/example.png</span><span class="p">)</span>
</code></pre></div></div>

<p>変換後は、ブログの絶対URLになります。</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nv">Qiita CLI</span><span class="p">](</span><span class="sx">https://reotech736.com/terms/qiita-cli/</span><span class="p">)</span>
<span class="p">![</span><span class="nv">設定画面</span><span class="p">](</span><span class="sx">https://reotech736.com/assets/images/example.png</span><span class="p">)</span>
</code></pre></div></div>

<p>変換対象はMarkdownのリンクと画像、HTMLの<code class="language-plaintext highlighter-rouge">href</code>と<code class="language-plaintext highlighter-rouge">src</code>です。コード例を壊さないよう、fenced code block内は変換しません。</p>

<h2 id="生成確認投稿の流れ">生成、確認、投稿の流れ</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby scripts/export-qiita.rb
ruby scripts/export-qiita.rb <span class="nt">--dry-run</span>
ruby scripts/export-qiita.rb <span class="nt">--check</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> qiita-preview/public
<span class="nb">cp</span> <span class="nt">-R</span> qiita/public/. qiita-preview/public/
npx qiita preview <span class="nt">--root</span> qiita-preview <span class="nt">--config</span> qiita-preview
</code></pre></div></div>

<p>各コマンドの用途は次のとおりです。</p>

<ul>
  <li>引数なし: Qiita用Markdownを生成</li>
  <li><code class="language-plaintext highlighter-rouge">--dry-run</code>: ファイルを書き換えず変更予定を表示</li>
  <li><code class="language-plaintext highlighter-rouge">--check</code>: 生成物が最新でなければエラー終了</li>
  <li><code class="language-plaintext highlighter-rouge">qiita preview</code>: Git管理対象外の作業ディレクトリで表示を確認</li>
</ul>

<p>Pull Requestでは<code class="language-plaintext highlighter-rouge">_posts/</code>と<code class="language-plaintext highlighter-rouge">qiita/public/</code>を確認してからmainへマージします。</p>

<p>プレビューを開くと、生成した記事が未投稿の記事として表示されます。</p>

<p><img src="/assets/images/posts/qiita-cli-blog-mirroring/qiita-preview-draft-list.png" alt="Qiita Previewのサイドバーに、生成した記事が未投稿として表示されている" /></p>

<p>記事を選択すると、タイトル、タグ、本文、図をQiitaに近い表示で確認できます。</p>

<p><img src="/assets/images/posts/qiita-cli-blog-mirroring/qiita-preview-article.png" alt="Qiita Previewで「Jekyllブログの記事をQiita CLIで自動ミラーする」を表示している" /></p>

<h2 id="github-actionsから投稿する">GitHub Actionsから投稿する</h2>

<p>mainブランチでは、生成物の検査後にQiita CLIを実行します。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check generated Qiita articles</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">ruby scripts/export-qiita.rb --check</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Publish articles</span>
  <span class="na">env</span><span class="pi">:</span>
    <span class="na">QIITA_TOKEN</span><span class="pi">:</span> <span class="s">GitHub Actions Secretから設定</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">npx qiita publish --all --root qiita</span>
</code></pre></div></div>

<p>投稿後の処理も自動化しています。</p>

<ul>
  <li>Qiita CLIが更新した<code class="language-plaintext highlighter-rouge">id</code>と<code class="language-plaintext highlighter-rouge">updated_at</code>をbotコミットでmainへ戻す</li>
  <li>ワークフローを直列実行し、記事IDの同時更新を防ぐ</li>
  <li>トークンは<code class="language-plaintext highlighter-rouge">QIITA_TOKEN</code>としてActions Secretへ登録する</li>
  <li>トークンをファイルやログへ出力しない</li>
</ul>

<p>トークンには<code class="language-plaintext highlighter-rouge">read_qiita</code>と<code class="language-plaintext highlighter-rouge">write_qiita</code>権限が必要です。</p>

<h2 id="ローカルで確認した結果">ローカルで確認した結果</h2>

<p>変換スクリプトでは、次をテストしています。</p>

<ul>
  <li>Front Matterとタグの検証</li>
  <li>記事IDと更新日時の保持</li>
  <li>リンク変換とコードブロックの除外</li>
  <li>同期停止と生成物の差分検出</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby <span class="nb">test</span>/export_qiita_test.rb
</code></pre></div></div>

<p>8テスト、41 assertionsが成功し、<code class="language-plaintext highlighter-rouge">--check</code>とJekyllの本番ビルドも完了しました。Qiitaへの最初の公開対象はこの記事自身です。投稿後は記事IDの保存と、再実行時に重複せず更新されることを確認します。</p>

<h2 id="まとめ">まとめ</h2>

<p>Jekyllの記事を正本にし、形式の変換だけを自作して、投稿と記事ID管理はQiita CLIへ任せました。公開対象をFront Matterで明示し、生成結果をPull Requestで確認してから投稿する構成です。</p>

<p>まずはこの記事1本で新規投稿と更新を確認し、安定してから既存記事のミラーを検討します。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Jekyll" /><category term="Qiita" /><category term="GitHub Actions" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">HandyとローカルLLMで音声入力環境を構築しようとした話</title><link href="https://reotech736.com/2026/08/04/handy-local-llm-voice-input.html" rel="alternate" type="text/html" title="HandyとローカルLLMで音声入力環境を構築しようとした話" /><published>2026-08-04T00:00:00+09:00</published><updated>2026-08-04T00:00:00+09:00</updated><id>https://reotech736.com/2026/08/04/handy-local-llm-voice-input</id><content type="html" xml:base="https://reotech736.com/2026/08/04/handy-local-llm-voice-input.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>Windowsで使っている<a href="/terms/handy/">Handy</a>の音声入力へ、用途別の文章整形を加える仕組みを作りました。<a href="/terms/automatic-speech-recognition/">自動音声認識（ASR）</a>の後段に<a href="/terms/local-llm/">ローカルLLM</a>を置き、AIへの指示、業務連絡、友人向けチャットに合う文体へ整える構成です。</p>

<p>構成は実機で動きました。しかし、待ち時間に見合う改善を安定して得られなかったため、現在はHandyのWhisper Mediumだけで文字起こしする運用に戻しています。</p>

<p>この記事は、作った仕組みと実測結果から「今は採用しない」と判断するまでの記録です。実装と評価データは<a href="https://github.com/Reotech736/voice-profiles">voice-profiles</a>で公開しています。</p>

<h2 id="何を作ろうとしたのか">何を作ろうとしたのか</h2>

<p>Handyは、ショートカットで録音し、端末内で音声を文字へ変換して、フォーカス中の入力欄へ貼り付けるアプリです。音声入力だけならHandy単体で完結しますが、文字起こしにはフィラーや言い直しが残ることがあります。</p>

<p>そこで、文字起こし結果を次の3種類に整える機能と、AIを通さないRaw Pathを用意しました。</p>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>目的</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Prompt</td>
      <td>AIへの指示を整理する</td>
    </tr>
    <tr>
      <td>Boss</td>
      <td>業務向けの自然な丁寧語へ整える</td>
    </tr>
    <tr>
      <td>Friend</td>
      <td>元の口調を保ちながら読みやすくする</td>
    </tr>
    <tr>
      <td>Raw</td>
      <td>Handyの文字起こしをそのまま使う</td>
    </tr>
  </tbody>
</table>

<p>文章整形には、Windows上でQwen3を実行する<a href="/terms/ollama/">Ollama</a>を利用しました。Handy自体は変更せず、Custom Post-Processing Providerと公開CLIから連携します。</p>

<h2 id="全体構成">全体構成</h2>

<p>自作キーボードのroBaはF13〜F18をWindowsへ送り、AutoHotkey v2がプロファイル選択とHandyを操作します。Handyからlocalhostの<a href="/terms/openai-compatible-api/">OpenAI互換API</a>へ文字起こしを渡し、<code class="language-plaintext highlighter-rouge">voice-profiles</code>がOllamaへ文章整形を依頼する構成です。</p>

<pre><code class="language-mermaid">flowchart LR
  U[User] --&gt; R[roBa / ZMK&lt;br/&gt;F13〜F18]
  R --&gt; A[AutoHotkey v2]
  A --&gt; H[Handy 0.9.4&lt;br/&gt;録音・ASR・貼り付け]
  A --&gt; V[voice-profiles&lt;br/&gt;FastAPI]
  H --&gt; ASR[Whisper Medium&lt;br/&gt;Raw Transcript]
  ASR --&gt;|Formatted Path| V
  V --&gt; O[Ollama 0.32.5]
  O --&gt; Q[Qwen3 8B&lt;br/&gt;Radeon 780M]
  Q --&gt; V
  V --&gt;|整形結果またはRaw Fallback| H
  ASR --&gt;|F16 Raw Path| APP[入力欄]
  H --&gt; APP

  classDef base fill:#d9eef2,stroke:#036982,color:#024450;
  classDef ai fill:#7eb3bf,stroke:#024450,color:#024450;
  class U,R,A,H,ASR,APP base;
  class V,O,Q ai;
</code></pre>

<p>F16のRaw Pathは<code class="language-plaintext highlighter-rouge">voice-profiles</code>とLLMを完全に迂回します。APIやLLMが停止しても、Handyが使えれば音声入力を続けられるようにしました。</p>

<h3 id="handyからローカルapiへつなぐ">HandyからローカルAPIへつなぐ</h3>

<p>Handyの後処理プロバイダーを<code class="language-plaintext highlighter-rouge">Custom</code>にすると、任意のlocalhost APIを呼び出せます。ここへ<code class="language-plaintext highlighter-rouge">voice-profiles</code>が提供する最小限のOpenAI互換APIを設定しました。</p>

<p><img src="/assets/images/posts/handy-local-llm-voice-input/handy-custom-post-processing.png" alt="Handyの後処理設定でCustom Provider、localhostのBase URL、voice-profiles-pocモデルを選択している" /></p>

<p>APIは<code class="language-plaintext highlighter-rouge">127.0.0.1</code>だけで待ち受け、文字起こし本文を通常ログへ保存しません。LLM出力は自動送信せず、入力欄へ貼り付けた後に人が確認します。</p>

<h3 id="1回の音声入力の流れ">1回の音声入力の流れ</h3>

<p>Formatted Pathでは、押したキーに対応するプロファイルを次の要求1回だけ有効にします。これにより、HandyをフォークせずにPrompt、Boss、Friendを切り替えました。</p>

<pre><code class="language-mermaid">sequenceDiagram
  actor User
  participant Key as roBa / AutoHotkey
  participant Handy
  participant API as voice-profiles
  participant LLM as Ollama / Qwen3

  User-&gt;&gt;Key: F13〜F15を押す
  Key-&gt;&gt;API: 次のProfileを選択
  Key-&gt;&gt;Handy: 録音開始
  User-&gt;&gt;Key: 同じキーで録音終了
  Key-&gt;&gt;Handy: 録音停止
  Handy-&gt;&gt;Handy: 音声認識
  Handy-&gt;&gt;API: Raw Transcript
  API-&gt;&gt;LLM: 選択Profileで整形
  LLM--&gt;&gt;API: 整形候補
  API-&gt;&gt;API: 出力を検証
  API--&gt;&gt;Handy: 整形結果またはRaw Fallback
  Handy--&gt;&gt;User: 入力欄へ貼り付け
</code></pre>

<p>タイムアウト、空出力、検証違反ではRaw Transcriptへ戻します。詳しい状態遷移は<a href="https://github.com/Reotech736/voice-profiles/blob/main/docs/architecture.md">MVPアーキテクチャ</a>に残しています。</p>

<h2 id="windows実機で試した構成">Windows実機で試した構成</h2>

<p>今回は構築手順の網羅ではなく、次の1環境で実用性を測りました。</p>

<table>
  <thead>
    <tr>
      <th>項目</th>
      <th>検証環境</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PC</td>
      <td>GMKtec NucBox K12</td>
    </tr>
    <tr>
      <td>CPU / GPU</td>
      <td>AMD Ryzen 7 H 255 / Radeon 780M</td>
    </tr>
    <tr>
      <td>メモリ</td>
      <td>64GB</td>
    </tr>
    <tr>
      <td>ASR</td>
      <td>Whisper Medium</td>
    </tr>
    <tr>
      <td>LLM</td>
      <td>Ollama 0.32.5 / Qwen3 8B Q4_K_M</td>
    </tr>
    <tr>
      <td>推論経路</td>
      <td>Vulkan</td>
    </tr>
  </tbody>
</table>

<h3 id="音声認識モデルを選ぶ">音声認識モデルを選ぶ</h3>

<p>Handyには複数の音声認識モデルがあります。日本語中の英字・数字と動作の安定性を比較し、今回はWhisper Mediumを選びました。</p>

<p><img src="/assets/images/posts/handy-local-llm-voice-input/handy-model-selection.png" alt="Handyのモデル選択画面にNemotron Streaming 3.5、Cohere Transcribe、Whisper Mediumなどが並んでいる" /></p>

<table>
  <thead>
    <tr>
      <th>モデル</th>
      <th>実機での観察</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Whisper Medium</td>
      <td>英字と数字は比較的正確。空白や句読点が弱い</td>
    </tr>
    <tr>
      <td>Nemotron Streaming 3.5</td>
      <td>軽快だが、日本語中の英字・略語・数字で誤認識が目立つ</td>
    </tr>
    <tr>
      <td>Cohere Transcribe</td>
      <td>単発精度は高いが、同じ英語句の長い反復を確認した</td>
    </tr>
  </tbody>
</table>

<p>固定音声では<code class="language-plaintext highlighter-rouge">Raw Path</code>が<code class="language-plaintext highlighter-rouge">Lowpass</code>になる誤認識もありました。後段のLLMだけで推測修正すると別の意味へ変える危険があるため、ASRと文章整形は分けて評価しています。条件は<a href="https://github.com/Reotech736/voice-profiles/blob/main/docs/verification/milestone-0-results.md">Milestone 0 技術検証結果</a>に記録しました。</p>

<h3 id="ローカルllmを選ぶ">ローカルLLMを選ぶ</h3>

<p>Radeon 780MでLLMを動かすため、Ollamaの実験的な<a href="/terms/vulkan/">Vulkan</a>経路を利用しました。Ollamaの<a href="https://docs.ollama.com/windows">Windows向け資料</a>と<a href="https://docs.ollama.com/gpu">GPU対応資料</a>でも、WindowsのAMD Radeon対応とVulkanの位置付けを確認できます。</p>

<table>
  <thead>
    <tr>
      <th>モデル</th>
      <th style="text-align: right">常駐後の処理時間</th>
      <th>判断</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Qwen3 8B Q4_K_M</td>
      <td style="text-align: right">1.91〜2.09秒</td>
      <td>暫定採用</td>
    </tr>
    <tr>
      <td>Qwen3 14B Q4_K_M</td>
      <td style="text-align: right">3.30〜3.63秒</td>
      <td>品質差が小さく不採用</td>
    </tr>
  </tbody>
</table>

<p>Qwen3 8Bの推論中は、Radeon 780MのGPU使用率が約80%まで上がりました。内蔵GPUでモデル全体を動かせていることは確認できました。</p>

<p><img src="/assets/images/posts/handy-local-llm-voice-input/radeon-780m-llm-usage.png" alt="WindowsタスクマネージャーでQwen3推論中のRadeon 780Mが約80パーセント使用されている" /></p>

<p>短い固定入力では約2秒でしたが、これはOllama単体の測定です。詳細は<a href="https://github.com/Reotech736/voice-profiles/blob/main/docs/verification/milestone-1-results.md">Milestone 1 ローカルLLM基盤 実測記録</a>に残しています。</p>

<h2 id="12件を実際のapi経路で評価する">12件を実際のAPI経路で評価する</h2>

<p>最終判断では、匿名化したRaw Transcript 4件を3プロファイルへ投入しました。合計12件をtemperature 0、seed 42、10秒上限、リトライなしで実行した結果です。</p>

<table>
  <thead>
    <tr>
      <th>指標</th>
      <th style="text-align: right">結果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Formatted / Raw Fallback</td>
      <td style="text-align: right">6 / 6</td>
    </tr>
    <tr>
      <td>自動品質条件まで合格</td>
      <td style="text-align: right">3 / 12</td>
    </tr>
    <tr>
      <td>平均 / 最大</td>
      <td style="text-align: right">5.551秒 / 9.855秒</td>
    </tr>
    <tr>
      <td>3秒以内</td>
      <td style="text-align: right">0 / 12</td>
    </tr>
    <tr>
      <td>5秒以内</td>
      <td style="text-align: right">7 / 12</td>
    </tr>
  </tbody>
</table>

<p>Ollama単体では約2秒だった8Bモデルも、実際のAPI経路では平均5.551秒かかりました。代表的な品質問題は次のとおりです。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">F17ではなくF18</code>から<code class="language-plaintext highlighter-rouge">F17</code>を削除した</li>
  <li>「笑は付けないでね」という明示情報を削除した</li>
  <li>Bossが丁寧語ではなく<code class="language-plaintext highlighter-rouge">大丈夫だ</code>へ変えた</li>
  <li>Friendは数秒待ってもRawとほぼ変わらない場合があった</li>
</ul>

<p>英数字識別子の欠落を検出するvalidatorを追加すると、問題のある候補を貼り付けずRawへ戻せるようになりました。ただし、これは品質改善ではなく入力を守る安全策です。全結果は<a href="https://github.com/Reotech736/voice-profiles/blob/main/docs/verification/evaluation-12-results.md">12件プロファイル評価結果</a>で確認できます。</p>

<h2 id="handy単体の運用に戻した理由">Handy単体の運用に戻した理由</h2>

<p>Formatted Pathは平均約5.5秒待ったうえで、半分がRaw Fallbackになりました。自動品質条件を満たしたのも4分の1で、毎回待つほど安定した価値にはなっていません。</p>

<p>一方、HandyのWhisper Mediumだけを使う経路は速く、すでに日常の音声入力として便利でした。そのため現在は、次の運用にしています。</p>

<ul>
  <li>日常の既定はHandyによる通常の文字起こし</li>
  <li>AI整形は積極利用しない</li>
  <li>実装と固定評価は、将来の再検証用に残す</li>
</ul>

<p>HandyやローカルLLM全般が使えないという結論ではありません。今回のPC、モデル、プロンプトでは、既定経路にする価値を示せなかったという判断です。</p>

<h2 id="再開条件">再開条件</h2>

<p>より高速な小型モデルやRadeon 780M向けランタイムが登場したときは、同じ12件を再実行します。再開の目安は次のとおりです。</p>

<ul>
  <li>平均3秒以内、12件すべて5秒以内</li>
  <li>自動品質合格10 / 12以上</li>
  <li>重大な意味変更が0件</li>
  <li>Raw Fallbackが例外的である</li>
  <li>3プロファイルに待ち時間に見合う差がある</li>
</ul>

<h2 id="まとめ">まとめ</h2>

<p>Handy、AutoHotkey、localhostのAPI、Ollama、Qwen3を組み合わせた音声入力環境は動きました。しかし、実経路では平均5.551秒、自動品質合格3 / 12となり、日常利用にはHandy単体の方が合っていました。</p>

<p>「動いた」と「毎日使える」を分けて評価できたことが、今回の一番の成果です。固定評価と再開条件を残したので、環境が進歩したときに同じ基準で試し直せます。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Windows" /><category term="AI" /><category term="音声入力" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">初めてのIaCで学んだAWS CloudFormation</title><link href="https://reotech736.com/2026/07/16/first-cloudformation-iac.html" rel="alternate" type="text/html" title="初めてのIaCで学んだAWS CloudFormation" /><published>2026-07-16T00:00:00+09:00</published><updated>2026-07-16T00:00:00+09:00</updated><id>https://reotech736.com/2026/07/16/first-cloudformation-iac</id><content type="html" xml:base="https://reotech736.com/2026/07/16/first-cloudformation-iac.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>前回、自宅Linuxサーバの監視値をCustom GPTから確認できる環境を作りました。AWS側にはAMP、IAM、API Gateway、Lambda、Cognitoなど複数のリソースがあり、これらを手作業ではなく<a href="/terms/infrastructure-as-code/">Infrastructure as Code（IaC）</a>で構築しました。</p>

<p>使用したのは<a href="/terms/aws-cloudformation/">AWS CloudFormation</a>です。これが私にとって初めてのIaCだったため、使う前に疑問だったことと、実際に使って分かったことを初心者目線でまとめます。</p>

<p>この記事の要点は次の4つです。</p>

<ul>
  <li>なぜAWSリソースをコードで管理するのか</li>
  <li>CloudFormationのテンプレートとスタックとは何か</li>
  <li>Linux監視環境でどのように使ったか</li>
  <li>初めて使って便利だった点と注意点</li>
</ul>

<h2 id="iacとcloudformationの基本">IaCとCloudFormationの基本</h2>

<h3 id="なぜiacを使うのか">なぜIaCを使うのか</h3>

<p>AWS Management Consoleから一つずつ作る方法は、動きを理解しやすい一方、リソースが増えるほど手順と設定を追いにくくなります。IaCでは、必要なリソースと期待する状態をテキストファイルへ宣言します。</p>

<table>
  <thead>
    <tr>
      <th>手作業で困りやすいこと</th>
      <th>IaCで変わること</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>作成手順を人が覚える</td>
      <td>テンプレートに手順と設定が残る</td>
    </tr>
    <tr>
      <td>同じ環境でも設定差が出る</td>
      <td>同じ定義から繰り返し作成できる</td>
    </tr>
    <tr>
      <td>何を変更したか追いにくい</td>
      <td>Gitで差分と理由を確認できる</td>
    </tr>
    <tr>
      <td>リソース間の値をコピーする</td>
      <td>参照関係として記述できる</td>
    </tr>
  </tbody>
</table>

<p>AWS公式の<a href="https://aws.amazon.com/what-is/iac/">IaC解説</a>でも、手作業ではなくコードでインフラを用意・維持し、再現可能な構成として扱う考え方が説明されています。</p>

<p>ただし、IaCにすれば設計や確認が不要になるわけではありません。権限、料金、削除時の影響を、レビューできる形にするための手段だと感じました。</p>

<h3 id="cloudformationとは">CloudFormationとは</h3>

<p>CloudFormationは、テンプレートに記述したAWSリソースを作成・更新するサービスです。最初に覚えた用語は次の3つでした。</p>

<ul>
  <li><strong>テンプレート</strong>: 必要なリソースと設定をYAMLまたはJSONで記述したファイル</li>
  <li><strong>スタック</strong>: 一つのテンプレートから作成・管理されるリソースのまとまり</li>
  <li><strong>変更セット</strong>: スタックへ適用する予定の追加・変更・削除を事前確認する仕組み</li>
</ul>

<pre><code class="language-mermaid">flowchart LR
  YAML[CloudFormationテンプレート] --&gt; Stack[CloudFormationスタック]
  Stack --&gt; AMP[AMP workspace]
  Stack --&gt; IAM[IAMユーザー]
</code></pre>

<p>リソース同士に参照関係がある場合は、CloudFormationが依存関係を判断して作成します。基本的な仕組みは、AWS公式の<a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html">CloudFormationユーザーガイド</a>でも確認できます。</p>

<h2 id="linux監視環境での実践">Linux監視環境での実践</h2>

<h3 id="今回作成した2つのスタック">今回作成した2つのスタック</h3>

<p>Linux監視環境では、役割を分けて2つのスタックを使いました。</p>

<table>
  <thead>
    <tr>
      <th>スタック</th>
      <th>主な管理対象</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">linux-monitoring-gpt-amp</code></td>
      <td>AMP workspace、Agent用IAMユーザー</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">linux-monitoring-gpt-diagnostic-api</code></td>
      <td>API Gateway、Lambda、Cognito、IAM</td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/posts/first-cloudformation-iac/cloudformation-stacks.png" alt="CloudFormationコンソールで、Linux Monitoring GPT用の2スタックが正常な状態になっている一覧。2026年7月16日確認時点の画面" /></p>

<p>スタック名と状態がまとまっているため、AWS側の構成をどの単位で管理しているか分かりやすくなりました。</p>

<h3 id="最初は2つのリソースから始めた">最初は2つのリソースから始めた</h3>

<p>最初のテンプレートで作成したのは、次の2リソースだけです。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">AmpWorkspace</code>: Linuxサーバの監視メトリクスを保存するAMP workspace</li>
  <li><code class="language-plaintext highlighter-rouge">AgentUser</code>: 対象workspaceへの<code class="language-plaintext highlighter-rouge">aps:RemoteWrite</code>だけを許可するIAMユーザー</li>
</ul>

<p><img src="/assets/images/posts/first-cloudformation-iac/amp-stack-resources.png" alt="CloudFormationのlinux-monitoring-gpt-ampスタックで、AmpWorkspaceとAgentUserの2リソースがCREATE_COMPLETEになっている。Physical IDは非表示" /></p>

<p>テンプレートの中心部分は次のようになっています。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Resources</span><span class="pi">:</span>
  <span class="na">AmpWorkspace</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::APS::Workspace</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Alias</span><span class="pi">:</span> <span class="s">linux-monitoring-gpt-poc</span>

  <span class="na">AgentUser</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::IAM::User</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Policies</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">PolicyName</span><span class="pi">:</span> <span class="s">amp-remote-write</span>
          <span class="na">PolicyDocument</span><span class="pi">:</span>
            <span class="na">Statement</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="na">Effect</span><span class="pi">:</span> <span class="s">Allow</span>
                <span class="na">Action</span><span class="pi">:</span> <span class="s">aps:RemoteWrite</span>
                <span class="na">Resource</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s">AmpWorkspace.Arn</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">!GetAtt AmpWorkspace.Arn</code>によって、作成したAMP workspaceのARNをIAMポリシーから参照しています。ARNを手でコピーせず、リソース同士の関係をテンプレート内に書ける点が印象に残りました。</p>

<p>アクセスキーは秘密情報として別管理するため、CloudFormationでは作成していません。すべてをIaCへ入れるのではなく、管理範囲を決めることも必要でした。</p>

<h3 id="変更内容を見てから作成する">変更内容を見てから作成する</h3>

<p>今回は、次の順序でスタックを作成しました。</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">validate-template</code>でテンプレートを検証する</li>
  <li><code class="language-plaintext highlighter-rouge">create-change-set</code>で変更セットを作る</li>
  <li><code class="language-plaintext highlighter-rouge">describe-change-set</code>で対象リソースを確認する</li>
  <li><code class="language-plaintext highlighter-rouge">execute-change-set</code>で変更を実行する</li>
</ol>

<p>IAMユーザーを作るため、変更セット作成時には<code class="language-plaintext highlighter-rouge">CAPABILITY_NAMED_IAM</code>も指定しました。権限に関わるリソースを作ることへ、明示的に了承するための指定です。</p>

<p>変更セットは、既存リソースの置換や削除へ気付くための大切な確認箇所です。ただし、AWS公式の<a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html">変更セットの説明</a>にもあるとおり、デプロイの成功を保証するものではありません。</p>

<h3 id="aws-samで構成を広げた">AWS SAMで構成を広げた</h3>

<p>診断API側では、LambdaやAPI Gatewayを簡潔に書けるAWS Serverless Application Model（AWS SAM）を使いました。SAMはCloudFormationの拡張で、デプロイ時にはCloudFormationが扱うリソースへ変換されます。</p>

<p><img src="/assets/images/posts/first-cloudformation-iac/diagnostic-api-stack-resources.png" alt="AWS SAMでデプロイしたlinux-monitoring-gpt-diagnostic-apiスタックに、API Gateway、Lambda、Cognito、IAMなどのリソースが並んでいる。Physical IDは非表示" /></p>

<p>最初は2リソースだったスタック管理を、API Gateway、Lambda、Cognitoを含む構成へ広げられました。SAMとCloudFormationの関係は、AWS公式の<a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html">AWS SAM概要</a>でも説明されています。</p>

<h2 id="使って分かったこと">使って分かったこと</h2>

<h3 id="実際に感じた利点">実際に感じた利点</h3>

<table>
  <thead>
    <tr>
      <th>利点</th>
      <th>実感したこと</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>構成が残る</td>
      <td>AMPの保持期間やIAM権限を後からテンプレートで確認できる</td>
    </tr>
    <tr>
      <td>差分を確認できる</td>
      <td>Gitで、いつ何を変えたか追える</td>
    </tr>
    <tr>
      <td>依存関係を書ける</td>
      <td>ARNなどの値を手作業でコピーせず参照できる</td>
    </tr>
    <tr>
      <td>管理単位が明確になる</td>
      <td>関連リソースをスタックとしてまとめられる</td>
    </tr>
    <tr>
      <td>変更前に確認できる</td>
      <td>変更セットで追加・変更・削除の予定を見られる</td>
    </tr>
  </tbody>
</table>

<p>一番大きかったのは、AWS環境を「画面で作ったもの」ではなく「Gitに残る構成」として考えられるようになったことです。</p>

<h3 id="使って学んだ注意点">使って学んだ注意点</h3>

<ul>
  <li><strong>検証だけでは作成成功まで保証されない</strong>: サービスクォータや実行時の条件によって失敗する可能性がある</li>
  <li><strong>変更内容によってはリソースが置換される</strong>: データを持つリソースでは<code class="language-plaintext highlighter-rouge">Replacement</code>と削除ポリシーを確認する</li>
  <li><strong>コンソールで直接変更するとドリフトが生まれる</strong>: テンプレートの期待状態と実際の設定がずれる</li>
  <li><strong>スタック外のリソースは自動で片付かない</strong>: 手動作成したアクセスキーなどは別の削除手順が必要</li>
  <li><strong>IaCでも権限と料金の設計は必要</strong>: 広すぎる権限や高コストな構成も、そのまま再現される</li>
</ul>

<p>ドリフトの対象や制約は、AWS公式の<a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html">ドリフト検出ガイド</a>で確認できます。</p>

<h2 id="まとめ">まとめ</h2>

<p>初めてのIaCは、小さな2リソースのテンプレートから始めました。今回の学びを短くまとめると、次のとおりです。</p>

<ul>
  <li>IaCはインフラの期待状態をコードとして残す方法</li>
  <li>CloudFormationはAWSリソースをスタック単位で管理する</li>
  <li>変更セットを確認してから適用する</li>
  <li>秘密情報、権限、料金、削除方法は別途設計する</li>
  <li>コンソールで直接変更した場合はドリフトを意識する</li>
</ul>

<p>便利さだけでなく、どこまでを管理し、何を確認するべきかも具体的に理解できました。今後もテンプレートを小さく変更し、差分を確認してから適用する流れを続けます。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="AWS" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Custom GPTでLinuxサーバを診断する</title><link href="https://reotech736.com/2026/07/14/linux-monitoring-gpt.html" rel="alternate" type="text/html" title="Custom GPTでLinuxサーバを診断する" /><published>2026-07-14T00:00:00+09:00</published><updated>2026-07-14T00:00:00+09:00</updated><id>https://reotech736.com/2026/07/14/linux-monitoring-gpt</id><content type="html" xml:base="https://reotech736.com/2026/07/14/linux-monitoring-gpt.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>自宅Linuxサーバの状態を、ChatGPTから「今どうなっている？」と確認できるようにしました。ただし、GPTへSSH接続やサーバ操作の権限は渡していません。<a href="/terms/prometheus/">Prometheus</a>で収集した監視値だけを返す小さなAPIを用意し、<a href="/terms/custom-gpt-actions/">Custom GPTのActions</a>から呼び出す構成です。</p>

<p>この記事では、CPU使用率とメモリ使用率を自然言語で確認できるようになるまでの構成と、安全性のために絞った公開範囲をまとめます。実装は<a href="https://github.com/Reotech736/linux-monitoring-gpt">linux-monitoring-gpt</a>で公開しています。</p>

<h2 id="できること">できること</h2>

<p>Custom GPTにサーバの状態を尋ねると、現在の監視値を日本語で要約します。</p>

<p><img src="/assets/images/posts/linux-monitoring-gpt/chatgpt-diagnostic-result.png" alt="Custom GPTがLinuxサーバのCPU・メモリ・ディスク使用率、ロードアベレージ、稼働時間、アラートの有無を日本語で要約している。2026年7月14日確認時点の画面" /></p>

<p>取得できる主な項目は次のとおりです。</p>

<table>
  <thead>
    <tr>
      <th>項目</th>
      <th>内容</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>到達可否</td>
      <td><a href="/terms/node-exporter/">Node Exporter</a>を監視上確認できるか</td>
    </tr>
    <tr>
      <td>CPU使用率</td>
      <td>5分平均の使用率</td>
    </tr>
    <tr>
      <td>メモリ使用率</td>
      <td>利用可能メモリから算出した使用率</td>
    </tr>
    <tr>
      <td>補助情報</td>
      <td>最大ディスク使用率、5分ロード、稼働時間、アラート</td>
    </tr>
  </tbody>
</table>

<p>一方で、次の操作はできないようにしています。</p>

<ul>
  <li>サーバへのログイン、コマンド実行、設定変更</li>
  <li>任意のPromQLや任意のホスト名の指定</li>
  <li>メトリクスや監視基盤への書き込み</li>
</ul>

<h2 id="全体構成">全体構成</h2>

<p>監視値の収集、保存、可視化、自然言語での確認を分けて構成しました。</p>

<pre><code class="language-mermaid">flowchart LR
  subgraph Home[自宅Linuxサーバ]
    NE[Node Exporter&lt;br/&gt;systemd / 127.0.0.1:9100]
    PA[Prometheus Agent&lt;br/&gt;Docker Compose]
    LP[Prometheus&lt;br/&gt;Docker Compose]
    GF[Grafana&lt;br/&gt;LAN・Tailscale限定]
    NE --&gt; PA
    NE --&gt; LP
    LP --&gt; GF
  end

  subgraph AWS["AWSリソース"]
    AMP[Amazon Managed Service for Prometheus]
    API[API Gateway + Lambda&lt;br/&gt;読み取り専用API]
  end

  PA --&gt;|remote_write&lt;br/&gt;SigV4・HTTPS| AMP
  GPT[Custom GPT] --&gt;|Cognito OAuth| API
  API --&gt;|固定PromQL| AMP
</code></pre>

<p>外部へ公開するのは、Custom GPT用の読み取り専用APIだけです。Node Exporter、Prometheus、Prometheus Agentはインターネットへ公開せず、Grafanaも自宅LANとTailscaleからだけ使います。</p>

<p>なお、この構成で使っているのはMCPサーバではなく、Custom GPTのActionsです。OpenAPIスキーマでHTTP APIをActionとして登録しています。</p>

<h2 id="この構成にした理由">この構成にした理由</h2>

<p>最初は<a href="/terms/grafana/">Grafana</a>だけで確認する構成も考えました。時系列を詳しく見るにはGrafanaが適していますが、外出先からスマホで「CPUは高いか」「メモリは足りているか」を確認するには、ダッシュボードを開いて読み取る手間があります。</p>

<p>今回検討した選択肢と判断は次のとおりです。</p>

<table>
  <thead>
    <tr>
      <th>選択肢</th>
      <th>採らなかった、または役割を限定した理由</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Grafanaだけを使う</td>
      <td>詳細な時系列には向く一方、短い状態確認には情報量が多い</td>
    </tr>
    <tr>
      <td>Node ExporterやPrometheusを公開する</td>
      <td>監視用ポートをインターネットへ出す必要があり、公開範囲が広くなる</td>
    </tr>
    <tr>
      <td>GPTから<a href="/terms/amazon-managed-service-for-prometheus/">AMP</a>やサーバへ直接接続する</td>
      <td>GPTに必要以上の接続先・権限・クエリ自由度を渡すことになる</td>
    </tr>
    <tr>
      <td>任意<a href="/terms/promql/">PromQL</a>を受け付けるAPI</td>
      <td>便利だが、想定外の高コストなクエリや情報の露出を防ぎにくい</td>
    </tr>
  </tbody>
</table>

<p>そのため、GPTには状態確認に必要な値だけを返すAPIを渡し、監視基盤の詳細操作はGrafanaとAWS側へ残すことにしました。取得元のメトリクスをAMPへ保存しているため、インターネット接続とChatGPTが使える環境であれば、外出先のスマホからでも状態を確認できます。利用時は<a href="/terms/cognito-oauth/">Cognito OAuth</a>で認証するため、GPTのリンクを知っているだけでは監視値を取得できません。</p>

<h2 id="実装の流れ">実装の流れ</h2>

<h3 id="1-node-exporterでホストの状態を集める">1. Node Exporterでホストの状態を集める</h3>

<p>Node Exporterはsystemdサービスとして動かし、<code class="language-plaintext highlighter-rouge">127.0.0.1:9100</code>だけで待ち受けます。Dockerコンテナ内ではなくホスト上で動かすことで、CPU、メモリ、ディスクなどOS全体の状態を素直に取得できます。</p>

<p>Prometheus AgentはDocker Composeで動かし、Node Exporterから必要なメトリクスだけを収集してAMPへ送信します。送信時はHTTPSと<a href="/terms/aws-signature-version-4/">AWS Signature Version 4（SigV4）</a>で通信とリクエストを保護します。最初の成功条件は、AMP上で次の値が<code class="language-plaintext highlighter-rouge">1</code>になることでした。</p>

<pre><code class="language-promql">up{host_id="home-server", job="node-exporter"}
</code></pre>

<h3 id="2-grafanaで時系列を確認する">2. Grafanaで時系列を確認する</h3>

<p>GPTは現在値を短く確認するための入口です。推移や負荷の変化は、ローカルPrometheusとGrafanaで確認します。</p>

<p><img src="/assets/images/posts/linux-monitoring-gpt/grafana-home-server-overview.png" alt="Home Server Overview。Node Exporterの稼働状態、CPU使用率、メモリ使用率、最大ディスク使用率、ロードアベレージを表示している" /></p>

<p>Grafanaのダッシュボードには、次の値をまとめました。</p>

<ul>
  <li>Node Exporterの稼働状態</li>
  <li>CPU使用率とメモリ使用率</li>
  <li>最大ディスク使用率</li>
  <li>1分・5分・15分のロードアベレージ</li>
</ul>

<h3 id="3-gptには固定クエリのapiだけを渡す">3. GPTには固定クエリのAPIだけを渡す</h3>

<p>Custom GPTがサーバやAMPへ直接接続するのではなく、<code class="language-plaintext highlighter-rouge">GET /hosts/home-server/status</code>だけを公開しました。Lambdaが固定PromQLでAMPを照会し、GPTに必要な値だけをJSONで返します。</p>

<p>このAPIは<code class="language-plaintext highlighter-rouge">home-server</code>以外を受け付けず、任意のPromQLやシェルコマンドも受け付けません。APIが失敗したときは、GPTが正常と推測せず「監視APIが応答できなかった」と案内するようにしています。</p>

<h3 id="4-cognito-oauthで利用者を認可する">4. Cognito OAuthで利用者を認可する</h3>

<p>共有APIキーでは、GPTを使った利用者を区別できません。そこでCognito OAuth 2.0を使い、利用者ごとに認証する構成へ移行しました。</p>

<table>
  <thead>
    <tr>
      <th>確認箇所</th>
      <th>役割</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="/terms/api-gateway/">API Gateway</a></td>
      <td>アクセストークン、client ID、<code class="language-plaintext highlighter-rouge">linux-monitoring/status.read</code>スコープを検証</td>
    </tr>
    <tr>
      <td><a href="/terms/aws-lambda/">AWS Lambda</a></td>
      <td>Cognitoグループを確認し、対象ホストの閲覧を許可</td>
    </tr>
    <tr>
      <td>API</td>
      <td>固定クエリの読み取り結果だけを返す</td>
    </tr>
  </tbody>
</table>

<p>未認証はHTTP 401、閲覧グループ外はHTTP 403、AMPの照会失敗はHTTP 502として扱います。認証、認可、監視データの取得失敗を分けて確認できるようにしました。</p>

<h2 id="つまずいたところ">つまずいたところ</h2>

<h3 id="node-exporterのポート競合">Node Exporterのポート競合</h3>

<p>待受アドレスを変更した際に、既存プロセスと再起動が重なって<code class="language-plaintext highlighter-rouge">address already in use</code>になりました。設定ファイルだけではなく、次の2点を分けて確認する必要がありました。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ss <span class="nt">-ltnH</span> | rg <span class="s1">':9100\b'</span>
curl http://127.0.0.1:9100/metrics
</code></pre></div></div>

<h3 id="cognitoのログイン画面とcallback-url">Cognitoのログイン画面とcallback URL</h3>

<p>CognitoはApp Clientを作るだけではmanaged login画面を使えず、login brandingの設定も必要でした。また、callback URLは公開GPTのURLから推測せず、GPT Editorに表示された値をそのまま登録する必要があります。</p>

<p>OAuth Client Secretや<a href="/terms/iam/">AWS Identity and Access Management（IAM）</a>認証情報は、Git、OpenAPIスキーマ、記事本文、画面キャプチャへ保存しません。</p>

<h2 id="現在の使い分けと今後">現在の使い分けと今後</h2>

<p>日常的な使い分けは次のとおりです。</p>

<ul>
  <li><strong>Custom GPT</strong>: 「今のCPUやメモリは大丈夫か」をすぐに確認する</li>
  <li><strong>Grafana</strong>: 負荷や使用率の推移を時系列で確認する</li>
</ul>

<p>今後は、Node ExporterやPrometheus Agentの停止、メトリクス欠損、閾値超過を安全に再現し、GPTが推測せず状況を説明できるかを検証します。その後、通知、必要なメトリクスの追加、複数ホスト対応を段階的に検討する予定です。</p>

<p>特に次の段階では、Dockerコンテナの稼働状況も確認できるようにしたいと考えています。コンテナ名を自由入力で受け付けるのではなく、監視対象と返却項目をAPI側で明示的に定義し、現在の読み取り専用・最小権限の方針を保ったまま診断APIを拡張する予定です。</p>

<h2 id="まとめ">まとめ</h2>

<p>Custom GPTにサーバ操作権限を渡さなくても、読み取り専用APIを挟めばCPU・メモリなどの状態を自然言語で確認できます。</p>

<ul>
  <li>監視の入口は<code class="language-plaintext highlighter-rouge">up == 1</code>という小さな成功条件から作る</li>
  <li>時系列の確認はGrafana、現在値の問い合わせはCustom GPTに分ける</li>
  <li>任意クエリを受け付けず、OAuthとグループで閲覧範囲を絞る</li>
  <li>AMPへ保存したメトリクスを使い、外出先からもChatGPTで状態を確認する</li>
</ul>

<p>監視と公開範囲を少しずつ広げることで、自宅サーバの状態を安全に確認できる仕組みになりました。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Ubuntu" /><category term="Docker" /><category term="AWS" /><category term="ChatGPT" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">tmuxのインストールと基本操作｜SSH作業向けチートシート</title><link href="https://reotech736.com/2026/07/05/tmux-install.html" rel="alternate" type="text/html" title="tmuxのインストールと基本操作｜SSH作業向けチートシート" /><published>2026-07-05T00:00:00+09:00</published><updated>2026-07-05T00:00:00+09:00</updated><id>https://reotech736.com/2026/07/05/tmux-install</id><content type="html" xml:base="https://reotech736.com/2026/07/05/tmux-install.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>自宅のLinuxサーバへ<a href="/terms/ssh/">SSH</a>接続して作業していると、通信が切れたときに実行中のコマンドまで終了してしまうことがあります。時間のかかる処理を動かしている場合や、複数のターミナルを行き来する場合に備えて、<a href="/terms/terminal-multiplexer/">ターミナルマルチプレクサ</a>の<a href="/terms/tmux/">tmux</a>を導入しました。</p>

<p>この記事では、Ubuntuへのインストールから、<a href="/terms/tmux-session/">セッション</a>の作成・デタッチ・再接続、<a href="/terms/tmux-window/">ウィンドウ</a>と<a href="/terms/tmux-pane/">ペイン</a>の操作までを整理します。最後に、普段使うコマンドとキー操作をチートシートとしてまとめます。</p>

<p>今回の確認環境は次のとおりです。</p>

<ul>
  <li>OS: Ubuntu 24.04.3 LTS</li>
  <li>tmux: 3.4</li>
  <li>接続方法: SSH</li>
</ul>

<p>記事内のキー操作は、<code class="language-plaintext highlighter-rouge">~/.tmux.conf</code>で変更していないデフォルト設定を前提としています。</p>

<h2 id="tmuxをインストールする">tmuxをインストールする</h2>

<p>UbuntuやDebianでは、APTからtmuxをインストールできます。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>tmux
</code></pre></div></div>

<p>インストール後にバージョンを確認します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux <span class="nt">-V</span>
</code></pre></div></div>

<p>今回の環境では、次のように表示されました。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux 3.4
</code></pre></div></div>

<p>これでtmuxを利用する準備は完了です。</p>

<h2 id="tmuxとは">tmuxとは</h2>

<p>tmuxは、1つのターミナル上で複数のシェルやプログラムを扱うためのターミナルマルチプレクサです。ターミナルのタブに近い「ウィンドウ」と、画面分割に使う「ペイン」を組み合わせて作業できます。</p>

<p>SSHでサーバを操作するときは、作業を「セッション」としてサーバ側へ残せる点が特に便利です。SSH接続が切れてtmuxの画面から離れても、セッション内のプログラムは動作を続けるため、再接続して同じ画面へ戻れます。</p>

<p>ただし、tmuxはサーバの再起動をまたいで状態を保存する仕組みではありません。サーバを再起動するとtmuxのセッションも終了するため、必要なデータはファイルへ保存し、常時稼働させるサービスにはsystemdやDockerの再起動設定などを使います。</p>

<h2 id="セッションウィンドウペインの構造">セッション・ウィンドウ・ペインの構造</h2>

<p>tmuxを使ううえでは、セッション・ウィンドウ・ペインの3つを押さえておくと操作を理解しやすくなります。</p>

<pre><code class="language-mermaid">flowchart TB
  subgraph Session["セッション: blog"]
    direction LR
    subgraph Window0["ウィンドウ 0: editor"]
      direction TB
      Pane0["ペイン 0&lt;br&gt;エディタ"]
      Pane1["ペイン 1&lt;br&gt;開発サーバ"]
    end
    subgraph Window1["ウィンドウ 1: logs"]
      direction TB
      Pane2["ペイン 0&lt;br&gt;ログ監視"]
      Pane3["ペイン 1&lt;br&gt;Git操作"]
    end
  end
</code></pre>

<h3 id="セッション">セッション</h3>

<p>セッションは、tmuxで管理する作業全体の単位です。<code class="language-plaintext highlighter-rouge">blog</code>、<code class="language-plaintext highlighter-rouge">docker</code>、<code class="language-plaintext highlighter-rouge">maintenance</code>のように用途ごとの名前を付けると、複数の作業を見分けやすくなります。</p>

<h3 id="ウィンドウ">ウィンドウ</h3>

<p>ウィンドウは、セッション内に作る作業画面です。一般的なターミナルアプリのタブに近く、エディタ、Git操作、ログ確認などをウィンドウごとに分けられます。</p>

<h3 id="ペイン">ペイン</h3>

<p>ペインは、1つのウィンドウを分割した領域です。左側でエディタを開き、右側で開発サーバを実行するなど、複数のシェルを同時に表示できます。</p>

<h2 id="tmuxセッションを作成する">tmuxセッションを作成する</h2>

<p>tmuxを引数なしで実行すると、自動的に番号が付いたセッションを作成して接続します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux
</code></pre></div></div>

<p>サーバ作業では、あとから目的を判別できるように名前付きセッションを作成する方が管理しやすくなります。次のコマンドは、<code class="language-plaintext highlighter-rouge">blog</code>というセッションを作成して接続します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux new <span class="nt">-s</span> blog
</code></pre></div></div>

<p>同じ名前のセッションがすでに存在する可能性がある場合は、<code class="language-plaintext highlighter-rouge">-A</code>を付ける方法もあります。<code class="language-plaintext highlighter-rouge">blog</code>セッションがあれば接続し、なければ新しく作成します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux new <span class="nt">-A</span> <span class="nt">-s</span> blog
</code></pre></div></div>

<p>普段使うセッションが決まっている場合は、このコマンドを覚えておくと作成と再接続を1つにまとめられます。</p>

<h2 id="プレフィックスキーの押し方">プレフィックスキーの押し方</h2>

<p>tmux内の操作は、多くの場合<code class="language-plaintext highlighter-rouge">Ctrl + b</code>から始めます。この<code class="language-plaintext highlighter-rouge">Ctrl + b</code>はプレフィックスキーと呼ばれ、続けて押したキーによってtmuxへ操作を指示します。</p>

<p>例えば、セッションからデタッチするキー操作は次のように表します。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + b → d
</code></pre></div></div>

<p>実際には、次の順番で操作します。</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Ctrl</code>を押しながら<code class="language-plaintext highlighter-rouge">b</code>を押す</li>
  <li>いったんキーを離す</li>
  <li><code class="language-plaintext highlighter-rouge">d</code>を押す</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">Ctrl + b + d</code>を同時に押す操作ではありません。キー操作が分からなくなったときは、<code class="language-plaintext highlighter-rouge">Ctrl + b → ?</code>でデフォルトのキーバインド一覧を確認できます。</p>

<h2 id="デタッチして作業を残す">デタッチして作業を残す</h2>

<p>tmuxセッションと実行中のプログラムを残したまま、元のシェルへ戻る操作をデタッチと呼びます。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + b → d
</code></pre></div></div>

<p>デタッチすると、次のようなメッセージが表示されます。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[detached (from session blog)]
</code></pre></div></div>

<p>SSH接続が意図せず切れた場合も、tmuxセッションはサーバ側に残ります。作業を終えるわけではなく一時的に離れる場合は、<code class="language-plaintext highlighter-rouge">exit</code>ではなくデタッチを使うと意図が明確です。</p>

<h2 id="セッションを確認して再接続する">セッションを確認して再接続する</h2>

<p>起動中のセッションを確認するには、tmuxの外側で次のコマンドを実行します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux <span class="nb">ls</span>
</code></pre></div></div>

<p>複数のセッションがある場合は、次のように表示されます。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blog: 1 windows (created Sun Jul  5 15:30:00 2026)
docker: 2 windows (created Sun Jul  5 15:35:00 2026)
</code></pre></div></div>

<p>セッションが1つもない場合は、<code class="language-plaintext highlighter-rouge">no server running</code>と表示されます。これはtmuxの故障ではなく、接続できるセッションが存在しない状態です。</p>

<p><code class="language-plaintext highlighter-rouge">blog</code>セッションへ再接続するには、対象名を指定します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux a <span class="nt">-t</span> blog
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">attach-session</code>の公式なエイリアスが<code class="language-plaintext highlighter-rouge">attach</code>です。さらに、tmuxは他のコマンドと区別できる範囲までコマンド名を省略できるため、普段の操作では<code class="language-plaintext highlighter-rouge">a</code>まで短くできます。</p>

<p><code class="language-plaintext highlighter-rouge">tmux a</code>、<code class="language-plaintext highlighter-rouge">tmux at</code>、<code class="language-plaintext highlighter-rouge">tmux attach</code>、<code class="language-plaintext highlighter-rouge">tmux attach-session</code>はいずれも接続コマンドとして動作します。対話操作では短い<code class="language-plaintext highlighter-rouge">a</code>が便利ですが、手順書やスクリプトでは意味を読み取りやすい<code class="language-plaintext highlighter-rouge">attach</code>を使う方が分かりやすくなります。</p>

<p>対象を指定せずに<code class="language-plaintext highlighter-rouge">tmux a</code>を実行すると、直近に使った接続可能なセッションへ接続します。複数のセッションを使う場合は、意図しないセッションへ入らないように名前を指定する方が確実です。</p>

<h2 id="セッションを終了する">セッションを終了する</h2>

<p>tmux内で<code class="language-plaintext highlighter-rouge">exit</code>を実行すると、現在のペインで動いているシェルが終了し、そのペインが閉じます。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">exit</span>
</code></pre></div></div>

<p>ウィンドウ内の最後のペインを閉じると、そのウィンドウも削除されます。セッション内の最後のウィンドウまで閉じると、セッション自体が終了します。</p>

<p>tmuxの外側からセッションを指定して終了する場合は、<code class="language-plaintext highlighter-rouge">kill-session</code>を使います。セッション内で動いている処理も終了するため、対象名を確認してから実行します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux kill-session <span class="nt">-t</span> blog
</code></pre></div></div>

<h2 id="ウィンドウを操作する">ウィンドウを操作する</h2>

<p>1つのセッション内に複数のウィンドウを作ると、エディタ、Git操作、ログ確認などを切り替えられます。</p>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>新しいウィンドウを作成</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → c</code></td>
    </tr>
    <tr>
      <td>次のウィンドウへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → n</code></td>
    </tr>
    <tr>
      <td>前のウィンドウへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → p</code></td>
    </tr>
    <tr>
      <td>番号を指定して移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → 0〜9</code></td>
    </tr>
    <tr>
      <td>セッション・ウィンドウ一覧を表示</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → w</code></td>
    </tr>
    <tr>
      <td>現在のウィンドウ名を変更</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → ,</code></td>
    </tr>
  </tbody>
</table>

<p>ウィンドウ名は、<code class="language-plaintext highlighter-rouge">editor</code>、<code class="language-plaintext highlighter-rouge">git</code>、<code class="language-plaintext highlighter-rouge">logs</code>、<code class="language-plaintext highlighter-rouge">docker</code>のように作業内容へ合わせて変更できます。画面下部のステータスバーで目的のウィンドウを見つけやすくなります。</p>

<h2 id="ペインを操作する">ペインを操作する</h2>

<p>1つのウィンドウ内で複数の処理を同時に確認したい場合は、ペインを分割します。</p>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>左右に分割</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → %</code></td>
    </tr>
    <tr>
      <td>上下に分割</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → "</code></td>
    </tr>
    <tr>
      <td>上下左右のペインへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → 矢印キー</code></td>
    </tr>
    <tr>
      <td>ペイン番号を表示</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → q</code></td>
    </tr>
    <tr>
      <td>ペインを最大化／元に戻す</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → z</code></td>
    </tr>
    <tr>
      <td>現在のペインを閉じる</td>
      <td><code class="language-plaintext highlighter-rouge">exit</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">Ctrl + b → z</code>は、ログやエディタを一時的に広く表示したいときに便利です。もう一度同じ操作をすると、元の分割状態へ戻ります。</p>

<h2 id="過去の出力を確認する">過去の出力を確認する</h2>

<p>tmux内で過去の出力を確認するときは、コピーモードへ入ります。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + b → [
</code></pre></div></div>

<p>コピーモードでは、矢印キーや<code class="language-plaintext highlighter-rouge">PageUp</code>、<code class="language-plaintext highlighter-rouge">PageDown</code>を使って表示位置を移動できます。確認を終えて通常の入力へ戻るには、<code class="language-plaintext highlighter-rouge">q</code>を押します。</p>

<p>コピー範囲の選択やOSのクリップボードとの連携は設定によって操作が変わるため、この記事ではスクロールによる確認までを扱います。</p>

<h2 id="tmuxチートシート">tmuxチートシート</h2>

<h3 id="セッション操作">セッション操作</h3>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>基本形</th>
      <th>短縮形</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>tmuxを起動</td>
      <td><code class="language-plaintext highlighter-rouge">tmux new-session</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux</code></td>
    </tr>
    <tr>
      <td>名前付きセッションを作成</td>
      <td><code class="language-plaintext highlighter-rouge">tmux new-session -s セッション名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux new -s セッション名</code></td>
    </tr>
    <tr>
      <td>バックグラウンドにセッションを作成</td>
      <td><code class="language-plaintext highlighter-rouge">tmux new-session -d -s セッション名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux new -d -s セッション名</code></td>
    </tr>
    <tr>
      <td>存在すれば接続、なければ作成</td>
      <td><code class="language-plaintext highlighter-rouge">tmux new-session -A -s セッション名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux new -A -s セッション名</code></td>
    </tr>
    <tr>
      <td>セッション一覧を表示</td>
      <td><code class="language-plaintext highlighter-rouge">tmux list-sessions</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux ls</code></td>
    </tr>
    <tr>
      <td>直近のセッションに接続</td>
      <td><code class="language-plaintext highlighter-rouge">tmux attach-session</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux a</code></td>
    </tr>
    <tr>
      <td>指定したセッションに接続</td>
      <td><code class="language-plaintext highlighter-rouge">tmux attach-session -t セッション名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux a -t セッション名</code></td>
    </tr>
    <tr>
      <td>他の接続を外してセッションに接続</td>
      <td><code class="language-plaintext highlighter-rouge">tmux attach-session -d -t セッション名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux a -d -t セッション名</code></td>
    </tr>
    <tr>
      <td>セッション名を変更</td>
      <td><code class="language-plaintext highlighter-rouge">tmux rename-session -t 旧名 新名</code></td>
      <td><code class="language-plaintext highlighter-rouge">tmux rename -t 旧名 新名</code></td>
    </tr>
    <tr>
      <td>指定したセッションを終了</td>
      <td><code class="language-plaintext highlighter-rouge">tmux kill-session -t セッション名</code></td>
      <td>同左</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">tmux attach</code>は<code class="language-plaintext highlighter-rouge">attach-session</code>の公式エイリアスで、<code class="language-plaintext highlighter-rouge">tmux a</code>は一意な位置まで詰めた省略形です。<code class="language-plaintext highlighter-rouge">new</code>、<code class="language-plaintext highlighter-rouge">ls</code>、<code class="language-plaintext highlighter-rouge">rename</code>はtmuxのマニュアルに記載されている公式エイリアスです。</p>

<h3 id="セッション内の操作">セッション内の操作</h3>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>セッションからデタッチ</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → d</code></td>
    </tr>
    <tr>
      <td>セッション一覧から選択</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → s</code></td>
    </tr>
    <tr>
      <td>現在のセッション名を変更</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → $</code></td>
    </tr>
    <tr>
      <td>キーバインド一覧を表示</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → ?</code></td>
    </tr>
    <tr>
      <td>指定したキーの説明を表示</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → /</code></td>
    </tr>
    <tr>
      <td>tmuxのコマンドプロンプトを開く</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → :</code></td>
    </tr>
  </tbody>
</table>

<h3 id="ウィンドウ操作">ウィンドウ操作</h3>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>新しいウィンドウを作成</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → c</code></td>
    </tr>
    <tr>
      <td>次／前のウィンドウへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → n</code>／<code class="language-plaintext highlighter-rouge">Ctrl + b → p</code></td>
    </tr>
    <tr>
      <td>直前に表示していたウィンドウへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → l</code></td>
    </tr>
    <tr>
      <td>番号を指定して移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → 0〜9</code></td>
    </tr>
    <tr>
      <td>セッション・ウィンドウ一覧から選択</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → w</code></td>
    </tr>
    <tr>
      <td>現在のウィンドウ名を変更</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → ,</code></td>
    </tr>
    <tr>
      <td>現在のウィンドウ番号を変更</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → .</code></td>
    </tr>
    <tr>
      <td>現在のウィンドウを終了</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → &amp;</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">Ctrl + b → &amp;</code>は、ウィンドウ内のすべてのペインと実行中の処理を終了します。通常は各ペインのシェルで<code class="language-plaintext highlighter-rouge">exit</code>を実行し、強制的に閉じたい場合だけ使う方が安全です。</p>

<h3 id="ペイン操作">ペイン操作</h3>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>左右／上下に分割</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → %</code>／<code class="language-plaintext highlighter-rouge">Ctrl + b → "</code></td>
    </tr>
    <tr>
      <td>上下左右のペインへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → 矢印キー</code></td>
    </tr>
    <tr>
      <td>次のペインへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → o</code></td>
    </tr>
    <tr>
      <td>直前に使っていたペインへ移動</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → ;</code></td>
    </tr>
    <tr>
      <td>ペイン番号を表示して選択</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → q → 番号</code></td>
    </tr>
    <tr>
      <td>ペインを最大化／元に戻す</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → z</code></td>
    </tr>
    <tr>
      <td>ペインを1セルずつリサイズ</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → Ctrl + 矢印キー</code></td>
    </tr>
    <tr>
      <td>ペインを5セルずつリサイズ</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → Alt + 矢印キー</code></td>
    </tr>
    <tr>
      <td>ペインを上／下のペインと入れ替え</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → {</code>／<code class="language-plaintext highlighter-rouge">Ctrl + b → }</code></td>
    </tr>
    <tr>
      <td>ペインの配置を切り替え</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → Space</code></td>
    </tr>
    <tr>
      <td>現在のペインを終了</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → x</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">Ctrl + b → x</code>は確認後に現在のペインと実行中の処理を終了します。シェルを正常終了できる場合は、ペイン内で<code class="language-plaintext highlighter-rouge">exit</code>を実行します。</p>

<h3 id="スクロールコピーモード">スクロール・コピーモード</h3>

<table>
  <thead>
    <tr>
      <th>操作</th>
      <th>キー</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>コピーモードに入る</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → [</code></td>
    </tr>
    <tr>
      <td>コピーモードに入り1ページ戻る</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + b → PageUp</code></td>
    </tr>
    <tr>
      <td>1行ずつ移動</td>
      <td><code class="language-plaintext highlighter-rouge">↑</code>／<code class="language-plaintext highlighter-rouge">↓</code></td>
    </tr>
    <tr>
      <td>1ページずつ移動</td>
      <td><code class="language-plaintext highlighter-rouge">PageUp</code>／<code class="language-plaintext highlighter-rouge">PageDown</code></td>
    </tr>
    <tr>
      <td>下方向へインクリメンタル検索</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + s</code></td>
    </tr>
    <tr>
      <td>上方向へインクリメンタル検索</td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + r</code></td>
    </tr>
    <tr>
      <td>次／前の検索結果へ移動</td>
      <td><code class="language-plaintext highlighter-rouge">n</code>／<code class="language-plaintext highlighter-rouge">N</code></td>
    </tr>
    <tr>
      <td>コピーモードを終了</td>
      <td><code class="language-plaintext highlighter-rouge">q</code>または<code class="language-plaintext highlighter-rouge">Esc</code></td>
    </tr>
  </tbody>
</table>

<p>この表はデフォルトのEmacs形式を前提としています。<code class="language-plaintext highlighter-rouge">mode-keys vi</code>を設定している場合は検索や範囲選択のキーが変わります。</p>

<h2 id="ssh作業でよく使う流れ">SSH作業でよく使う流れ</h2>

<p>SSH接続後に、作業用の名前付きセッションを作成します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux new <span class="nt">-A</span> <span class="nt">-s</span> blog
</code></pre></div></div>

<p>必要に応じて、ウィンドウやペインを追加して作業します。作業を中断するときは、セッションを残したままデタッチします。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + b → d
</code></pre></div></div>

<p>次回のSSH接続後にセッションを確認し、同じ作業へ戻ります。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux <span class="nb">ls
</span>tmux a <span class="nt">-t</span> blog
</code></pre></div></div>

<p>まずは「名前付きセッションを作る」「デタッチする」「再接続する」という流れを覚えるだけでも、SSH作業を中断・再開しやすくなります。</p>

<h2 id="まとめ">まとめ</h2>

<p>tmuxを使うと、1つのSSH接続内で複数の作業画面を管理し、接続が切れたあともセッションへ戻れます。特に、時間のかかる処理やログ監視をサーバ上で行うときに役立ちます。</p>

<p>最初に覚えておきたいコマンドは、次の3つです。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux new <span class="nt">-s</span> blog
tmux <span class="nb">ls
</span>tmux a <span class="nt">-t</span> blog
</code></pre></div></div>

<p>tmux内では、デタッチ、ペイン分割、ペイン移動を覚えるところから始めると扱いやすくなります。</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + b → d
Ctrl + b → %
Ctrl + b → "
Ctrl + b → 矢印キー
</code></pre></div></div>

<p>次は<code class="language-plaintext highlighter-rouge">~/.tmux.conf</code>を作成し、マウス操作やステータスバー、キーバインドを自分の作業に合わせて設定してみたいと思います。</p>

<h2 id="参考資料">参考資料</h2>

<ul>
  <li><a href="https://github.com/tmux/tmux/wiki/Getting-Started">tmux公式Wiki：Getting Started</a></li>
  <li><a href="https://github.com/tmux/tmux/wiki/Installing">tmux公式Wiki：Installing</a></li>
</ul>]]></content><author><name>Reo Komatsubara</name></author><category term="Ubuntu" /><category term="tmux" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Nape Proのキーマップ設定方法｜Keychron Launcherを初心者向けに解説</title><link href="https://reotech736.com/2026/06/28/nape-pro-keymap-guide.html" rel="alternate" type="text/html" title="Nape Proのキーマップ設定方法｜Keychron Launcherを初心者向けに解説" /><published>2026-06-28T00:00:00+09:00</published><updated>2026-06-28T00:00:00+09:00</updated><id>https://reotech736.com/2026/06/28/nape-pro-keymap-guide</id><content type="html" xml:base="https://reotech736.com/2026/06/28/nape-pro-keymap-guide.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>Nape Proは、トラックボールの周囲に複数のボタンとダイヤルを備えたデバイスです。初期設定のままでも使えますが、<a href="https://launcher.keychron.com/">Keychron Launcher</a>でボタンの役割を変えると、コピー＆ペーストや<a href="/terms/keymap-layer/">レイヤー</a>切り替え、<a href="/terms/keyboard-macro/">マクロ</a>などを手元に集約できます。</p>

<p>この記事では、<a href="/terms/keymap/">キーマップ</a>を初めて変更する人に向けて、接続から基本的な割り当て、<code class="language-plaintext highlighter-rouge">MO</code>・<code class="language-plaintext highlighter-rouge">TG</code>・<code class="language-plaintext highlighter-rouge">TO</code>を使ったレイヤー、マクロやトラックボールジェスチャーまで順番に説明します。自分で設定をやり直すときの備忘録も兼ねています。</p>

<p>この記事の画面と動作は、2026年6月28日時点の次の環境で確認しています。</p>

<ul>
  <li>Keychron Launcher: V1.3.8</li>
  <li>Nape Pro本体: V1.2.5</li>
  <li>Keychron Link-KM（USBドングル）: V0.1.3</li>
  <li>OS: Windows</li>
</ul>

<p>Launcherや<a href="/terms/firmware/">ファームウェア</a>の更新により、項目名や画面構成が変わる可能性があります。</p>

<h2 id="本体のボタンを確認する">本体のボタンを確認する</h2>

<p>付属説明書では、主な操作部分を次のように表記しています。Launcherの画面でも、デバイス図の各部分をクリックして割り当てを変更します。</p>

<table>
  <thead>
    <tr>
      <th>表記</th>
      <th>初期状態の主な役割</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">M1</code></td>
      <td>左クリック</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">M2</code></td>
      <td>右クリック</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">01</code></td>
      <td>戻る</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">02</code></td>
      <td>DPI切り替え</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">03</code></td>
      <td>ボールスクロール</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">04</code></td>
      <td>8方向の角度切り替え</td>
    </tr>
    <tr>
      <td>トラックボール周囲のダイヤル</td>
      <td>音量の上下</td>
    </tr>
    <tr>
      <td>丸いボタン</td>
      <td>接続先やペアリングの操作</td>
    </tr>
    <tr>
      <td>モード切り替えスイッチ</td>
      <td>有線・2.4GHz・Bluetoothの切り替え</td>
    </tr>
  </tbody>
</table>

<p>本体の向きを変えて使うことを想定しているため、<code class="language-plaintext highlighter-rouge">01〜04</code>の位置は設置角度によって見え方が変わります。本記事では、Launcherと説明書に合わせてボタン上の番号で呼びます。</p>

<h2 id="pcへ接続する">PCへ接続する</h2>

<p>Nape Proには、有線・2.4GHz・Bluetoothの3つの接続モードがあります。最初の設定やファームウェア更新では、通信が途切れにくい有線接続を使用するのが安全です。</p>

<h3 id="有線接続">有線接続</h3>

<ol>
  <li>本体のモード切り替えスイッチを中央にする</li>
  <li>Nape ProとPCをUSB Type-Cケーブルで接続する</li>
  <li>Chromium系ブラウザでKeychron Launcherを開く</li>
  <li>接続ボタンを押し、表示されたNape Proを選択する</li>
</ol>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/launcher-connect.png" alt="Keychron Launcherの接続機器選択画面" /></p>

<p>Chrome、Edge、Operaなど、<a href="/terms/webhid/">WebHID</a>に対応したブラウザを使用します。ブラウザからデバイスへの接続許可を求められたら、Nape Proを選択して接続します。</p>

<h3 id="24ghz接続">2.4GHz接続</h3>

<p>モード切り替えスイッチを<code class="language-plaintext highlighter-rouge">G</code>側へ動かし、USBドングルをPCへ接続します。再ペアリングが必要な場合は、USBドングルを一度外し、丸いボタンと<code class="language-plaintext highlighter-rouge">04</code>を約3秒間長押ししてインジケーターが速く緑色点滅してから、USBドングルを本体から20cm以内で接続します。</p>

<h3 id="bluetooth接続">Bluetooth接続</h3>

<p>モード切り替えスイッチを<code class="language-plaintext highlighter-rouge">BT</code>側へ動かします。丸いボタンと<code class="language-plaintext highlighter-rouge">01</code>・<code class="language-plaintext highlighter-rouge">02</code>・<code class="language-plaintext highlighter-rouge">03</code>のいずれかを組み合わせて接続スロットを選択でき、長押しすると選択したスロットのペアリングを開始します。WindowsのBluetooth設定には<code class="language-plaintext highlighter-rouge">Keychron Nape Pro</code>として表示されます。</p>

<h2 id="最初にファームウェアを確認する">最初にファームウェアを確認する</h2>

<p>到着時点では、本体とUSBドングルのファームウェアが最新とは限りません。今回の個体も本体はV1.1.6だったため、V1.2.5へ更新しました。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/firmware-device-update.png" alt="Nape Pro本体のファームウェア更新中画面" /></p>

<p>更新中はケーブルを抜いたり、ブラウザを再読み込みしたりしないでください。書き込みが完了したら、本体の現在バージョンと最新バージョンが一致していることを確認します。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/firmware-device-current.png" alt="Nape Pro本体が最新バージョンになった画面" /></p>

<p>USBドングルは、本体とは別の「受信機のアップデート」から確認します。今回使用したKeychron Link-KMはV0.1.3が最新でした。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/firmware-receiver-current.png" alt="USBドングルが最新バージョンになった画面" /></p>

<p>記事と同じバージョンへ無理にそろえるのではなく、Launcherに表示される「最新」を基準にしてください。</p>

<h2 id="基本的なキーマップ変更">基本的なキーマップ変更</h2>

<p>キーマップ変更の基本操作は、「変更する物理ボタンを選ぶ」「割り当てたい機能を選ぶ」の2段階です。</p>

<ol>
  <li>左メニューから「カスタマイズ」を開く</li>
  <li>上部のNape Pro図から変更したいボタンやダイヤルをクリックする</li>
  <li>下部のカテゴリから割り当てたい機能を選ぶ</li>
  <li>デバイス図の表示が変わったことを確認する</li>
  <li>実際に操作して期待どおり動くか確認する</li>
</ol>

<p>「基本」にはクリック、進む・戻る、スクロール、英数字、ファンクションキー、修飾キーなどが並んでいます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-basic.png" alt="カスタマイズの基本キー一覧" /></p>

<p>設定はデバイスへ反映されるため、変更前の割り当てをスクリーンショットで残しておくと復旧しやすくなります。画面右側の「リセット」は影響が大きいため、押す前に対象レイヤーと現在の設定を確認してください。</p>

<h2 id="レイヤーとは">レイヤーとは</h2>

<p>レイヤーは、同じ物理ボタンへ別の操作セットを重ねる仕組みです。普段はLayer 0を使い、特定のボタンを押している間だけLayer 1へ移れば、Nape Proのボタン数を実質的に増やせます。</p>

<p>LauncherではLayer 0〜7を編集できます。各レイヤーへ移る方法として、主に<code class="language-plaintext highlighter-rouge">MO</code>・<code class="language-plaintext highlighter-rouge">TG</code>・<code class="language-plaintext highlighter-rouge">TO</code>を使用します。</p>

<table>
  <thead>
    <tr>
      <th>機能</th>
      <th>動作</th>
      <th>向いている用途</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MO(1)</code></td>
      <td>押している間だけLayer 1を有効にする</td>
      <td>一時的なコピー＆ペーストや編集操作</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TG(1)</code></td>
      <td>押すたびにLayer 1の有効・無効を切り替える</td>
      <td>一定時間、別の操作セットを使い続ける</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TO(1)</code></td>
      <td>他の一時レイヤーを解除してLayer 1へ移る</td>
      <td>操作モードを明示的に切り替える</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">MO</code>はキーボードの<code class="language-plaintext highlighter-rouge">Fn</code>キーに近く、初心者にも状態を把握しやすい方法です。<code class="language-plaintext highlighter-rouge">TG</code>や<code class="language-plaintext highlighter-rouge">TO</code>は便利ですが、現在どのレイヤーにいるか分からなくなると操作できなくなるため、戻るためのキーも同時に用意してください。</p>

<p>レイヤーの基本動作は、<a href="/terms/qmk/">QMK Firmware</a>の<a href="https://docs.qmk.fm/feature_layers">Layersドキュメント</a>でも確認できます。</p>

<h2 id="mo1を使った実用レイヤー例">MO(1)を使った実用レイヤー例</h2>

<p>ここでは、Layer 0の<code class="language-plaintext highlighter-rouge">M1</code>を押している間だけLayer 1へ移り、編集操作を呼び出す例を作ります。</p>

<h3 id="layer-0へmo1を割り当てる">Layer 0へMO(1)を割り当てる</h3>

<ol>
  <li>Layer 0を選択する</li>
  <li>デバイス図の<code class="language-plaintext highlighter-rouge">M1</code>をクリックする</li>
  <li>「レイヤー」カテゴリを開く</li>
  <li><code class="language-plaintext highlighter-rouge">MO(1)</code>を選択する</li>
</ol>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/layer-0-mo1.png" alt="Layer 0のM1へMO(1)を割り当てた状態" /></p>

<p>この例では<code class="language-plaintext highlighter-rouge">M1</code>の左クリックを<code class="language-plaintext highlighter-rouge">MO(1)</code>で上書きしています。Nape Pro単体で左クリックを使いたい場合は、<code class="language-plaintext highlighter-rouge">M1</code>ではなく、既定の「戻る」が割り当てられている<code class="language-plaintext highlighter-rouge">01</code>などをレイヤーキーにする方が安全です。</p>

<h3 id="layer-1へ操作を割り当てる">Layer 1へ操作を割り当てる</h3>

<p>今回の設定例は次のとおりです。</p>

<table>
  <thead>
    <tr>
      <th>操作部分</th>
      <th>Layer 1の割り当て</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">03</code></td>
      <td>ボールスクロール</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">04</code></td>
      <td><code class="language-plaintext highlighter-rouge">Shift</code></td>
    </tr>
    <tr>
      <td>ダイヤル上方向</td>
      <td><code class="language-plaintext highlighter-rouge">Backspace</code></td>
    </tr>
    <tr>
      <td>ダイヤル下方向</td>
      <td><code class="language-plaintext highlighter-rouge">Delete</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">01</code></td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl+C</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">02</code></td>
      <td><code class="language-plaintext highlighter-rouge">Ctrl+V</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">M1</code></td>
      <td>透過</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">M2</code></td>
      <td><code class="language-plaintext highlighter-rouge">Esc</code></td>
    </tr>
  </tbody>
</table>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/layer-1-example.png" alt="Layer 1へ編集操作を設定した完成状態" /></p>

<p>Layer 1側の<code class="language-plaintext highlighter-rouge">M1</code>には、下向き三角で表示される透過キーを設定しています。これによりLayer 1でもLayer 0の<code class="language-plaintext highlighter-rouge">MO(1)</code>が引き継がれ、<code class="language-plaintext highlighter-rouge">M1</code>を離したときにLayer 0へ戻れます。</p>

<p>設定後は、メモ帳など失敗しても影響の少ないアプリで確認します。いきなりファイル操作や本番作業に使わず、コピー、貼り付け、削除、レイヤーからの復帰を一つずつ試してください。</p>

<h2 id="カスタマイズ項目の意味">カスタマイズ項目の意味</h2>

<h3 id="基本">基本</h3>

<p>クリック、スクロール、文字、ファンクションキー、修飾キーなど、単一の標準操作を割り当てます。まずは「戻る」を<code class="language-plaintext highlighter-rouge">Esc</code>へ変えるなど、元に戻しやすい変更から試すのがおすすめです。</p>

<h3 id="octashift">OctaShift</h3>

<p>OctaShiftは8方向ジェスチャーではなく、Nape Proを置く角度に合わせてトラックボールの移動方向を補正する機能です。<code class="language-plaintext highlighter-rouge">0°</code>・<code class="language-plaintext highlighter-rouge">45°</code>・<code class="language-plaintext highlighter-rouge">90°</code>・<code class="language-plaintext highlighter-rouge">135°</code>・<code class="language-plaintext highlighter-rouge">180°</code>・<code class="language-plaintext highlighter-rouge">225°</code>・<code class="language-plaintext highlighter-rouge">270°</code>・<code class="language-plaintext highlighter-rouge">315°</code>から直接指定する機能と、「8方向を切り替え」をボタンへ割り当てられます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-octashift.png" alt="OctaShiftの割り当て一覧" /></p>

<p>付属説明書では、角度ごとのインジケーター色を次のように案内しています。</p>

<table>
  <thead>
    <tr>
      <th>角度</th>
      <th>色</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0°</td>
      <td>白</td>
    </tr>
    <tr>
      <td>45°</td>
      <td>緑</td>
    </tr>
    <tr>
      <td>90°</td>
      <td>青</td>
    </tr>
    <tr>
      <td>135°</td>
      <td>黄</td>
    </tr>
    <tr>
      <td>180°</td>
      <td>赤</td>
    </tr>
    <tr>
      <td>225°</td>
      <td>紫</td>
    </tr>
    <tr>
      <td>270°</td>
      <td>シアン</td>
    </tr>
    <tr>
      <td>315°</td>
      <td>オレンジ</td>
    </tr>
  </tbody>
</table>

<p>トラックボールを動かした方向とカーソルの方向が一致する角度を選びます。キーボードの手前、左右、斜めなど、設置場所を変えたときに調整してください。</p>

<h3 id="ショートカット">ショートカット</h3>

<p><code class="language-plaintext highlighter-rouge">Ctrl</code>・<code class="language-plaintext highlighter-rouge">Shift</code>・<code class="language-plaintext highlighter-rouge">Alt/Option</code>・<code class="language-plaintext highlighter-rouge">Win/Command</code>と通常キーを組み合わせ、1回の操作として割り当てます。Windowsなら<code class="language-plaintext highlighter-rouge">Ctrl+C</code>、<code class="language-plaintext highlighter-rouge">Ctrl+V</code>、<code class="language-plaintext highlighter-rouge">Win+Shift+S</code>など、同時押しするだけで完結する操作に向いています。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-shortcut.png" alt="ショートカット作成画面" /></p>

<p>単純なキーの同時押しは、後述するマクロではなくショートカットを使う方が設定内容を読み取りやすくなります。</p>

<h3 id="応用タップホールド">応用（タップ／ホールド）</h3>

<p>短く押したときと、長く押したときで別の機能を実行します。例えば、短く押すと<code class="language-plaintext highlighter-rouge">Esc</code>、長押しすると<code class="language-plaintext highlighter-rouge">Ctrl</code>というように、使用頻度の高い2操作を1つのボタンへまとめられます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-tap-hold.png" alt="タップとホールドの設定画面" /></p>

<p>押し方の判定が加わるため、クリックのように即時性が必要な操作には向きません。最初は<code class="language-plaintext highlighter-rouge">Esc</code>や修飾キーなど、多少の判定時間があっても困りにくい操作で試してください。</p>

<h3 id="同時押し">同時押し</h3>

<p><code class="language-plaintext highlighter-rouge">01〜04</code>・<code class="language-plaintext highlighter-rouge">M1</code>・<code class="language-plaintext highlighter-rouge">M2</code>から複数のボタンを組み合わせ、その組み合わせを押したときだけ別の操作を実行できます。Launcherの画面では最大30件の同時押し設定を登録でき、タップ時とホールド時の出力を分けられます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-combo.png" alt="同時押しの設定画面" /></p>

<p>通常操作と同時押し判定が競合しやすいため、頻繁に単独で使うボタン同士は避けます。登録後は、単独押しと同時押しの両方が意図どおり動くことを確認してください。</p>

<h3 id="トラックボール">トラックボール</h3>

<p>トラックボールカテゴリでは、<a href="/terms/dpi/">DPI</a>・<a href="/terms/polling-rate/">ポーリングレート</a>の切り替えや、トラックボールジェスチャーを割り当てられます。ジェスチャーでは上・下・左・右へボールを動かしたときのキーを指定できます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/customize-trackball.png" alt="トラックボールジェスチャーの設定画面" /></p>

<p>ジェスチャーモード中は通常のカーソル移動として扱われないため、ジェスチャーを有効にするボタンと解除方法を先に確認しておきます。</p>

<h2 id="マクロで複数操作をまとめる">マクロで複数操作をまとめる</h2>

<p>ショートカットが複数キーの同時押しをまとめる機能なのに対し、マクロはキーを押す・離す順序を記録して再生する機能です。文字入力後にカーソルを移動するなど、順番のある操作に向いています。</p>

<p>今回の例では、<code class="language-plaintext highlighter-rouge">()</code>を入力してから左矢印を押し、カーソルを括弧の中へ移動するマクロを作成しました。</p>

<video controls="" preload="metadata" style="display: block; width: 100%; height: auto;">
  <source src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/macro-recording.mp4" type="video/mp4" />
  <a href="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/macro-recording.mp4">マクロのレコーディング例を再生する</a>
</video>

<p>レコーディング機能を開始し、<code class="language-plaintext highlighter-rouge">Shift+8</code>、<code class="language-plaintext highlighter-rouge">Shift+9</code>、左矢印の順に操作すると、押下と解放がコードとして保存されます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/macro-code.png" alt="括弧を入力してカーソルを戻すマクロのコード" /></p>

<p>この例のコードは、次の順番を表しています。</p>

<ol>
  <li>左<code class="language-plaintext highlighter-rouge">Shift</code>を押す</li>
  <li><code class="language-plaintext highlighter-rouge">8</code>を押して離す</li>
  <li><code class="language-plaintext highlighter-rouge">9</code>を押して離す</li>
  <li>左<code class="language-plaintext highlighter-rouge">Shift</code>を離す</li>
  <li>左矢印を押して離す</li>
</ol>

<p>このキー操作で<code class="language-plaintext highlighter-rouge">()</code>になるのは、日本語JIS配列として認識されているWindows環境です。US配列では括弧のキー位置が異なるため、実際のOS配列に合わせてレコーディングしてください。</p>

<p>マクロを作成しただけでは、物理ボタンからは実行できません。「カスタマイズ」の「マクロ」カテゴリを開き、作成した<code class="language-plaintext highlighter-rouge">M0</code>を実行したいボタンへ割り当てます。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/macro-assignment.png" alt="作成したM0マクロを04へ割り当てた例" /></p>

<p>マクロはアクティブなウィンドウへそのままキー入力を送ります。削除、送信、終了などを含むマクロは誤操作の影響が大きいため、まずメモ帳で確認してください。</p>

<h2 id="dpiとポーリングレート">DPIとポーリングレート</h2>

<p>DPIはトラックボールを同じ距離だけ動かしたときのカーソル移動量です。値を上げるほど少ないボール操作でカーソルが大きく動き、値を下げるほど細かく狙いやすくなります。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/dpi-settings.png" alt="DPIとポーリングレートの設定画面" /></p>

<p>初期プリセットとインジケーター色は次のとおりです。</p>

<table>
  <thead>
    <tr>
      <th>DPI</th>
      <th>色</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>400</td>
      <td>白</td>
    </tr>
    <tr>
      <td>800</td>
      <td>緑</td>
    </tr>
    <tr>
      <td>1600</td>
      <td>青</td>
    </tr>
    <tr>
      <td>3200</td>
      <td>黄</td>
    </tr>
    <tr>
      <td>4000</td>
      <td>赤</td>
    </tr>
  </tbody>
</table>

<p>最初は1600 DPIを基準にし、カーソルが飛びすぎるなら800、移動量が足りないなら3200へ変えると調整しやすくなります。LauncherではカスタムDPIも指定できます。</p>

<p>ポーリングレートは、PCへ位置情報を送る頻度です。有線・2.4GHzでは125Hz・500Hz・1000Hzを選択でき、Bluetoothでは125Hzに固定されます。高い値は滑らかさを期待できますが消費電力も増えるため、用途とバッテリー持続時間のバランスで選びます。</p>

<h2 id="アドバンスモード">アドバンスモード</h2>

<p>アドバンスモードでは、キー割り当て以外の動作を変更します。</p>

<p><img src="/assets/images/posts/2026-06-28-nape-pro-keymap-guide/advanced-mode.png" alt="アドバンスモードの設定画面" /></p>

<ul>
  <li>オートスリープモード開始時間: 無操作からスリープへ入るまでの時間</li>
  <li>常にジェスチャーモードを有効にする: トラックボールを常時ジェスチャー入力として使い、カーソル移動を無効化</li>
  <li>常にスクロールモードを有効にする: トラックボールを常時スクロールとして使い、カーソル移動を無効化</li>
  <li>レイヤー編集モード: Launcherで別レイヤーを編集中でも、Nape Pro本体の使用レイヤーを連動させない</li>
</ul>

<p>常時ジェスチャーと常時スクロールは同時に選択できず、どちらも通常のカーソル操作ができなくなります。Nape Proを通常のポインティングデバイスとして使う場合はオフのままにします。</p>

<p>付属説明書では、2.4GHz・Bluetooth接続時に約10分間操作がないとオートスリープへ入り、ボタンやトラックボール操作で復帰すると案内されています。有線接続ではオートスリープは動作しません。</p>

<h2 id="困ったときの確認事項">困ったときの確認事項</h2>

<h3 id="ボタンを押しても想定した操作にならない">ボタンを押しても想定した操作にならない</h3>

<p>まず、Launcherで選択しているレイヤーを確認します。<code class="language-plaintext highlighter-rouge">TG</code>や<code class="language-plaintext highlighter-rouge">TO</code>を使用している場合は別レイヤーに残っている可能性があるため、Layer 0へ戻るキーが機能するか確認してください。</p>

<h3 id="moを離しても元のレイヤーへ戻れない">MOを離しても元のレイヤーへ戻れない</h3>

<p>移動先レイヤーで、<code class="language-plaintext highlighter-rouge">MO</code>を割り当てた物理ボタンが別機能に上書きされていないか確認します。今回の例のように透過キーを置くと、下のレイヤーにある<code class="language-plaintext highlighter-rouge">MO</code>の動作を引き継げます。</p>

<h3 id="接続が不安定">接続が不安定</h3>

<p>有線で認識するか確認した後、2.4GHzまたはBluetoothを再ペアリングします。2.4GHzではUSBドングルとNape Proの距離を近づけ、必要に応じて付属の延長アダプターを使います。</p>

<h3 id="設定を工場出荷時へ戻したい">設定を工場出荷時へ戻したい</h3>

<p>付属説明書では、<code class="language-plaintext highlighter-rouge">01</code>・<code class="language-plaintext highlighter-rouge">02</code>・<code class="language-plaintext highlighter-rouge">03</code>・<code class="language-plaintext highlighter-rouge">04</code>を同時に約4秒間長押しすると工場出荷時設定へ戻ると案内されています。インジケーターが3回点滅すると完了です。</p>

<p>この操作はキーマップを失う可能性があるため、接続やレイヤーだけが原因でないことを確認してから実行してください。</p>

<h2 id="まとめ">まとめ</h2>

<p>Nape Proの設定は、最初からすべてを変更するより、単一キー、レイヤー、ショートカット、マクロの順に試すと理解しやすくなります。特に<code class="language-plaintext highlighter-rouge">MO</code>は、押している間だけ別レイヤーを使うため、初めてでも現在の状態を把握しやすい機能です。</p>

<p>一方、<code class="language-plaintext highlighter-rouge">TG</code>・<code class="language-plaintext highlighter-rouge">TO</code>・同時押し・タップ／ホールド・常時ジェスチャーは、設定次第で通常操作へ戻りにくくなります。変更前のスクリーンショットを残し、メモ帳などで一つずつ動作確認しながら、自分の操作に合うキーマップへ調整していくのが安全です。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Windows" /><category term="Keychron" /><category term="Nape Pro" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Obsidian &amp;amp; Discord-bot で個人用ナレッジベースを構築する</title><link href="https://reotech736.com/2026/05/19/knowledge-base-pipeline.html" rel="alternate" type="text/html" title="Obsidian &amp;amp; Discord-bot で個人用ナレッジベースを構築する" /><published>2026-05-19T00:00:00+09:00</published><updated>2026-05-19T00:00:00+09:00</updated><id>https://reotech736.com/2026/05/19/knowledge-base-pipeline</id><content type="html" xml:base="https://reotech736.com/2026/05/19/knowledge-base-pipeline.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>このブログに、個人用のナレッジベースとして <a href="/terms/">技術メモ</a> と <a href="/study-logs/">学習記録</a> を追加しました。</p>

<p>技術メモは用語や概念を後から引くための辞書で、学習記録は日々の作業等を時系列で残すログです。<br />
やりたかったことは、個人開発や業務で得た知識を「あとで記事にするかもしれないメモ」として溜めることです。ただし、ブログのリポジトリを直接編集する運用にすると、気軽さがなくなります。</p>

<p>そこで、<a href="/terms/obsidian/">Obsidian</a> を母艦にした <code class="language-plaintext highlighter-rouge">knowledge-vault-work</code> を作り、そこからブログ用 <a href="/terms/markdown/">Markdown</a> を生成するパイプラインを組みました。さらに、思いついた瞬間に Discord からも追加できるように、専用の Discord Bot も作っています。</p>

<h2 id="作ったもの">作ったもの</h2>

<p>今回追加した入口は次の 2 つです。</p>

<ul>
  <li><a href="/terms/">技術メモ</a>: AWS、Docker、Shell などの用語を整理する辞書</li>
  <li><a href="/study-logs/">学習記録</a>: 日々の学びや作業ログを残す場所</li>
</ul>

<p>元データはブログリポジトリではなく、Obsidian で扱う <code class="language-plaintext highlighter-rouge">knowledge-vault-work</code> に置いています。<br />
ブログ側の <code class="language-plaintext highlighter-rouge">_terms/</code> と <code class="language-plaintext highlighter-rouge">_study_logs/</code> は生成物として扱い、source of truth は vault 側に寄せています。</p>

<h2 id="全体構成">全体構成</h2>

<p>全体像は次のような構成です。</p>

<pre><code class="language-mermaid">flowchart LR
  Obsidian[Obsidian] --&gt; Work[knowledge-vault-work]
  Discord[Discord] --&gt; Bot[discord-knowledge-vault-bot]
  Bot --&gt; Work
  Work --&gt; Bare[bare/knowledge-vault.git]
  Bare --&gt; Hook[post-receive hook]
  Hook --&gt; Publish[publish-knowledge-vault.sh]
  Publish --&gt; Blog[Reotech736.github.io]
  Blog --&gt; Pages[GitHub Pages]
</code></pre>

<p>ポイントは、ブログのためだけに Obsidian のノートを直接公開しないことです。<br />
vault 側では自分が書きやすい形を保ち、公開対象だけを import script で <a href="/terms/jekyll/">Jekyll</a> 用に変換しています。</p>

<h2 id="bare-リポジトリを使う理由">bare リポジトリを使う理由</h2>

<p>この構成では <code class="language-plaintext highlighter-rouge">bare/knowledge-vault.git</code> という <a href="/terms/bare-repository/">bareリポジトリ</a>を挟んでいます。bareリポジトリは、作業ツリーを持たない Git リポジトリです。普通のリポジトリのようにファイルを編集する場所ではなく、push を受け取るための受け口として使います。</p>

<p>今回の使い方では、<code class="language-plaintext highlighter-rouge">knowledge-vault-work</code> から bare リポジトリへ push すると、<a href="/terms/post-receive-hook/">post-receiveフック</a>が動きます。この hook からブログ側の publish script を起動して、公開用 Markdown の生成までつなげています。</p>

<h2 id="公開パイプライン">公開パイプライン</h2>

<p>公開までの流れは次の通りです。</p>

<pre><code class="language-mermaid">sequenceDiagram
  participant V as knowledge-vault-work
  participant B as bare/knowledge-vault.git
  participant H as post-receive hook
  participant P as publish script
  participant R as Blog repo
  participant G as GitHub Pages

  V-&gt;&gt;B: git push origin main
  B-&gt;&gt;H: refs/heads/main を受信
  H-&gt;&gt;P: publish-knowledge-vault.sh を実行
  P-&gt;&gt;V: pull --rebase
  P-&gt;&gt;R: reset --hard origin/main
  P-&gt;&gt;R: import-study-logs.rb / import-terms.rb
  P-&gt;&gt;R: JEKYLL_ENV=production bundle exec jekyll build
  P-&gt;&gt;R: _study_logs / _terms に差分があれば commit &amp; push
  R-&gt;&gt;G: main push を契機に Pages deploy
</code></pre>

<p>ブログ側では <code class="language-plaintext highlighter-rouge">scripts/import-study-logs.rb</code> と <code class="language-plaintext highlighter-rouge">scripts/import-terms.rb</code> が、vault の Markdown を Jekyll の collection に変換します。変換対象は <code class="language-plaintext highlighter-rouge">publish: true</code> のノートだけです。<br />
これにより、Obsidian 側には下書きや個人的なメモを残しつつ、公開するものだけをブログに流せます。</p>

<p>また、Obsidian の <code class="language-plaintext highlighter-rouge">[[EC2]]</code> のような wiki link は、公開済みの技術メモであれば <code class="language-plaintext highlighter-rouge">/terms/ec2/</code> のようなリンクに変換します。まだ公開していない用語は、リンクではなく「準備中の用語」として表示します。</p>

<h2 id="discord-bot-から追加できるようにした">Discord Bot から追加できるようにした</h2>

<p>Obsidian は腰を据えて整理するには便利ですが、スマホから一瞬で追加するには少し重いです。そこで、Discord から技術メモと学習記録を追加できる <code class="language-plaintext highlighter-rouge">discord-knowledge-vault-bot</code> を作りました。<br />
Bot には次の 2 つの<a href="/terms/slash-command/">スラッシュコマンド</a>を用意しています。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/term_add</code>: <code class="language-plaintext highlighter-rouge">01_terms/&lt;slug&gt;.md</code> に技術メモを作成</li>
  <li><code class="language-plaintext highlighter-rouge">/study_log_add</code>: <code class="language-plaintext highlighter-rouge">03_study_logs/YYYY-MM-DD-study-log.md</code> に学習記録を作成</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">/term_add</code> の modal は次のような形です。</p>

<p><img src="/assets/images/posts/2026-05-19-knowledge-base-pipeline/discord-modal.png" alt="Discord modal for adding term" /></p>

<p>Discord の modal は Text Input を最大 5 つまでしか置けません。<br />
そのため、技術メモでは入力欄を次の 5 つに絞りました。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">title:slug</code></li>
  <li><code class="language-plaintext highlighter-rouge">category</code></li>
  <li><code class="language-plaintext highlighter-rouge">一言でいうと</code></li>
  <li><code class="language-plaintext highlighter-rouge">より具体的には</code></li>
  <li><code class="language-plaintext highlighter-rouge">関連</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">title</code> と <code class="language-plaintext highlighter-rouge">slug</code> を別入力にすると、それだけで入力枠を 2 つ使ってしまいます。<br />
そこで <code class="language-plaintext highlighter-rouge">セキュリティグループ:security-group</code> のように 1 つの欄にまとめ、残りを本文や関連用語に使えるようにしました。</p>

<h2 id="bot-側の運用">Bot 側の運用</h2>

<p>Bot は Docker Compose で常駐させています。<code class="language-plaintext highlighter-rouge">knowledge-vault-work</code> と bare リポジトリを volume mount し、Bot から vault に Markdown を作成して commit / push します。<br />
運用上のポイントは次の 3 つです。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">DISCORD_ALLOWED_USER_IDS</code> で実行できるユーザーを制限する</li>
  <li>Git 操作前に vault の worktree が dirty なら停止する</li>
  <li><code class="language-plaintext highlighter-rouge">gitBusy</code> で同時に複数の Git 操作が走らないようにする</li>
</ul>

<p>Discord から作成したノートにも <code class="language-plaintext highlighter-rouge">publish: true</code> を入れるので、push 後は既存の publish pipeline がそのまま動きます。<br />
Bot は「ノートを作って push するところ」までを担当し、ブログへの変換は既存パイプラインに任せています。</p>

<h2 id="実装してよかったこと">実装してよかったこと</h2>

<p>一番よかったのは、知識を残す心理的なハードルが下がったことです。PC の前にいるときは Obsidian で整理し、外出中や作業中に思いついたことは Discord から追加できます。<br />
どちらから入れても最終的には同じ <code class="language-plaintext highlighter-rouge">knowledge-vault-work</code> に集まり、公開対象だけがブログに流れます。</p>

<p>また、Terms と Study Logs を分けたことで、用語として整理したいものと、日々の作業ログとして残したいものを分けられるようになりました。</p>

<h2 id="今後やりたいこと">今後やりたいこと</h2>

<p>今後は、カテゴリや未分類の整理をもう少し進めたいです。<br />
特に技術メモは数が増えるほど探しやすさが重要になるので、カテゴリ設計と検索体験を育てていきたいところです。</p>

<p>Discord 側も、modal の 5 入力制限の中でどこまで気持ちよく書けるかはまだ改善余地があります。<br />
まずはこの形で運用しながら、入力項目や追記フローを調整していきます。</p>

<h2 id="まとめ">まとめ</h2>

<p>今回の機能追加で、ブログは単なる記事置き場ではなく、日々の学びを溜めていくナレッジベースとして使えるようになりました。<br />
Obsidian、bare リポジトリ、publish pipeline、Discord Bot を組み合わせることで、書く場所と公開する場所を分離しつつ、必要なものだけをブログに出せる構成になっています。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="ブログ" /><category term="Discord" /><category term="Docker" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Minecraft serverをDockerで安全運用する</title><link href="https://reotech736.com/2026/03/02/minecraft-server-setup.html" rel="alternate" type="text/html" title="Minecraft serverをDockerで安全運用する" /><published>2026-03-02T00:00:00+09:00</published><updated>2026-03-02T00:00:00+09:00</updated><id>https://reotech736.com/2026/03/02/minecraft-server-setup</id><content type="html" xml:base="https://reotech736.com/2026/03/02/minecraft-server-setup.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p><a href="/2026/01/21/docker-dev-setup.html">前回の記事「Docker環境の構築」</a>で Docker を導入したので、今回はその上で <strong>Minecraft Java サーバを2ワールド構成で運用</strong>できるようにしました。さらに、友人向けに SSH 操作を開放しつつ、サーバ全体に触れられないように <strong>操作範囲を強制的に限定</strong>しています。<br />
今回のゴールは次の3つです。</p>

<ul>
  <li>サバイバル/クリエイティブを <a href="/terms/docker-compose/">Docker Compose</a> で分離運用する</li>
  <li>起動・停止・ログ管理をスクリプト化して保守しやすくする</li>
  <li>友人用ユーザーに<a href="/terms/least-privilege/">最小権限</a>だけを渡し、誤操作や悪用時の被害を抑える</li>
</ul>

<p>マインクラフトのサーバ構成は、<a href="https://github.com/Reotech736/minecraft-server">minecraft-server（GitHub）</a> で管理しています。</p>

<h2 id="運用構成">運用構成</h2>

<ul>
  <li>Ubuntu Server 上で Docker Compose を使用</li>
  <li><code class="language-plaintext highlighter-rouge">mc-survival</code>（<code class="language-plaintext highlighter-rouge">25565</code>）と <code class="language-plaintext highlighter-rouge">mc-creative</code>（<code class="language-plaintext highlighter-rouge">25566</code>）を別コンテナで運用</li>
  <li>ワールドデータは<a href="/terms/docker-volume/">Docker Composeのボリューム設定</a>で <code class="language-plaintext highlighter-rouge">data-survival</code> / <code class="language-plaintext highlighter-rouge">data-creative</code> に分離して永続化</li>
  <li>起動時にログ整理を自動判定するラッパースクリプトを利用</li>
  <li>友人用ユーザー <code class="language-plaintext highlighter-rouge">guest-minecraft</code> は <a href="/terms/tailscale/">Tailscale</a> 経由 SSH + 許可コマンドのみ実行</li>
</ul>

<p><img src="/assets/images/posts/2026-03-02-minecraft-server-setup/minecraft-server-architecture.svg" alt="minecraft server architecture" /></p>

<h2 id="docker-compose-で2ワールドを分離">Docker Compose で2ワールドを分離</h2>

<p><code class="language-plaintext highlighter-rouge">docker-compose.yml</code> では、ポート・メモリ・ワールド保存先を分けています。<br />
<code class="language-plaintext highlighter-rouge">restart: "no"</code> にしているのは、「遊ぶときだけ起動」の運用に合わせるためです。</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">mc-survival</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">itzg/minecraft-server:java21</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">mc-survival</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">25565:25565"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MEMORY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4G"</span>
      <span class="na">ENABLE_WHITELIST</span><span class="pi">:</span> <span class="s2">"</span><span class="s">TRUE"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./data-survival:/data</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s2">"</span><span class="s">no"</span>

  <span class="na">mc-creative</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">itzg/minecraft-server:java21</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">mc-creative</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">25566:25565"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MEMORY</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2G"</span>
      <span class="na">MODE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">creative"</span>
      <span class="na">FORCE_GAMEMODE</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./data-creative:/data</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s2">"</span><span class="s">no"</span>
</code></pre></div></div>

<h2 id="日常運用はスクリプト化">日常運用はスクリプト化</h2>

<p>起動・停止は次のスクリプトで統一しています。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./scripts/start-minecraft.sh mc-survival
./scripts/start-minecraft.sh mc-creative
./scripts/stop-minecraft.sh mc-survival
./scripts/stop-minecraft.sh mc-creative
./scripts/stop-minecraft.sh
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">start-minecraft.sh</code> は、前回ログ整理から24時間以上経っていたら <code class="language-plaintext highlighter-rouge">cleanup-minecraft-logs.sh</code> を実行し、30日超の圧縮ログ（<code class="language-plaintext highlighter-rouge">*.gz</code>）を削除します。<br />
加えて、コンテナログ側も <code class="language-plaintext highlighter-rouge">max-size: 10m</code> / <code class="language-plaintext highlighter-rouge">max-file: 5</code> で<a href="/terms/log-rotation/">ログローテーション</a>しており、長期運用でディスクが詰まりにくい構成にしています。</p>

<h2 id="友人向け-ssh-は実行できる操作を固定">友人向け SSH は「実行できる操作」を固定</h2>

<p>「SSH を渡す = 何でもできる」状態にしないため、入口を固定しました。</p>

<ol>
  <li>友人用ユーザー <code class="language-plaintext highlighter-rouge">guest-minecraft</code> を作成</li>
  <li>パスワードログインを無効化し、<a href="/terms/ssh-public-key-authentication/">SSH公開鍵認証</a>のみ許可</li>
  <li><a href="/terms/authorized-keys/"><code class="language-plaintext highlighter-rouge">authorized_keys</code></a> で <code class="language-plaintext highlighter-rouge">command="/usr/local/bin/mc-guest-entrypoint"</code> を強制</li>
  <li><code class="language-plaintext highlighter-rouge">mc-guest-entrypoint</code> で許可コマンドをホワイトリスト化</li>
  <li><a href="/terms/sudoers/"><code class="language-plaintext highlighter-rouge">sudoers</code></a> で <code class="language-plaintext highlighter-rouge">/usr/local/bin/mc-ctl</code> の限定サブコマンドだけ許可</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">authorized_keys</code> 例:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restrict,command="/usr/local/bin/mc-guest-entrypoint" ssh-ed25519 &lt;public-key&gt;
</code></pre></div></div>

<p>この1行は、次の意味です。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ssh-ed25519 &lt;public-key&gt;</code>: この公開鍵で認証できる</li>
  <li><code class="language-plaintext highlighter-rouge">command="/usr/local/bin/mc-guest-entrypoint"</code>: 鍵認証に成功したら、ユーザーが <code class="language-plaintext highlighter-rouge">ssh ... "任意コマンド"</code> を指定しても無視し、常にこのスクリプトを実行する</li>
  <li><code class="language-plaintext highlighter-rouge">restrict</code>: <code class="language-plaintext highlighter-rouge">authorized_keys</code> 側の包括制限で、ポートフォワーディング（<code class="language-plaintext highlighter-rouge">-L/-R/-D</code>）、agent forward、X11 forward、PTY割り当て、<code class="language-plaintext highlighter-rouge">~/.ssh/rc</code> 実行を無効化する</li>
</ul>

<p>つまり、接続時の実行フローは次のようになります。</p>

<ol>
  <li>公開鍵認証が通る</li>
  <li><code class="language-plaintext highlighter-rouge">mc-guest-entrypoint</code> が強制実行される</li>
  <li>元のコマンドは <code class="language-plaintext highlighter-rouge">SSH_ORIGINAL_COMMAND</code> としてスクリプト側で受け取り、ホワイトリスト判定</li>
  <li>許可コマンドのみ <code class="language-plaintext highlighter-rouge">sudo -n /usr/local/bin/mc-ctl ...</code> を実行</li>
  <li>不許可コマンドは即時拒否</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">command=...</code> だけだと SSH 側のフォワーディング機能が残るため、今回のように <code class="language-plaintext highlighter-rouge">restrict</code> を併用して入口を狭める構成にしています。<br />
許可しているのは以下のみです。</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">start survival</code></li>
  <li><code class="language-plaintext highlighter-rouge">start creative</code></li>
  <li><code class="language-plaintext highlighter-rouge">stop survival</code></li>
  <li><code class="language-plaintext highlighter-rouge">stop creative</code></li>
  <li><code class="language-plaintext highlighter-rouge">stop all</code></li>
  <li><code class="language-plaintext highlighter-rouge">status</code></li>
</ul>

<p>それ以外はサーバ側で拒否されます。<br />
また、<code class="language-plaintext highlighter-rouge">guest-minecraft</code> を <code class="language-plaintext highlighter-rouge">docker</code> グループには入れていません（<code class="language-plaintext highlighter-rouge">docker</code> グループは実質 root 相当のため）。</p>

<h2 id="再現性を上げるための配置方針">再現性を上げるための配置方針</h2>

<p><code class="language-plaintext highlighter-rouge">/usr/local/bin</code> や <code class="language-plaintext highlighter-rouge">/etc/sudoers.d</code> を直接手編集すると、差分管理が難しくなります。<br />
そこで、正本はリポジトリ配下の <code class="language-plaintext highlighter-rouge">scripts/admin/</code> に置き、インストーラで反映する方式にしました。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> ./scripts/admin/install-guest-minecraft.sh
</code></pre></div></div>

<p>主な管理対象:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">scripts/admin/mc-ctl</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/admin/mc-guest-entrypoint</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/admin/guest-minecraft-mc.sudoers</code></li>
  <li><code class="language-plaintext highlighter-rouge">scripts/admin/install-guest-minecraft.sh</code></li>
</ul>

<h2 id="友人側の操作導線windows">友人側の操作導線（Windows）</h2>

<p>友人向けには、<code class="language-plaintext highlighter-rouge">workspace/Guide_for_Friends/</code> に <code class="language-plaintext highlighter-rouge">.bat</code> と PowerShell UI を用意しました。<br />
中身は最終的に <code class="language-plaintext highlighter-rouge">ssh guest-minecraft@&lt;tailscale-ip&gt; "&lt;allowed-command&gt;"</code> を叩くだけなので、実行範囲はサーバ側ポリシーで担保されます。</p>

<h2 id="悪意ある相手でも安全かへの整理">「悪意ある相手でも安全か？」への整理</h2>

<p>この構成は、<strong>被害を小さくする設計</strong>としては有効です。<br />
ただし「絶対安全」ではありません 例えば次のリスクは残ります。</p>

<ul>
  <li>鍵の漏えい（正規ユーザーとして接続される）</li>
  <li>許可スクリプト自体のバグ</li>
  <li>ホスト OS / Docker / SSH の脆弱性</li>
</ul>

<p>運用では、鍵ローテーション・OS更新・ログ監視を継続し、必要に応じて接続元制限（<a href="/terms/ufw/">UFW</a> + Tailscale <a href="/terms/access-control-list/">ACL</a>）を強化するのが現実的です。</p>

<h2 id="まとめ">まとめ</h2>

<p>今回の構成で、Minecraft サーバ運用は次の状態になりました。</p>

<ul>
  <li>2ワールドを明確に分離し、運用コマンドを統一</li>
  <li>ログ肥大化を自動で抑制</li>
  <li>友人には「必要な操作だけ」委譲し、ホスト全体の露出を最小化</li>
</ul>

<p>「動けばOK」で終わらせず、運用・保守・権限設計まで含めて形にできたのが一番の収穫でした。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Ubuntu" /><category term="Docker" /><category term="Minecraft" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Discord Bot を Docker で常駐運用する</title><link href="https://reotech736.com/2026/01/26/discord-chatgpt-bot-docker.html" rel="alternate" type="text/html" title="Discord Bot を Docker で常駐運用する" /><published>2026-01-26T00:00:00+09:00</published><updated>2026-01-26T00:00:00+09:00</updated><id>https://reotech736.com/2026/01/26/discord-chatgpt-bot-docker</id><content type="html" xml:base="https://reotech736.com/2026/01/26/discord-chatgpt-bot-docker.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p><a href="/2026/01/21/docker-dev-setup.html">前回の記事「Docker環境の構築」</a>で Docker 環境までは構築できたので、今回は <strong>実際のアプリをコンテナ化</strong>します。題材は「Discord 上で動く ChatGPT Bot（Node.js）」です（厳密には “Discord サーバ” ではなく “Discord Bot” のコンテナ化です）。<br />
この記事のゴールは次の 3 つです。</p>

<ul>
  <li>いままでターミナルで直接起動していた Bot を Docker コンテナとして常駐させる</li>
  <li>秘密情報（トークン）や設定ファイルを、イメージと分離して運用できるようにする</li>
  <li>運用・更新（ログ確認、再起動、自動起動）を Docker コマンドで揃える</li>
</ul>

<h2 id="なぜコンテナ化するのか">なぜコンテナ化するのか</h2>

<p>今までの「サーバに入ってターミナルで <code class="language-plaintext highlighter-rouge">npm start</code>」運用は、次のような “事故りポイント” がありました。</p>

<ul>
  <li>端末（RDP/SSH）を閉じると止まりがちで常駐できない</li>
  <li>Node.js のバージョン差分や依存関係で、別環境に移したとき再現しにくい</li>
</ul>

<p>Docker 化すると、起動と管理が <strong>「コンテナ」</strong> という単位に揃うので、運用がかなり楽になります。</p>

<ul>
  <li>起動・停止・ログ確認が <code class="language-plaintext highlighter-rouge">docker ...</code> に統一される</li>
  <li><code class="language-plaintext highlighter-rouge">--restart unless-stopped</code> という<a href="/terms/restart-policy/">再起動ポリシー</a>で自動起動できるため、サーバ再起動後も復旧しやすい</li>
  <li>アプリの実行環境（Node.js / 依存関係）がイメージとして固定される</li>
  <li>秘密情報（トークン）と設定（settings.json）をホスト側で管理し、差し替えできる</li>
</ul>

<h2 id="bot-の事前確認事項">Bot の事前確認事項</h2>

<p>使用する Bot のリポジトリは <a href="https://github.com/Reotech736/discord-chatgpt-bot">こちら（GitHub）</a> です。<br />
この Bot は以下を前提にしています（Docker 化に効いてくる部分だけ抜粋）。</p>

<ul>
  <li>Node.js で動作し、起動は <code class="language-plaintext highlighter-rouge">npm start</code></li>
  <li><code class="language-plaintext highlighter-rouge">.env</code> に <code class="language-plaintext highlighter-rouge">DISCORD_TOKEN</code> と <code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code> を置く</li>
  <li>モデル選択や履歴 ON/OFF の設定を <code class="language-plaintext highlighter-rouge">settings.json</code> に保存（このファイルは永続化が必要）</li>
</ul>

<h2 id="アーキテクチャ図">アーキテクチャ図</h2>

<p>今回、構築予定の全体像です。黄色がホスト（Ubuntu Server + Docker Engine）、緑が Bot コンテナです。<br />
<code class="language-plaintext highlighter-rouge">.env</code> と <code class="language-plaintext highlighter-rouge">settings.json</code> はホスト側で管理して、起動時に渡す/マウントします。</p>

<p><img src="/assets/images/posts/2026-01-26-discord-chatgpt-bot-docker/discord-chatgpt-bot-architecture.svg" alt="discord-chatgpt-bot architecture" /></p>

<h2 id="dockerfileコンテナの設計図">Dockerfile（コンテナの設計図）</h2>

<p>今回の <a href="/terms/dockerfile/">Dockerfile</a> は「Node.js の公式イメージをベースに、依存関係を入れて起動する」というシンプルな形です。ポイントは <strong>キャッシュが効く順序</strong>と、<strong>root ではなく node ユーザーで動かす</strong>ことです。</p>

<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:18-slim</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># 依存関係だけ先にコピー（ここがキャッシュに効く）</span>
<span class="k">COPY</span><span class="s"> package.json package-lock.json ./</span>
<span class="k">RUN </span>npm ci <span class="nt">--omit</span><span class="o">=</span>dev

<span class="c"># アプリ本体をコピー</span>
<span class="k">COPY</span><span class="s"> . .</span>

<span class="c"># 非 root 実行のために権限を調整</span>
<span class="k">RUN </span><span class="nb">chown</span> <span class="nt">-R</span> node:node /app
<span class="k">USER</span><span class="s"> node</span>

<span class="k">CMD</span><span class="s"> ["npm", "start"]</span>
</code></pre></div></div>

<h3 id="tips-なぜ-copy-packagejson-を先にやるのか">Tips: なぜ <code class="language-plaintext highlighter-rouge">COPY package*.json</code> を先にやるのか</h3>

<p>アプリのソースコードが変わっても、依存関係が変わらない限り <code class="language-plaintext highlighter-rouge">npm ci</code> のレイヤーをキャッシュできるため、ビルドが速くなります。<br />
（逆に最初に <code class="language-plaintext highlighter-rouge">COPY . .</code> してしまうと、ちょっとした変更でも毎回依存インストールが走りがちです）。</p>

<h3 id="tips-なぜ非-rootuser-nodeで動かすのか">Tips: なぜ非 root（<code class="language-plaintext highlighter-rouge">USER node</code>）で動かすのか</h3>

<p>コンテナの中とはいえ root で動かすと、万一の侵入時に影響が大きくなります。公式の <code class="language-plaintext highlighter-rouge">node</code> イメージには <code class="language-plaintext highlighter-rouge">node</code> ユーザーが最初から用意されているので、素直にそれを使うのが手軽です。</p>

<h2 id="dockerignoreビルドに不要なものを除外する"><code class="language-plaintext highlighter-rouge">.dockerignore</code>（ビルドに不要なものを除外する）</h2>

<p>Docker はビルド時に “<a href="/terms/build-context/">ビルドコンテキスト</a>” としてディレクトリ一式を送るので、不要なものは最初から<a href="/terms/dockerignore/"><code class="language-plaintext highlighter-rouge">.dockerignore</code></a>で除外します。とくに <code class="language-plaintext highlighter-rouge">.env</code> と <code class="language-plaintext highlighter-rouge">node_modules</code> を入れないのが重要です。</p>

<pre><code class="language-txt">node_modules
npm-debug.log
.env
.git
.gitignore
workspace
</code></pre>

<h3 id="tips-env-を-dockerignore-に入れる理由">Tips: <code class="language-plaintext highlighter-rouge">.env</code> を <code class="language-plaintext highlighter-rouge">.dockerignore</code> に入れる理由</h3>

<ul>
  <li><strong>ビルドコンテキストに入れない</strong>ため（誤って <code class="language-plaintext highlighter-rouge">COPY . .</code> でイメージに混ざるのを防ぐ）</li>
  <li><code class="language-plaintext highlighter-rouge">.env</code> は環境ごとに違い、またトークンなど秘密情報が含まれるので、イメージの設計図（Dockerfile）から分離したい</li>
</ul>

<h2 id="環境変数の渡し方そして-dockerfile-に含めない理由">環境変数の渡し方（そして Dockerfile に含めない理由）</h2>

<p>この Bot に必要な<a href="/terms/environment-variable/">環境変数</a>は <code class="language-plaintext highlighter-rouge">DISCORD_TOKEN</code> と <code class="language-plaintext highlighter-rouge">OPENAI_API_KEY</code> です。これらは <strong>Dockerfile に書かず、起動時に渡します</strong>。例：<code class="language-plaintext highlighter-rouge">.env</code></p>

<pre><code class="language-env">DISCORD_TOKEN=xxxxxxxx
OPENAI_API_KEY=xxxxxxxx
</code></pre>

<p>起動（<code class="language-plaintext highlighter-rouge">--env-file</code> を使う）</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--name</span> discord-chatgpt-bot <span class="se">\</span>
  <span class="nt">--env-file</span> .env <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">/settings.json:/app/settings.json"</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  discord-chatgpt-bot:1.0
</code></pre></div></div>

<h3 id="tips-なぜ環境変数を-dockerfile-に含めないのか">Tips: なぜ環境変数を Dockerfile に含めないのか</h3>

<p>結論：<strong>秘密情報をイメージに焼き込まない</strong>ためです。</p>

<ul>
  <li>イメージは配布・共有され得る（秘密情報を含めると漏えいリスクが跳ね上がる）</li>
  <li>Dockerfile の <code class="language-plaintext highlighter-rouge">ENV</code> や <code class="language-plaintext highlighter-rouge">ARG</code> は、ビルドキャッシュや履歴・レイヤーから追跡される可能性がある</li>
  <li>本番/検証/開発でキーを切り替えたい（起動時に差し替えた方が運用が楽）</li>
</ul>

<h2 id="設定ファイルの永続化settingsjson">設定ファイルの永続化（<code class="language-plaintext highlighter-rouge">settings.json</code>）</h2>

<p>この Bot はモデル選択や履歴 ON/OFF を <code class="language-plaintext highlighter-rouge">settings.json</code> に保存します。コンテナは作り直す前提の仕組みなので、ファイルをコンテナ内に置きっぱなしにすると消えます。<br />
そこで、<code class="language-plaintext highlighter-rouge">settings.json</code> はホスト側に置いて、コンテナに <strong><a href="/terms/bind-mount/">バインドマウント</a></strong>します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">/settings.json:/app/settings.json"</span>
</code></pre></div></div>

<h3 id="tips-マウントは-必要最小限-にする">Tips: マウントは “必要最小限” にする</h3>

<p>ホストのパスをマウントした分だけ、コンテナからホストのファイルに触れられます。今回は <code class="language-plaintext highlighter-rouge">settings.json</code> だけをマウントし、範囲を最小化します（安全・管理の両面でメリットがあります）。</p>

<h2 id="ビルドと起動手順まとめ">ビルドと起動（手順まとめ）</h2>

<p>今回は単一のBotを<code class="language-plaintext highlighter-rouge">docker run</code>で起動します。複数のサービスをまとめて構成管理する場合は、<a href="/terms/docker-compose/">Docker Compose</a>を使う方法もあります。</p>

<h3 id="1-イメージをビルド">1) イメージをビルド</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> discord-chatgpt-bot:1.0 <span class="nb">.</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">permission denied while trying to connect to the docker API</code> が出る場合は、<code class="language-plaintext highlighter-rouge">sudo docker ...</code> で実行するか、ユーザーを <code class="language-plaintext highlighter-rouge">docker</code> グループに追加します。</p>

<h3 id="2-コンテナ起動バックグラウンド">2) コンテナ起動（バックグラウンド）</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--name</span> discord-chatgpt-bot <span class="se">\</span>
  <span class="nt">--env-file</span> .env <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">/settings.json:/app/settings.json"</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  discord-chatgpt-bot:1.0
</code></pre></div></div>

<h3 id="3-起動確認とログ">3) 起動確認とログ</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps
docker logs <span class="nt">-f</span> discord-chatgpt-bot
</code></pre></div></div>

<p>ログに <code class="language-plaintext highlighter-rouge">ログイン成功</code> / <code class="language-plaintext highlighter-rouge">全コマンド登録完了</code> が出れば OK です。</p>

<h2 id="運用停止更新自動起動">運用：停止・更新・自動起動</h2>

<h3 id="停止再起動">停止・再起動</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker stop discord-chatgpt-bot
docker start discord-chatgpt-bot
</code></pre></div></div>

<h3 id="更新コードを直したら">更新（コードを直したら）</h3>

<p>コンテナは「作り直す」運用が基本なので、更新は次の流れになります。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> discord-chatgpt-bot:1.1 <span class="nb">.</span>
docker stop discord-chatgpt-bot
docker <span class="nb">rm </span>discord-chatgpt-bot
docker run <span class="nt">-d</span> <span class="nt">--name</span> discord-chatgpt-bot <span class="se">\</span>
  <span class="nt">--env-file</span> .env <span class="se">\</span>
  <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$PWD</span><span class="s2">/settings.json:/app/settings.json"</span> <span class="se">\</span>
  <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
  discord-chatgpt-bot:1.1
</code></pre></div></div>

<h3 id="自動起動サーバ再起動対策">自動起動（サーバ再起動対策）</h3>

<p>今回の起動コマンドでは <code class="language-plaintext highlighter-rouge">--restart unless-stopped</code> を付けています。これにより、サーバ再起動や Docker デーモン再起動後も、明示的に止めていない限り自動で復旧します。</p>

<h2 id="まとめ">まとめ</h2>

<p>ターミナルでの手動起動をやめて Docker 化すると、「起動・停止・ログ・自動復旧」の運用が揃って管理がかなり楽になります。<br />
さらに <code class="language-plaintext highlighter-rouge">.env</code>（秘密情報）と <code class="language-plaintext highlighter-rouge">settings.json</code>（永続化が必要な設定）をイメージから分離できるので、保守性と安全性が一段上がります。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Ubuntu" /><category term="Docker" /><category term="Discord" /><summary type="html"><![CDATA[はじめに]]></summary></entry><entry><title type="html">Docker環境の構築</title><link href="https://reotech736.com/2026/01/21/docker-dev-setup.html" rel="alternate" type="text/html" title="Docker環境の構築" /><published>2026-01-21T00:00:00+09:00</published><updated>2026-01-21T00:00:00+09:00</updated><id>https://reotech736.com/2026/01/21/docker-dev-setup</id><content type="html" xml:base="https://reotech736.com/2026/01/21/docker-dev-setup.html"><![CDATA[<h2 id="はじめに">はじめに</h2>

<p>Ubuntu に <a href="/terms/docker/">Docker</a> をインストールして、<a href="/terms/container/">コンテナ</a>を動かせる状態にするまでの手順をまとめます。Docker を触るのが初めてでも理解しやすいように、まず用語（Docker / コンテナ / イメージ）をざっくり説明してから進めます。この記事のゴールは「Docker 環境の構築」です（特定のアプリを Docker で動かす話ではありません）。</p>

<h2 id="dockerとは">Dockerとは</h2>

<p>Docker は、アプリを <strong>「コンテナ」</strong> という単位で動かすための仕組み（ツール群）です。アプリの実行に必要なもの（ライブラリや設定など）をまとめて扱えるので、環境差分によるトラブルを減らしやすくなります。<br />
Docker を使うときの登場人物は大体この3つです。</p>

<ul>
  <li><strong><a href="/terms/docker-engine/">Docker Engine</a>（デーモン）</strong>: バックグラウンドで動いてコンテナを起動・停止する本体</li>
  <li><strong>Docker CLI</strong>: <code class="language-plaintext highlighter-rouge">docker</code> コマンド（エンジンに指示するための操作口）</li>
  <li><strong><a href="/terms/container-registry/">Docker Registry</a></strong>: イメージの置き場（Docker Hub など）</li>
</ul>

<h2 id="コンテナとは">コンテナとは</h2>

<p>コンテナは、ざっくり言うと <strong>「隔離された実行環境」</strong> です。「別マシンのように見えるプロセス」を作って、その中でアプリを動かします。<br />
ポイントはここです。</p>

<ul>
  <li>コンテナは <strong>VM（仮想マシン）ではない</strong>（ホストOSのカーネルを共有して動く）</li>
  <li>その分、起動が速くて軽い</li>
  <li>ただし、コンテナの中で作ったデータは <strong>基本的に消えやすい</strong>（必要なら<a href="/terms/docker-volume/">ボリューム</a>で永続化する）</li>
</ul>

<h2 id="docker-imageとは">Docker Imageとは</h2>

<p><a href="/terms/container-image/">Docker Image（コンテナイメージ）</a>は、コンテナの元になる <strong>「実行環境のテンプレート」</strong> です。イメージそのものは読み取り専用で、イメージから起動された実体がコンテナです。</p>

<ul>
  <li><strong>イメージ</strong>: 料理のレシピ（テンプレート）</li>
  <li><strong>コンテナ</strong>: できあがった料理（動いている実体）</li>
</ul>

<p>イメージは手元で作る（<code class="language-plaintext highlighter-rouge">docker build</code>）こともできますし、配布されているものを取得（<code class="language-plaintext highlighter-rouge">docker pull</code>）して使うこともできます。</p>

<h2 id="dockerのインストールubuntu-2404-lts">Dockerのインストール（Ubuntu 24.04 LTS）</h2>

<p>今回は、公式のリポジトリ（<code class="language-plaintext highlighter-rouge">download.docker.com</code>）を追加して Docker Engine を入れます。<br />
前提: <code class="language-plaintext highlighter-rouge">sudo</code> できるユーザーで作業します。</p>

<h3 id="1-既存のdocker関連パッケージを削除入っている場合">1) 既存のDocker関連パッケージを削除（入っている場合）</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get remove <span class="nt">-y</span> docker docker-engine docker.io containerd runc
</code></pre></div></div>

<h3 id="2-事前パッケージをインストール">2) 事前パッケージをインストール</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates curl gnupg
</code></pre></div></div>

<h3 id="3-docker公式gpgキーの追加">3) Docker公式GPGキーの追加</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings
curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /etc/apt/keyrings/docker.gpg
<span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.gpg
</code></pre></div></div>

<h3 id="4-docker公式リポジトリを追加">4) Docker公式リポジトリを追加</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">UBUNTU_CODENAME</span><span class="o">=</span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$VERSION_CODENAME</span><span class="s2">"</span><span class="si">)</span>
<span class="nb">echo</span> <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu </span><span class="k">${</span><span class="nv">UBUNTU_CODENAME</span><span class="k">}</span><span class="s2"> stable"</span> <span class="se">\</span>
| <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
</code></pre></div></div>

<h3 id="5-docker-engineをインストール">5) Docker Engineをインストール</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">docker-compose-plugin</code> まで入れているので、<a href="/terms/docker-compose/">Docker Compose</a> は <code class="language-plaintext highlighter-rouge">docker compose ...</code> の形式で使えます。</p>

<h2 id="動作確認">動作確認</h2>

<h3 id="バージョン確認">バージョン確認</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nt">--version</span>
</code></pre></div></div>

<h3 id="hello-worldで確認">hello-worldで確認</h3>

<p>初回は権限の都合で <code class="language-plaintext highlighter-rouge">sudo docker ...</code> にしています。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker run hello-world
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Hello from Docker!</code> が表示されれば OK です。</p>

<h3 id="実際にコンテナを起動してみる">実際にコンテナを起動してみる</h3>

<p>次の例は、<code class="language-plaintext highlighter-rouge">ubuntu:24.04</code> イメージを取得して（初回はダウンロードされます）、その中で <code class="language-plaintext highlighter-rouge">bash</code> を起動します。<br />
抜けるときは <code class="language-plaintext highlighter-rouge">exit</code> です。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker run <span class="nt">--rm</span> <span class="nt">-it</span> ubuntu:24.04 bash
</code></pre></div></div>

<h2 id="任意sudoなしでdockerを使うdockerグループ">（任意）sudoなしでdockerを使う（dockerグループ）</h2>

<p>毎回 <code class="language-plaintext highlighter-rouge">sudo</code> を付けるのが面倒な場合は、ユーザーを <code class="language-plaintext highlighter-rouge">docker</code> グループに追加します。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="s2">"</span><span class="nv">$USER</span><span class="s2">"</span>
</code></pre></div></div>

<p>追加後は、ログアウト→ログイン（または再起動）で反映されます。<br />
すぐ試したい場合は一時的に <code class="language-plaintext highlighter-rouge">newgrp</code> でも OK です。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>newgrp docker
docker run <span class="nt">--rm</span> hello-world
</code></pre></div></div>

<p>※ <code class="language-plaintext highlighter-rouge">docker</code> グループは実質 root 権限相当の操作ができるため、共有マシンでは付与先に注意します。</p>

<h2 id="最低限覚えるdockerコマンド">最低限覚えるDockerコマンド</h2>

<p>環境構築後に「まず困らない」ための最小セットです。</p>

<ul>
  <li>イメージ一覧: <code class="language-plaintext highlighter-rouge">docker images</code></li>
  <li>コンテナ一覧: <code class="language-plaintext highlighter-rouge">docker ps</code> / <code class="language-plaintext highlighter-rouge">docker ps -a</code></li>
  <li>起動: <code class="language-plaintext highlighter-rouge">docker run ...</code></li>
  <li>停止: <code class="language-plaintext highlighter-rouge">docker stop &lt;name&gt;</code></li>
  <li>ログ: <code class="language-plaintext highlighter-rouge">docker logs -f &lt;name&gt;</code></li>
  <li>コンテナ削除: <code class="language-plaintext highlighter-rouge">docker rm &lt;name&gt;</code></li>
  <li>イメージ削除: <code class="language-plaintext highlighter-rouge">docker rmi &lt;image&gt;</code></li>
  <li>Compose（プラグイン）: <code class="language-plaintext highlighter-rouge">docker compose version</code></li>
</ul>

<h2 id="つまずきポイント">つまずきポイント</h2>

<h3 id="permission-denied-が出る"><code class="language-plaintext highlighter-rouge">permission denied</code> が出る</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sudo docker ...</code> で実行する
または <code class="language-plaintext highlighter-rouge">docker</code> グループ追加後にログインし直してから <code class="language-plaintext highlighter-rouge">docker ...</code> を実行します。</li>
</ul>

<h3 id="cannot-connect-to-the-docker-daemon-が出る"><code class="language-plaintext highlighter-rouge">Cannot connect to the Docker daemon</code> が出る</h3>

<p>Docker Engine が起動していない可能性があります。</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> docker
<span class="nb">sudo </span>systemctl status docker <span class="nt">--no-pager</span>
</code></pre></div></div>

<h2 id="まとめ">まとめ</h2>

<p>Ubuntu に Docker（Engine / Buildx / Compose）を入れて、コンテナを動かすところまで確認しました。<br />
次は、適当なイメージ（例: <code class="language-plaintext highlighter-rouge">ubuntu:24.04</code>）を <code class="language-plaintext highlighter-rouge">docker run --rm -it ...</code> で動かして、コンテナの操作感に慣れるのがおすすめです。</p>]]></content><author><name>Reo Komatsubara</name></author><category term="Ubuntu" /><category term="Docker" /><summary type="html"><![CDATA[はじめに]]></summary></entry></feed>