SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-28

Tự host Hister: công cụ tìm kiếm cá nhân trên VPS

Chạy Hister trên VPS để tìm toàn văn trang đã đọc và file đã lưu, với cài binary hoặc Docker, TLS, đăng nhập và MCP endpoint qua HTTP API.

Hister là gì và không phải là gì

Hister là một công cụ tìm kiếm cá nhân mà bạn tự host. Nó lập chỉ mục toàn văn các trang bạn đã truy cập và các file bạn lưu, sau đó cho phép bạn tìm kiếm trong bộ sưu tập đó qua web interface, terminal client, HTTP API hoặc trợ lý AI (trí tuệ nhân tạo). Hister trả lời một câu hỏi: tôi đã đọc thông tin đó ở đâu.

Hầu hết người dùng biết đến ý tưởng này qua SearXNG, nhưng hai công cụ này không giống nhau. Nếu tên bạn biết là Searx đời cũ, dự án đó không có commit mã nguồn nào kể từ năm 2023 và SearXNG tiếp tục phát triển nó, nên instance mới bạn triển khai hôm nay dù thế nào cũng là SearXNG. SearXNG là một proxy metasearch. Bạn gửi query đến nó, nó thay bạn truy vấn các engine khác, rồi trả về kết quả sau khi loại bỏ tracking. Index thuộc về các engine đó. Hister tự xây dựng index từ nội dung bạn cung cấp: các trang được browser extension ghi lại, browser history được import, các URL được crawl và các file trong những directory bạn chỉ định. Một instance SearXNG self-hosted cho phép bạn truy cập riêng tư vào public web. Hister cho phép bạn tìm kiếm nội dung mình đã đọc. Hai công cụ phục vụ các mục đích khác nhau, vì vậy chạy cả hai trên cùng một máy là bình thường. Nếu làm vậy, bạn nên biết SearXNG thực sự che giấu được bao nhiêu hoạt động tìm kiếm của bạn, vì nó thay IP của bạn bằng IP của server khi truy vấn các engine, chứ không che giấu chính các query.

Hister là free software theo AGPLv3 (GNU Affero General Public License, version 3) hoặc các phiên bản mới hơn. Nó không có telemetry và không cần cloud service. Guide này cố định version v0.17.0, là release hiện tại vào ngày 2026-07-28. Hãy kiểm tra releases page để xem tag hiện tại trước khi copy bất kỳ nội dung nào, sau đó cố định tag bạn tìm được tại đó.

Vì sao nên tự host Hister trên VPS

Một index chỉ hữu ích khi đầy đủ. Index chỉ đầy đủ nếu server đang chạy trong lúc bạn đọc. Laptop thường ở trạng thái ngủ nửa ngày. Các trang bạn mở trên điện thoại trong thời gian đó không bao giờ được gửi đến laptop, và một lần import chạy qua đêm cũng không thể bắt đầu. VPS (virtual private server) luôn hoạt động, vì vậy mọi thiết bị của bạn đều gửi dữ liệu vào cùng một index và crawler tiếp tục làm việc trong khi bạn ngủ.

Lý do thứ hai là phân tách. Thiết lập user_handling: true trong phần app cho phép mỗi account có credentials riêng và một bộ sưu tập document riêng trên cùng một instance. Khi đó, một server có thể phục vụ cả gia đình hoặc một nhóm nhỏ mà không ai tìm kiếm được nội dung đọc của người khác.

Lý do thứ ba là phần hạ tầng kết nối. VPS đã có hostname public và certificate. Đây là những gì browser extension cần để kết nối đến server từ một mạng mà bạn không kiểm soát. Cùng hostname và certificate đó còn được dùng cho mục đích khác trên máy chủ, vì openGym đăng ký passkey đầu tiên với hostname đang hoạt động tại thời điểm đó. Vì vậy, phải chốt hostname và certificate trước khi tạo account đầu tiên.

Cách cài đặt 1: binary release

Hister phát hành một binary cho mỗi platform. Tải binary cùng với file checksum, rồi xác minh trước khi cài đặt.

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

Kết quả hợp lệ chỉ có một dòng hister_0.17.0_linux_amd64: OK. Dòng FAILED cho biết file tải xuống bị hỏng hoặc đã bị thay đổi. Hãy tải lại thay vì cài đặt.

Cài binary, sau đó tạo system account và các thư mục mà binary sẽ sử dụng.

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 ghi một file cấu hình mặc định và đồng thời xác nhận binary chạy được trên máy này. Nếu tải nhầm binary cho kiến trúc khác, lệnh sẽ fail tại đây với cannot execute binary file: Exec format error.

Chỉnh một vài setting cần thiết. Có thể giữ nguyên phần còn lại của file được tạo.

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

Tạo token bằng openssl rand -hex 32. File này hiện chứa credential, vì vậy hãy hạn chế quyền truy cập trước khi service khởi động.

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

Chạy bằng systemd

Viết /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 là biến môi trường được tài liệu quy định cho đường dẫn cấu hình, nên unit không phụ thuộc vào thư mục home của account hister. ProtectSystem=strict đặt toàn bộ filesystem ở chế độ chỉ đọc đối với service này. Vì vậy, ReadWritePaths phải chỉ rõ thư mục dữ liệu. ProtectHome=yes ẩn /home khỏi service, nên thư mục được theo dõi bên dưới /home sẽ hiển thị là rỗng đối với indexer. Xóa dòng đó nếu bạn cần index các file tại đó.

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/

Bất kỳ mã trạng thái HTTP nào được in ra bởi lệnh cuối cùng đều có nghĩa là process đang listen. curl: (7) Failed to connect có nghĩa là process không listen, còn journalctl -u hister -n 50 --no-pager sẽ cho biết lý do.

Cách 2 để cài đặt: Docker Compose

Image được phát hành trên GitHub container registry, với mỗi release có một tag.

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

Mỗi configuration key có thể được ghi đè bằng biến môi trường theo dạng HISTER__<SECTION>__<KEY>, trong đó dấu phân cách là hai dấu gạch dưới. Vì vậy, khi deploy container, không cần mount file cấu hình. Giữ HISTER_ACCESS_TOKEN trong file .env đặt cạnh file compose. Nếu muốn chỉnh sửa file, docker run --rm ghcr.io/asciimoo/hister:v0.17.0 create-config > config.yml sẽ in ra các giá trị mặc định.

Hai dòng trên rất dễ cấu hình sai và đều đáng để hiểu rõ.

Địa chỉ bên trong container phải là 0.0.0.0:4433. Container có network namespace riêng. Vì vậy, process bind vào 127.0.0.1 bên trong container chỉ có thể được truy cập từ chính container đó, còn published port sẽ không có đích để forward đến.

Published port phải được ghi là 127.0.0.1:4433:4433, không phải 4433:4433. Docker publish port bằng cách thêm các rule netfilter riêng, và các rule này được xử lý trước rule của ufw. Vì vậy, một 4433:4433 thông thường vẫn có thể truy cập từ internet, ngay cả trên máy mà ufw status cho biết port đã bị đóng. Bind phía host vào 127.0.0.1 sẽ khiến reverse proxy là cách duy nhất để truy cập. Bẫy tương tự áp dụng cho mọi container trên server. Docker Compose trên VPS trình bày phần còn lại.

Image mặc định chạy với UID 1000 và GID 1000. Vì vậy, ./data phải cho account đó quyền ghi. Nếu không, container sẽ dừng khi khởi động và báo lỗi permission. sudo chown -R 1000:1000 ./data sẽ xử lý việc này. Nếu chưa quen với các con số này, hãy đọc container ghi file bằng UID và GID nào trước.

Vì sao personal search index là thứ tệ nhất để public ra Internet

Hister mặc định listen trên 127.0.0.1:4433, và đây là lựa chọn có chủ ý. Hãy xem index chứa gì sau một tháng sử dụng: các trang wiki nội bộ, hóa đơn, support ticket bạn đã mở khi đăng nhập, các trang reset password và toàn bộ nội dung của mọi thứ khác bạn đã đọc. Tài liệu dự án nêu rõ: “Hister truyền toàn bộ browsing history của bạn, cùng nội dung trang, đến và đi từ server.”

Một database password bị lộ vẫn phải được crack. Một personal index bị lộ ở dạng plain text và đã có thể search ngay, nên cần được bảo vệ cẩn thận hơn cả self-hosted app nhỏ mà nó trông giống.

Từ đó có 2 điều cần lưu ý. Mặc định Hister không yêu cầu authentication, nên chỉ một reverse proxy cũng có thể public bản sao có thể search của những gì bạn đã đọc cho bất kỳ ai biết hostname. MCP endpoint cũng mặc định được serve tại /mcp, và nếu không có token, bất kỳ client nào truy cập được endpoint này đều có thể chạy search trên index.

Hãy cấu hình authentication trước khi service rời localhost lần đầu. Một user chỉ cần app.access_token, tức một shared secret được browser extension, terminal client và mọi MCP client gửi lên. Với nhiều người dùng, hãy đặt user_handling: true rồi tạo các account:

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

Command sẽ yêu cầu password có ít nhất 8 ký tự. Mỗi account có bộ tài liệu riêng và một personal API token. Owner có thể tạo lại token từ profile page hoặc dùng flag --regen-token trên hister update-user. Khi tạo token mới, token cũ bị vô hiệu hóa ngay lập tức. Vì vậy, sau đó phải cập nhật mọi device mà account đó đang sử dụng.

Không thay đổi app.public trừ khi bạn thực sự muốn bật chế độ này. Public mode cho phép search không cần authentication, xem preview, serve file và search qua MCP, nhưng vẫn chặn thao tác ghi, truy cập history và các thao tác admin.

Reverse proxy, TLS và firewall

Hister không tự phục vụ HTTPS, vì vậy hãy terminate TLS (transport layer security) ở phía trước Hister. Caddy là cách ngắn gọn nhất, vì nó tự yêu cầu và gia hạn certificate thông qua ACME (automatic certificate management environment).

hister.example.com {
    reverse_proxy 127.0.0.1:4433
}

Reload bằng sudo systemctl reload caddy. Có 2 điều kiện phải được đáp ứng trước khi cấp certificate: bản ghi A cho hister.example.com phải trỏ đến server này, và port 80 phải được mở vì challenge HTTP-01 được xử lý tại đó. Khi thiếu một trong hai điều kiện, trình duyệt sẽ nhận lỗi TLS thay vì trang web, còn log của Caddy sẽ lặp lại thông báo challenge thất bại.

Sau đó đóng tất cả các port còn lại.

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

Port 4433 được cố ý bỏ khỏi danh sách đó. Public hostname không phải là cách duy nhất để truy cập; một onion service trỏ đến cùng loopback port vẫn có thể truy cập index từ các thiết bị của bạn mà không cần DNS record hoặc mở bất kỳ inbound port nào.

server.base_url phải khớp với địa chỉ bạn nhập vào trình duyệt, bao gồm cả scheme. Nếu không khớp, interface sẽ tải với văn bản không có style và thiếu image, vì server tạo các asset link dựa trên base_url, rồi trình duyệt yêu cầu chúng từ một origin không phản hồi. URL đó cũng được nhập vào browser extension.

Điền dữ liệu vào index

Browser extension là collector chính. Cài extension từ Mozilla Add-ons hoặc Chrome Web Store, mở trang tùy chọn, đặt server URL thành https://hister.example.com rồi dán access token. Sau đó extension sẽ lấy title, toàn bộ text, HTML và favicon của từng trang bạn truy cập rồi gửi chúng đến server. Việc trích xuất diễn ra ở phía client, bên trong browser. Extension không liên hệ với bên thứ ba nào; request duy nhất ra bên ngoài là request lấy favicon của trang.

Trích xuất phía client giúp tạo private index. Extension thấy trang đúng như bạn thấy sau khi đăng nhập và sau khi trang render xong. Vì vậy, extension có thể index chính xác trang wiki nội bộ hoặc bài viết trả phí, còn server không bao giờ cần credentials. Điều này cũng có nghĩa là mọi thứ bạn xem đều có thể được đưa vào index. Vì thế, hãy cấu hình skip rules trước khi thêm nội dung.

Skip rules nằm trong rules.json khi cài đặt cho một user, hoặc nằm riêng cho từng user trong database. Tab Rules trên web interface là cách dễ nhất để chỉnh sửa các rule này. Đây là các regular expression của Go, được so khớp với full URL:

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

Pattern như ^mail.example.com không bao giờ khớp, vì chuỗi được kiểm tra bắt đầu bằng https://. $ ở cuối cũng không khớp với URL có query string, vì query parameters vẫn được giữ lại khi so khớp.

Lịch sử hiện có được import bằng cách đọc database của browser. Vì vậy, command này phải chạy trên máy chứa browser profile, tức laptop của bạn chứ không phải VPS. Cài cùng binary trên máy đó rồi trỏ binary đến server:

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

Một lần import chạy dưới dạng resumable job có tên browser-import-YYYY-MM-DD. Bạn có thể dừng job rồi chạy lại sau. Các bookmark service cũng được import theo cách này, gồm Linkwarden, Karakeep, Wallabag, Linkding, Readeck và Shaarli. Khi import lại, hệ thống chỉ lấy những mục mới hơn lần import trước.

Các file trên server được index bằng cách khai báo directory trong config:

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

PDF, DOCX, Markdown, Org mode và các file text UTF-8 hợp lệ được đọc dưới dạng full text. Photos và video không nằm trong danh sách này. Vì vậy, image library cần một server có khả năng index khuôn mặt, địa điểm và ngày tháng thay vì text. PhotoPrism và Immich là hai lựa chọn thường được so sánh cho mục đích đó. Dùng hister index https://example.com để thêm một trang đơn. Chuyển toàn bộ website thành text sạch cho các tool khác là một công việc riêng, do các crawler self-hosted chuyển trang thành text sạch xử lý.

Search hoạt động theo field, nên bạn nên dành mười phút đọc query language:

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

Trỏ coding agent vào index riêng của bạn qua MCP

MCP (model context protocol) là giao diện để assistant gọi các tool trên server. Hister cung cấp MCP tại POST /mcp trên cùng base URL, sử dụng transport streamable HTTP, và cung cấp search, get_preview cùng get_history. Cơ chế xác thực dùng cùng bearer token như phần còn lại của API. Nếu tool calling còn mới với bạn, tự viết một agent loop nhỏ là cách nhanh nhất để thấy một endpoint như endpoint này thực sự cung cấp gì cho assistant.

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

Header X-Access-Token là lựa chọn thay thế cho Authorization.

Giá trị nằm ở nội dung mà agent tìm kiếm. Open web search trả về các kết quả đang được xếp hạng tại thời điểm hiện tại. Với phần mềm thay đổi nhanh, đó thường là documentation cho version mà bạn không chạy. Index riêng trả về đúng page mà bạn đã đọc và chọn lưu lại. get_preview cung cấp bản copy đã lưu, nên câu trả lời vẫn dùng được ngay cả khi page gốc không còn online. Nếu muốn có cả kết quả public, hãy cung cấp cho agent cả hai nguồn: một browser search skill dùng SearXNG thêm open web dưới dạng một tool riêng. Khi bạn chạy nhiều hơn một endpoint như vậy, host MCP server trên VPS là nội dung nên đọc, vì tất cả endpoint đều có cùng vấn đề về exposure.

Ổ đĩa, backup và bảo trì

Tài liệu ước tính mỗi trang được index chiếm khoảng 100 KB, đã tính cả preview nén, nên một trăm nghìn trang chiếm khoảng 10 GB. Hệ thống không có quota. Có 2 setting thường bị nhầm là một: indexer.max_file_size_mb (mặc định 1 MiB) giới hạn kích thước của một file được theo dõi, còn server.max_batch_body_size (mặc định 40 MiB) giới hạn kích thước của một API request.

Thư mục được chỉ định bởi app.directory chứa index.db với các file index theo từng ngôn ngữ, db.sqlite3 cho account và job, data/html/ cho preview và rules.json. Backup gồm service đã dừng, một bản sao của toàn bộ thư mục đó và file config. hister export backup.json ghi document dưới dạng JSON để migration; đây không phải là backup của server.

Có 2 lệnh bảo trì cần biết. hister reindex xây dựng lại các search index. Bạn phải chạy lệnh này sau khi thay đổi setting của indexer. Nếu mức sử dụng memory tăng trong khi import dữ liệu lớn, hãy đặt detect_languages: false trong section indexer rồi reindex. hister cleanup xóa các file preview và favicon bị mồ côi do thao tác xóa để lại.

Xóa là một query, nên trước tiên hãy chạy ở chế độ dry run:

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

Page đã xóa sẽ xuất hiện lại nếu collector vẫn submit page đó. Vì vậy, hãy thêm skip rule trước khi xóa.

AGPLv3 chỉ bắt đầu có hiệu lực khi bạn thay đổi code. Việc tự chạy một bản chưa chỉnh sửa không tạo ra nghĩa vụ nào. Nếu bạn sửa đổi Hister và cho người khác sử dụng phiên bản của bạn qua network, licence yêu cầu bạn cung cấp source đã sửa đổi cho họ.

Các trường hợp lỗi và chuỗi bạn sẽ thấy

Server không khởi động. Cổng 4433 có thể đã bị tiến trình khác chiếm dụng hoặc file cấu hình có lỗi cú pháp YAML. sudo ss -lntp | grep 4433 cho biết tiến trình nào đang giữ cổng, còn journalctl -u hister -n 50 --no-pager in ra lỗi phân tích cú pháp.

Interface tải được nhưng hiển thị lỗi. Văn bản bị xáo trộn và hình ảnh bị thiếu nghĩa là server.base_url không khớp với URL trên thanh địa chỉ. Dấu gạch chéo ở cuối cũng được tính là không khớp.

Extension không kết nối. Server URL trong extension phải khớp với base_url, server phải đang chạy và ở phiên bản hiện tại, đồng thời firewall trên đường truyền có thể chặn kết nối mà không hiển thị thông báo trên trang. Firefox không ghi log của extension vào console thông thường: mở about:debugging#/runtime/this-firefox rồi kiểm tra extension Hister.

Container thoát ngay khi khởi động. Lỗi quyền trên ./data cho biết thư mục thuộc về một UID khác 1000. UID 1000 là tài khoản bên trong image mặc định.

Nhận 403 Forbidden từ admin route. POST /api/reindexPOST /api/cleanup chỉ dành cho admin khi bật xử lý user, nên account thông thường sẽ bị từ chối tại đó.

Bộ nhớ tăng dần trong khi import. Phát hiện ngôn ngữ trên history lớn thường là nguyên nhân. Đặt detect_languages: false rồi chạy hister reindex sau đó.

FAQ

Hister khác SearXNG như thế nào?

SearXNG là một metasearch proxy: nó chuyển tiếp truy vấn của bạn đến các engine công khai và trả về kết quả sau khi loại bỏ tracking, nên index thuộc về các engine đó. Hister tự duy trì full-text index của những trang bạn đã truy cập và các file bạn lưu, nên nó trả lời câu hỏi “tôi đã đọc thông tin đó ở đâu”, còn SearXNG trả lời “web nói gì”. Hai công cụ giải quyết các vấn đề khác nhau, và nhiều người chạy cả hai trên cùng một server.

Đưa toàn bộ lịch sử duyệt web lên VPS có an toàn không?

Chỉ an toàn khi bạn xử lý việc giới hạn exposure trước. Hister bind vào 127.0.0.1:4433 và mặc định không yêu cầu authentication. Thiết lập app.access_token hoặc user_handling: true, đặt một reverse proxy có TLS ở phía trước, và đóng port 4433 trên firewall. Full-text index về những gì bạn đã đọc là plain text, nên bất kỳ ai truy cập được port này đều có thể đọc toàn bộ dữ liệu mà không cần crack gì.

Tôi có cần browser extension không, hay chỉ cần import history?

Import chỉ là thao tác backfill một lần. Nó đọc database history riêng của browser, nên chạy trên máy đang chứa browser profile, không phải trên server. Sau đó extension tiếp tục cập nhật index và lấy được cả các trang yêu cầu đăng nhập vì nó trích xuất nội dung trong browser sau khi trang render. Cách thiết lập phổ biến là import một lần, rồi cài extension.

Coding agent có thể tìm kiếm trong Hister index của tôi không?

Có. Hister là một MCP (model context protocol) server tại POST /mcp trên base URL của bạn, cung cấp search, get_previewget_history. Trỏ client đến https://your-host/mcp với một header Authorization: Bearer chứa access token của bạn. Khi đó agent sẽ tìm kiếm trong tài liệu mà bạn thực sự đã đọc, đúng phiên bản bạn đã đọc, thay vì tìm trong các kết quả đang được public search engine xếp hạng tại thời điểm hiện tại.