SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-28

HisterをVPSでセルフホストする方法

HisterをVPSで動かし、訪問ページと保存ファイルの全文を検索します。v0.17.0を固定したバイナリ版とDocker版、TLS、ログイン、MCP endpointの設定を解説します。

Hister とは何か、何ではないか

Hister は、自分でホストして使う個人向け検索エンジンです。閲覧したページと保管しているファイルの全文をインデックス化し、そのコレクションを Web インターフェイス、ターミナルクライアント、HTTP API、または AI (artificial intelligence) アシスタントから検索できます。Hister が答えるのは、「あれはどこで読んだか」という問いです。

多くの読者は、この考え方を SearXNG を通じて知りますが、両者は同じツールではありません。ご存じの名前が旧来の Searx であれば、そのプロジェクトでは 2023 年以降コードのコミットがなく、SearXNG が引き継いでいます。そのため、現在新たに構築するインスタンスは、いずれにしても SearXNG です。SearXNG はメタ検索プロキシです。検索クエリを SearXNG に送ると、SearXNG が代理で他の検索エンジンに問い合わせ、トラッキング情報を除去した結果を返します。インデックスは各検索エンジンが保持しています。一方、Hister は、ブラウザー拡張機能で取得したページ、インポートしたブラウザー履歴、クロールした URL、指定したディレクトリ内のファイルなど、提供したコンテンツから独自のインデックスを構築します。セルフホストした SearXNG インスタンスを使うと、公開 Web に非公開でアクセスできます。Hister は、自分が読んだコンテンツを検索するためのものです。役割が異なるため、1 台のサーバーで両方を実行する構成は一般的です。その場合は、SearXNG が検索内容を実際にどの程度隠せるかを把握しておくとよいでしょう。SearXNG は検索クエリ自体を隠すのではなく、検索エンジン側で送信元 IP アドレスをサーバーのものに置き換えます。

Hister は AGPLv3 (GNU Affero General Public License, version 3) 以降で提供される free software です。テレメトリはなく、クラウドサービスも必要ありません。このガイドでは、2026-07-28 時点の current release だった version v0.17.0 を固定します。何かをコピーする前に releases page で current tag を確認し、そこで見つけた tag を固定してください。

VPS で Hister をセルフホストする理由

インデックスは完全でなければ役に立ちません。また、読んでいる間にサーバーが稼働していて初めて完全になります。ノート PC は 1 日の半分をスリープしていることがあります。その間にスマートフォンで開いたページはノート PC に届かず、夜間のインポートも開始されません。VPS(仮想プライベートサーバー)は常時稼働するため、所有するすべてのデバイスから同じインデックスに登録でき、睡眠中もクローラーが動作します。

2 つ目の理由は分離です。app セクションで user_handling: true を設定すると、1 つのインスタンス上で各アカウントに固有の認証情報とドキュメントコレクションが割り当てられます。これにより、1 台のサーバーで家庭や小規模チームを運用しても、他の人の閲覧履歴を検索せずに済みます。

3 つ目の理由は、基盤の準備です。VPS にはすでに公開ホスト名と証明書があり、管理できないネットワークからブラウザー拡張機能がサーバーへ接続するために必要です。同じホスト名と証明書はサーバー上の別の処理にも使われます。openGym は、その時点で有効なホスト名に対して最初のパスキーを登録します。そのため、最初のアカウントを作成する前に、ホスト名と証明書を確定させる必要があります。

インストール方法 1: リリースバイナリ

Hister はプラットフォームごとに 1 つのバイナリを提供します。チェックサムファイルと一緒にダウンロードし、インストール前に検証してください。

cd /tmp
curl -LO https://github.com/asciimoo/hister/releases/download/v0.17.0/hister_0.17.0_linux_amd64
curl -LO https://github.com/asciimoo/hister/releases/download/v0.17.0/hister_0.17.0_checksums.txt
sha256sum --ignore-missing -c hister_0.17.0_checksums.txt

正常な結果は、hister_0.17.0_linux_amd64: OK だけが 1 行表示されることです。FAILED の行が表示された場合、ダウンロードしたファイルが破損しているか改変されています。インストールせず、再度ダウンロードしてください。

バイナリをインストールし、system account と、それが使用するディレクトリを作成します。

sudo install -m 755 /tmp/hister_0.17.0_linux_amd64 /usr/local/bin/hister
sudo useradd --system --home-dir /var/lib/hister --shell /usr/sbin/nologin hister
sudo install -d -o hister -g hister -m 750 /var/lib/hister
sudo install -d -m 755 /etc/hister
sudo hister create-config /etc/hister/config.yml

create-config はデフォルト設定ファイルを書き込みます。また、バイナリがこのマシンで実行できることも確認できます。アーキテクチャが異なるファイルをダウンロードした場合は、ここで cannot execute binary file: Exec format error と表示されて失敗します。

必要な設定だけを編集します。生成されたファイルの残りはそのまま使用できます。

app:
  directory: /var/lib/hister
  access_token: 'paste-a-long-random-string-here'
server:
  address: 127.0.0.1:4433
  base_url: https://hister.example.com

openssl rand -hex 32 で token を生成します。ファイルには credential が保存されるため、サービスを起動する前にアクセス権を制限してください。

sudo chown root:hister /etc/hister/config.yml
sudo chmod 640 /etc/hister/config.yml

systemd で実行する

/etc/systemd/system/hister.service を記述します。

[Unit]
Description=Hister personal search engine
After=network-online.target
Wants=network-online.target

[Service]
User=hister
Group=hister
Environment=HISTER_CONFIG=/etc/hister/config.yml
ExecStart=/usr/local/bin/hister listen
Restart=on-failure
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/hister

[Install]
WantedBy=multi-user.target

HISTER_CONFIG は設定ファイルのパスを指定する正式な環境変数です。そのため、この unit は hister アカウントのホームディレクトリに依存しません。ProtectSystem=strict により、このサービスから見えるファイルシステム全体が読み取り専用になります。そのため、ReadWritePaths でデータディレクトリを指定する必要があります。ProtectHome=yes はサービスから /home を隠します。したがって、/home 配下の監視対象ディレクトリは indexer から空に見えます。そこにあるファイルをインデックス化する必要がある場合は、その行を削除してください。

sudo systemctl daemon-reload
sudo systemctl enable --now hister
systemctl status hister --no-pager
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4433/

最後のコマンドが出力する HTTP ステータスコードは、プロセスが待ち受けていることを示します。curl: (7) Failed to connect の場合は待ち受けていません。journalctl -u hister -n 50 --no-pager で理由を確認できます。

Docker Compose を使用する方法

イメージは GitHub container registry に公開されており、リリースごとに 1 つのタグが付けられています。

services:
  hister:
    image: ghcr.io/asciimoo/hister:v0.17.0
    container_name: hister
    user: '1000:1000'
    restart: unless-stopped
    environment:
      - HISTER__SERVER__ADDRESS=0.0.0.0:4433
      - HISTER__SERVER__BASE_URL=https://hister.example.com
      - HISTER__APP__ACCESS_TOKEN=${HISTER_ACCESS_TOKEN}
    volumes:
      - ./data:/hister/data
    ports:
      - 127.0.0.1:4433:4433

すべての設定キーには、HISTER__<SECTION>__<KEY> という形式の環境変数による上書き方法があります。区切り文字は 2 個のアンダースコアです。そのため、コンテナのデプロイに設定ファイルをマウントする必要はありません。HISTER_ACCESS_TOKEN は compose ファイルと同じディレクトリにある .env ファイルへ保存します。ファイルを編集したい場合は、docker run --rm ghcr.io/asciimoo/hister:v0.17.0 create-config > config.yml でデフォルト値を表示できます。

上の 2 行は間違えやすいため、どちらも内容を理解しておく必要があります。

コンテナ内のアドレスは 0.0.0.0:4433 にする必要があります。コンテナには独自の network namespace があります。そのため、コンテナ内で 127.0.0.1 にバインドされたプロセスへは、そのコンテナの内部からしか接続できません。公開ポートが転送する接続先もなくなります。

公開ポートは 127.0.0.1:4433:4433 と記述します。4433:4433 ではありません。Docker は独自の netfilter ルールを挿入してポートを公開します。これらのルールは ufw のルールより先に評価されます。そのため、4433:4433 は、ufw status でポートが閉じていると表示されるホストでも、インターネットから到達可能なままです。ホスト側を 127.0.0.1 にバインドすると、接続経路をリバースプロキシだけに制限できます。同じ問題はサーバー上のすべてのコンテナに当てはまります。詳細は VPS 上の Docker Compose を参照してください。

デフォルトのイメージは UID 1000、GID 1000 で実行されます。そのため、./data はこのアカウントから書き込み可能でなければなりません。書き込みできない場合、コンテナは起動時に権限エラーで停止します。sudo chown -R 1000:1000 ./data で修正できます。これらの番号に馴染みがない場合は、先に コンテナがファイルを書き込む UID と GID を確認してください。

個人用検索インデックスを公開してはいけない理由

Hister はデフォルトで 127.0.0.1:4433 をリッスンします。このデフォルト設定は意図的なものです。1 か月使用した後にインデックスに何が含まれるかを考えてください。社内 Wiki のページ、請求書、ログイン中に開いたサポートチケット、パスワードリセットのページ、その他に読んだすべての内容の全文が含まれます。プロジェクトのドキュメントにも、次のように明記されています。「Hister は、ページの内容を含む閲覧履歴全体をサーバーとの間で送受信します。」

漏えいしたパスワードデータベースは、まず解析してパスワードを破る必要があります。一方、漏えいした個人用インデックスは平文で、すでに検索可能です。そのため、見た目が小規模な self-hosted アプリケーションであっても、より慎重に扱う必要があります。

ここから 2 つの事実が分かります。Hister は初期状態で認証を必要としません。そのため、リバースプロキシを置くだけで、ホスト名を知っている誰にでも、閲覧内容の検索可能なコピーを公開することになります。MCP エンドポイントもデフォルトで /mcp から提供されます。トークンがなければ、そこへ到達できるすべてのクライアントがインデックスを検索できます。

サービスを初めて localhost の外部へ公開する前に、認証を設定してください。1 人で使用する場合は app.access_token だけで十分です。ブラウザー拡張機能、ターミナルクライアント、任意の MCP クライアントから送信する共有 Secret を 1 つ設定します。複数人で使用する場合は user_handling: true を設定して、アカウントを作成します。

sudo -u hister hister create-user alice --admin --config /etc/hister/config.yml

このコマンドでは、8 文字以上のパスワードを入力します。各アカウントには固有のドキュメントと個人用 API トークンが割り当てられます。所有者は、プロフィールページから、または hister update-user--regen-token フラグを使用してトークンを再生成できます。新しいトークンを生成すると、以前のトークンは直ちに無効になります。そのため、そのアカウントで使用しているすべてのデバイスを後で更新する必要があります。

意図がない限り、app.public は変更しないでください。Public mode では、認証されていない検索、プレビュー、ファイル提供、MCP 検索が許可されます。一方で、書き込み、履歴へのアクセス、管理操作は引き続きブロックされます。

リバースプロキシ、TLS、ファイアウォール

Hister 自体は HTTPS を提供しないため、前段で TLS(トランスポート層セキュリティ)を終端します。Caddy が最も簡単です。ACME(自動証明書管理環境)を使用して、証明書を自動的に取得・更新するためです。

hister.example.com {
    reverse_proxy 127.0.0.1:4433
}

sudo systemctl reload caddyで再読み込みします。証明書を発行するには、2つの条件を満たす必要があります。hister.example.comの A レコードがこのサーバーを指していることと、HTTP-01 チャレンジへの応答に使用するポート 80 が開いていることです。どちらか一方でも満たさない場合、ブラウザーにはページではなく TLS エラーが表示され、Caddy のログには失敗したチャレンジが繰り返し記録されます。

それ以外の通信はすべて閉じます。

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

ポート 4433 が一覧にないのは意図的です。公開ホスト名だけが接続経路ではありません。同じ loopback ポートを指す onion service を使えば、DNS レコードや受信ポートを一切用意せず、自分のデバイスから index にアクセスできます。

server.base_urlは、スキームを含めてブラウザーに入力するアドレスと一致させる必要があります。一致しない場合、サーバーは base_url を基にアセットへのリンクを生成するため、インターフェースはスタイルのないテキストと欠落した画像で表示されます。その後、ブラウザーは応答しない origin に対してアセットを要求します。同じ URL をブラウザー拡張機能にも設定します。

インデックスへの登録

ブラウザー拡張機能が主な収集手段です。Mozilla Add-ons または Chrome Web Store からインストールし、オプションページを開いてサーバー URL に https://hister.example.com を設定し、アクセストークンを貼り付けます。以降、アクセスした各ページのタイトル、全文、HTML、favicon を取得し、サーバーへ送信します。抽出はブラウザー内のクライアント側で実行されます。拡張機能が接続する第三者サービスはなく、外部へ送信するリクエストはページの favicon 用だけです。

クライアント側で抽出するため、非公開のインデックスを作成できます。拡張機能は、ログイン後かつレンダリング後のページを、表示している状態のまま取得します。そのため、社内 wiki のページや有料記事も正しくインデックス化でき、サーバーに認証情報を渡す必要がありません。一方で、閲覧したものはすべてインデックス候補になります。したがって、追加のコンテンツを登録する前にスキップルールを設定します。

スキップルールは、単一ユーザー構成では rules.json に保存され、複数ユーザー構成ではユーザーごとにデータベースへ保存されます。Web インターフェースの Rules タブが、ルールを編集する最も簡単な方法です。ルールには Go の正規表現を使用し、完全な URL に対して照合します。

^https://mail\.example\.com
^https://bank\.example\.com
.*?utm_source=

https:// で始まる文字列が照合対象になるため、^mail.example.com のようなパターンは一致しません。クエリ文字列を含む URL では、照合時にクエリパラメーターが保持されるため、末尾の $ も一致しません。

既存の履歴はブラウザー自身のデータベースを読み取ってインポートします。そのため、このコマンドはブラウザープロファイルを保持するマシン、つまり VPS ではなくノート PC 上で実行します。そこに同じバイナリをインストールし、サーバーを指定します。

export HISTER_TOKEN='your-access-token'
hister import browser firefox -u https://hister.example.com -t "$HISTER_TOKEN"

インポートは browser-import-YYYY-MM-DD という名前の再開可能なジョブとして実行されます。そのため、途中で中断し、後から再実行できます。Linkwarden、Karakeep、Wallabag、Linkding、Readeck、Shaarli などのブックマークサービスも同じ方法でインポートできます。再度インポートすると、前回より新しいデータだけを取得します。

サーバー上のファイルは、設定でディレクトリを指定するとインデックス化されます。

indexer:
  directories:
    - path: '/var/lib/hister/documents'
      label: 'documents'
      filetypes: ['pdf', 'docx', 'md', 'txt']

PDF、DOCX、Markdown、Org mode、正しい UTF-8 のテキストファイルは全文として読み込まれます。写真と動画は対象外です。そのため、画像ライブラリにはテキストではなく顔、場所、日付をインデックス化するサーバーが必要です。この用途では、通常 PhotoPrism と Immich が比較対象になります。単一ページは hister index https://example.com で追加します。別のツールで利用するためにサイト全体を読みやすいテキストへ変換する作業は別の処理であり、ページを読みやすいテキストへ変換するセルフホスト型クローラーが担当します。

検索はフィールド単位で行われるため、クエリ言語を10分ほど読んでおくと役立ちます。

"connection reset" domain:github.com added:<30d
title:(wireguard|nftables) -tutorial sort:-visits

MCP を介して独自のインデックスをコーディングエージェントに利用させる

MCP(model context protocol)は、アシスタントがサーバー上のツールを呼び出すためのインターフェースです。Hister は同じベース URL で、ストリーム可能な HTTP トランスポートを使用して POST /mcp で提供し、searchget_previewget_history を公開します。認証には、API の他のエンドポイントと同じ bearer token を使用します。ツール呼び出しに慣れていない場合は、小さなエージェントループを自分で作成すると、このようなエンドポイントが実際にアシスタントへ渡す内容を最短で確認できます。

{
  "mcpServers": {
    "hister": {
      "url": "https://hister.example.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_ACCESS_TOKEN"
      }
    }
  }
}

X-Access-Token ヘッダーは Authorization の代替として使用できます。

ここで重要なのは、エージェントが検索する対象です。公開 Web 検索では、その時点で上位に表示される結果が返されます。動きの速いソフトウェアでは、実行中のバージョンとは異なるバージョンのドキュメントが返されることがよくあります。独自のインデックスでは、すでに読んで保存すると判断したページが返されます。また、get_preview が保存済みのコピーを提供するため、元のページがオフラインになっても回答を維持できます。公開結果も必要な場合は、エージェントに両方のソースを渡してください。SearXNG をバックエンドとするブラウザー検索スキルを追加すると、公開 Web を別のツールとして利用できます。これらのエンドポイントを複数運用する場合は、VPS 上で MCP サーバーをホスティングする方法も確認してください。すべてのエンドポイントで、同じ公開範囲の問題が発生するためです。

ディスク、バックアップ、保守

ドキュメントは、圧縮されたプレビューを含めて、インデックス化されたページ1件あたり約100 KBとしています。そのため、100,000ページでおよそ10 GBです。クォータの仕組みはありません。2つの設定が混同されます。indexer.max_file_size_mb(デフォルトは1 MiB)は監視対象の単一ファイルのサイズを制限し、server.max_batch_body_size(デフォルトは40 MiB)は1回の API リクエストのサイズを制限します。

app.directoryで指定されたディレクトリには、言語ごとのインデックスファイルを含む index.db、アカウントとジョブ用の db.sqlite3、プレビュー用の data/html/、および rules.json が格納されます。バックアップでは、サービスを停止して、このディレクトリ全体と設定ファイルをコピーします。hister export backup.jsonは移行用にドキュメントを JSON として書き出す機能であり、サーバーのバックアップではありません。

知っておくべき保守コマンドは2つあります。hister reindexは検索インデックスを再構築します。インデクサーの設定を変更した後に必要です。大規模なインポート中にメモリ使用量が増加する場合は、indexerセクションで detect_languages: false を設定してから、インデックスを再構築します。hister cleanupは、削除後に残った孤立したプレビューと favicon ファイルを削除します。

削除はクエリとして実行されるため、まず dry run モードで実行します。

hister delete 'domain:example.com' --dry --verbose

コレクターがまだそのページを送信している場合、削除したページは再び追加されます。そのため、削除する前にスキップ規則を追加してください。

AGPLv3 が問題になるのは、コードを変更した場合だけです。変更していないコピーを自分で実行するだけなら、義務はありません。Hister を変更し、そのバージョンをネットワーク経由で他の人に使わせる場合、ライセンスにより、変更したソースコードを利用者に提供する必要があります。

障害の発生パターンと表示されるメッセージ

サーバーが起動しない。 ポート 4433 がすでに使用されているか、設定ファイルに YAML の構文エラーがあります。sudo ss -lntp | grep 4433 でポートを使用しているプロセスを確認し、journalctl -u hister -n 50 --no-pager で解析エラーを表示します。

インターフェースは読み込まれるが、表示が崩れている。 文字化けや画像の欠落は、server.base_url がアドレスバーの URL と一致していないことを示します。末尾のスラッシュの有無も不一致として扱われます。

拡張機能が接続できない。 拡張機能に設定したサーバー URL は base_url と一致している必要があります。また、サーバーが稼働中で最新の状態であり、途中のファイアウォールがページにメッセージを表示せず接続を遮断していないことも必要です。Firefox は拡張機能のログを通常のコンソールに出力しません。about:debugging#/runtime/this-firefox を開き、Hister 拡張機能を確認します。

コンテナが起動時に終了する。 ./data の権限エラーは、そのディレクトリの所有者が 1000 以外の UID であることを示します。デフォルトイメージ内では、UID 1000 が使用されています。

管理者用ルートで 403 Forbidden が返る。 ユーザー管理が有効な場合、POST /api/reindexPOST /api/cleanup は管理者専用です。そのため、通常のアカウントでは拒否されます。

インポート中にメモリ使用量が増え続ける。 大量の履歴に対する言語検出が、通常の原因です。detect_languages: false を設定し、その後 hister reindex を実行します。

FAQ

Hister は SearXNG とどう違いますか?

SearXNG はメタ検索プロキシです。クエリを公開検索エンジンへ転送し、追跡情報を除いた検索結果を返します。そのため、インデックスは検索エンジン側にあります。Hister は、閲覧したページと保存したファイルの全文インデックスを独自に保持します。そのため、「あれはどこで読んだか」という検索に答えます。一方、SearXNG は「Web には何が書かれているか」という検索に答えます。両者は異なる問題を解決するものであり、1 台のサーバーで併用することもできます。

閲覧履歴全体を VPS に置いても安全ですか?

事前に公開範囲を適切に制限した場合に限り、安全です。Hister は 127.0.0.1:4433 に bind し、デフォルトでは認証を必要としません。app.access_token または user_handling: true を設定し、前段に TLS 対応のリバースプロキシを置いて、ファイアウォールではポート 4433 を閉じてください。閲覧内容の全文インデックスは平文です。そのため、ポートに到達できるユーザーは、何もクラックせずにすべてを読めます。

ブラウザー拡張機能は必要ですか。それとも履歴をインポートするだけで済みますか?

インポートは 1 回だけ実行する初期取り込みです。ブラウザー自身の履歴データベースを読み込むため、サーバーではなくブラウザープロファイルを保持しているコンピューター上で実行します。その後は拡張機能がインデックスを最新に保ちます。また、ページのレンダリング後にブラウザー内で内容を抽出するため、ログインが必要なページも取得できます。一般的な構成は、最初に 1 回インポートし、その後は拡張機能を使う方法です。

コーディングエージェントで Hister のインデックスを検索できますか?

はい。Hister はベース URL 上の POST /mcp にある MCP (model context protocol) サーバーで、searchget_previewget_history を公開します。Authorization: Bearer ヘッダーにアクセス トークンを設定し、クライアントを https://your-host/mcp に向けてください。エージェントは、現在公開検索エンジンで上位に表示される情報ではなく、実際に読んだバージョンのドキュメントを検索できます。