Tự host SearXNG: công cụ tìm kiếm riêng trên VPS
Chạy SearXNG trên VPS bằng Docker Compose: cấu hình settings.yml, limiter, nginx với TLS và JSON search API để script của bạn gọi trực tiếp.
Bạn đang xây dựng gì
Tự host SearXNG cho bạn một công cụ tìm kiếm riêng chạy trên server của chính bạn. SearXNG là một metasearch engine: nó nhận query của bạn, gửi query đó đến các engine khác như Google, Bing, DuckDuckGo và Wikipedia, rồi gộp kết quả trả về thành một trang kết quả. SearXNG không tạo profile và không đặt tracking cookie, vì máy duy nhất lưu query của bạn là máy của bạn. Nếu bạn tìm thấy các hướng dẫn cũ cho một dự án chỉ có tên là Searx, đó là dự án mà SearXNG đã fork, và dự án đó không có commit nào từ năm 2023, vì vậy hãy kiểm tra trạng thái của cả hai trước khi làm theo một hướng dẫn.
Stack này khá nhỏ. Bạn chỉ cần hai container, một settings file và một reverse proxy. Nó có thể chạy ổn trên một VPS nhỏ. Điều này không đúng với mọi dịch vụ self-hosted: các thư viện ảnh được so sánh trong PhotoPrism và Immich đặt mức RAM tối thiểu theo indexer thay vì web app. Quyết định thực sự là instance sẽ ở chế độ private, nghĩa là chỉ bạn và các script của bạn truy cập được, hay public, nghĩa là bất kỳ ai trên Internet cũng có thể gửi query. Lựa chọn này thay đổi các thiết lập bảo mật, vì vậy hãy quyết định trước khi nhập bất kỳ lệnh nào. Mặc định nên chọn private.
Có thêm một lý do để chạy instance riêng. SearXNG cung cấp JSON, nên mọi script hoặc AI agent bạn viết đều có thể dùng một search API do bạn sở hữu, không cần key, không tính phí theo từng query và không bị giới hạn quota.
Cài SearXNG bằng Docker Compose
Dự án cung cấp container image và Compose file. Hãy pull cả hai về một server Ubuntu 24.04 mới đã có Docker Engine và Compose plugin. Nếu bạn chưa quen Docker, hãy bắt đầu với Những điều cơ bản về Docker Compose trên VPS rồi quay lại.
sudo install -d -o "$USER" -g "$USER" -m 750 /opt/searxng
cd /opt/searxng
mkdir -p core-config
curl -fsSL \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/docker-compose.yml \
-O https://raw.githubusercontent.com/searxng/searxng/master/container/.env.example
cp -i .env.example .envCompose file định nghĩa 2 service. core là chính SearXNG, còn valkey là data store trong memory dùng để rate limiting và lưu state ngắn hạn. File này mount ./core-config/ vào /etc/searxng/ bên trong container, nên mọi cấu hình bạn thiết lập đều nằm trong một directory duy nhất trên host.
Bây giờ hãy sửa .env. Mọi dòng trong example được cung cấp đều đang bị comment, nên container khởi động với port 8080 trên mọi địa chỉ. Hãy bỏ comment và thiết lập 3 dòng sau.
SEARXNG_VERSION=latest
SEARXNG_HOST=127.0.0.1
SEARXNG_PORT=8080SEARXNG_HOST=127.0.0.1 là dòng quan trọng nhất. Nó đặt published port là 127.0.0.1:8080:8080 thay vì [::]:8080:8080, nên container chỉ trả lời trên địa chỉ loopback và Internet không thể truy cập trực tiếp vào container. Nếu bỏ qua bước này, container sẽ bị expose ngay khi khởi động, vì published Docker port được chèn trước các firewall rule của bạn. Hãy đọc kỹ bẫy này tại: published Docker port bypass ufw.
SEARXNG_VERSION=latest phù hợp trong thời gian bạn đang học. Trên server quan trọng, hãy pin tag. Tính đến tháng 7 năm 2026, các release tag dựa trên ngày và có dạng 2026.3.25-541c6c3cb, nên deployment đã pin version chỉ upgrade khi bạn quyết định, thay vì tự thay đổi khi registry cập nhật. Cách làm này cũng phù hợp với mọi thành phần khác chạy lâu dài trên máy, đó là lý do một RustDesk relay tự host cũng pin image tag: một upgrade không giám sát cho remote access service sẽ xuất hiện vào thời điểm tệ nhất.
settings.yml: các phần cần chú ý
Tạo core-config/settings.yml trước lần khởi động đầu tiên. use_default_settings: true yêu cầu SearXNG tải các giá trị mặc định đi kèm, sau đó chỉ áp dụng những key bạn đã ghi. Nhờ vậy, file luôn ngắn gọn và vẫn tương thích với các bản nâng cấp bổ sung option mới.
Trước tiên, hãy tạo secret vì giá trị này sẽ được ghi trực tiếp vào file.
openssl rand -hex 32use_default_settings: true
general:
instance_name: "search.example.com"
server:
base_url: "https://search.example.com/"
secret_key: "paste-the-openssl-output-here"
limiter: false
public_instance: false
image_proxy: true
valkey:
url: valkey://valkey:6379/0
search:
safe_search: 0
autocomplete: "duckduckgo"
formats:
- html
- jsonsecret_key dùng để ký dữ liệu session và token. Giá trị mặc định đi kèm là chuỗi nguyên văn ultrasecretkey. Nếu giữ nguyên, bất kỳ ai biết giá trị mặc định này đều có thể giả mạo các token đó. Hãy thay đổi giá trị một lần rồi giữ nguyên: thay đổi về sau sẽ xóa mọi tùy chọn đã lưu.
base_url phải là địa chỉ HTTPS public, có dấu gạch chéo ở cuối. SearXNG dùng địa chỉ này để ghi vào các liên kết mà nó render. Nếu để trỏ đến localhost, liên kết "trang tiếp theo" trong trình duyệt remote sẽ trỏ đến chính máy của người đọc và không hoạt động.
formats quyết định các kiểu output mà web endpoint sẽ trả về. json không có trong danh sách mặc định, nên request JSON sẽ trả về 403 cho đến khi bạn thêm nó. image_proxy: true chuyển tiếp thumbnail của kết quả qua server của bạn, vì vậy các site lưu những ảnh đó sẽ không thấy địa chỉ của visitor.
valkey.url dùng hostname valkey vì đó là service name trong file Compose. Compose đặt cả hai container trên cùng một network, nơi service name được phân giải. Nếu trỏ đến localhost, limiter sẽ fail vì bên trong container core, localhost chính là container đó.
Secret nằm trong một file plain text, vì vậy hãy bảo vệ directory chứa nó thay vì chỉ bảo vệ file. chmod 750 /opt/searxng ngăn các user khác trên host truy cập directory này. Không được siết core-config/settings.yml xuống mode 600: container chạy bằng user không có quyền cao riêng, và nếu không đọc được file thì SearXNG sẽ không khởi động.
Khởi động stack rồi kiểm tra.
cd /opt/searxng
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080/docker compose ps phải hiển thị cả hai container ở state running. curl phải trả về HTTP/1.1 200 OK. Nếu không trả về gì, hãy đọc docker compose logs core vì lỗi YAML trong settings.yml sẽ xuất hiện ở đó dưới dạng lỗi parse kèm số dòng.
Đặt sau nginx với TLS
Container chỉ lắng nghe trên loopback, nên nginx giúp truy cập được vào dịch vụ. nginx cũng bổ sung Transport Layer Security (TLS). Viết /etc/nginx/sites-available/searxng.
server {
listen 80;
server_name search.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}sudo ln -s /etc/nginx/sites-available/searxng /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d search.example.comnginx -t in ra syntax is ok và test is successful trước khi bạn reload. Certbot ghi lại chính file đó để lắng nghe trên cổng 443 với certificate, đồng thời thêm redirect từ cổng 80. DNS record của search.example.com phải trỏ đến server này từ trước, vì certificate authority xác minh quyền sở hữu bằng cách tải một file qua HTTP. Toàn bộ hướng dẫn, bao gồm cả việc renewal, có trong hướng dẫn Certbot và nginx cho Ubuntu 24.04.
Hai forwarding header này không phải để trang trí. Nếu thiếu X-Forwarded-For và X-Real-IP, mọi request đến SearXNG đều mang địa chỉ của proxy. Vì vậy rate limiter thấy một client đang tạo ra toàn bộ traffic và không thể phân biệt các visitor.
Vì sao script và agent cần một JSON search API
Với json trong formats, cùng một endpoint dùng để render trang cũng trả về dữ liệu có cấu trúc.
curl -s 'http://127.0.0.1:8080/search?q=wireguard+mtu&format=json' \
| jq -r '.results[0:5][] | .url'Bạn nhận được một object có mảng results. Mỗi entry chứa url, title, content và search engine đã cung cấp kết quả đó, cùng với answers, infoboxes và suggestions. Như vậy là đủ để cấp dữ liệu cho summariser, link checker hoặc vòng lặp nghiên cứu. Đưa các kết quả này cho language model là một bước lớn hơn vẻ ngoài, vì kết quả tìm kiếm là văn bản không đáng tin cậy và có thể chứa instruction riêng. Nội dung trỏ AI agent vào instance SearXNG của bạn trình bày chi tiết cách xử lý vấn đề này.
Điều này quan trọng với mọi hệ thống có dạng agent. Language model có training cutoff, nên cần tìm kiếm trực tiếp để trả lời các câu hỏi về hiện tại. Các search API thương mại tính phí theo query và rate limit rất chặt. Một instance cục bộ chỉ cần một container trên server mà bạn đã trả phí, và query không rời khỏi đó. Nếu bạn đang tích hợp tool vào model, cùng cách suy luận này dẫn đến việc chạy MCP server trên VPS, trong đó search tool thường là tool đầu tiên người dùng thêm vào.
Có 2 quy tắc khi dùng API. Giữ instance ở chế độ private. Hãy bind phía API vào loopback address hoặc private network, rồi chỉ cho các host của bạn truy cập. Sau đó gửi query với tốc độ vừa phải. SearXNG chuyển tiếp request của bạn đến các search engine thật, nên một script chạy 100 query mỗi giây đang yêu cầu Google block server của bạn.
Limiter và những thay đổi khi public instance
Limiter là lớp phòng vệ bot của SearXNG. Nó theo dõi request header, địa chỉ và tốc độ request, rồi loại bỏ traffic có dấu hiệu tự động. Limiter cần Valkey để lưu trạng thái đó, nên Valkey được đưa vào Compose file.
Trên private instance, hãy giữ limiter: false. Các script của bạn mặc định là traffic tự động, nên limiter sẽ chặn đúng những cuộc gọi JSON mà bạn dựng instance này để phục vụ. Thay vào đó, reverse proxy chịu trách nhiệm kiểm soát truy cập: một cặp allow và deny trong location của nginx, HTTP basic authentication, hoặc firewall chỉ cho phép các server khác của bạn kết nối. Nếu cần truy cập private instance từ laptop thường xuyên chuyển đổi giữa các mạng, đặt địa chỉ onion v3 phía trước nó là lựa chọn thứ tư, vì tor kết nối đến cùng một loopback port mà không expose thêm thứ gì ra internet.
Nếu bạn public instance cho người khác sử dụng, hãy bật cả hai switch.
server:
limiter: true
public_instance: trueMuốn kiểm soát chi tiết hơn, hãy dùng core-config/limiter.toml. Container đọc file này tại /etc/searxng/limiter.toml. Bạn chỉ cần ghi những key muốn thay đổi. Khi chạy sau proxy, bạn phải khai báo proxy; nếu không, limiter sẽ xem địa chỉ của nginx là client lạm dụng duy nhất.
[botdetection]
trusted_proxies = [
'127.0.0.0/8',
'::1',
]
[botdetection.ip_limit]
link_token = truelink_token = true khiến SearXNG cấp một token mà chỉ phiên browser thực mới lấy được, nhờ đó chặn được phần lớn scraper đơn giản. Hãy chuẩn bị để public instance thu hút các scraper này chỉ trong vài ngày. Bạn cũng sẽ gặp lỗi từ engine, vì càng chuyển tiếp nhiều traffic, các engine upstream càng sớm trả về CAPTCHA cho địa chỉ server của bạn. Vận hành một public SearXNG instance là công việc liên tục. Private instance thì không như vậy, nên nó nằm trong hầu hết các danh sách ngắn về những thứ đáng tự host trong 2026. Tuy nhiên, không phải mục nào trong các danh sách đó cũng là infrastructure: dựng lại thư viện Jellyfin thành một cửa hàng cho thuê băng đĩa 90s có thể đi dạo tham quan cũng chỉ là một container phía sau cùng một nginx block, hướng đến một buổi tối thay vì một workflow.
Vì sao tìm kiếm không trả về kết quả
Mở /stats trên instance của bạn. Trang này liệt kê mọi engine cùng tỷ lệ lỗi và thời gian phản hồi. Đây là nơi đầu tiên cần kiểm tra khi kết quả trả về quá ít.
Engine hiển thị lỗi “Access denied” hoặc “CAPTCHA” đã chặn địa chỉ server của bạn. Điều này thường xảy ra với các địa chỉ thuộc dải data centre, vì search engine cho rằng chúng được scraper sử dụng. Sau đó SearXNG sẽ tạm ngưng engine bị lỗi trong một khoảng thời gian thay vì thử lại, nên một engine bị chặn có thể âm thầm biến mất khỏi kết quả của bạn. Vô hiệu hóa engine đó trong settings.yml hoặc chấp nhận việc mất nó. Đây không phải là 2 lựa chọn duy nhất, vì một số block CAPTCHA có cách khắc phục vẫn có hiệu lực sau khi restart. Các engine còn lại vẫn trả lời. Mã 429 là trường hợp không rõ nguyên nhân, vì nó có thể xuất phát từ limiter của chính bạn hoặc từ upstream engine từ chối server của bạn; dòng log cho biết bạn đang gặp trường hợp nào trong 2 trường hợp này trước khi bạn bắt đầu thay đổi settings.
Nếu tất cả engine cùng lỗi một lúc, container không phân giải được tên miền outbound hoặc không có route đến Internet. Hãy kiểm tra từ bên trong container.
docker compose exec core wget -qO- https://duckduckgo.com > /dev/null && echo okKhông có thành phần nào trên máy cho bạn biết khi kiểm tra này bắt đầu lỗi. Vì vậy, hãy chạy kiểm tra từ cron và để lỗi gửi cảnh báo đến điện thoại của bạn từ ntfy server do chính bạn vận hành, thay vì chờ đến khi nhận thấy kết quả tìm kiếm đã quá ít.
FAQ
SearXNG có làm cho các lượt tìm kiếm của tôi ẩn danh không?
SearXNG ẩn danh tính của bạn với các engine mà nó truy vấn, vì các engine đó thấy server của bạn gửi request thay vì trình duyệt của bạn. Nó không ẩn truy vấn khỏi server của bạn và cũng không ẩn server của bạn khỏi các engine đó. Trên instance chỉ dành cho một người dùng, toàn bộ traffic từ địa chỉ đó là của bạn, nên chính địa chỉ đó trở thành identifier. Traffic giữa trình duyệt và instance của bạn được bảo vệ bằng TLS certificate. Mức độ bảo vệ của bạn trước ISP, operator của một instance công khai và chính các engine được giải thích trong SearXNG thực sự ẩn những gì.
Vì sao JSON request trả về 403 Forbidden?
Có 2 nguyên nhân và cả hai đều liên quan đến cấu hình. Hoặc json chưa có trong danh sách formats bên dưới search: trong settings.yml, đây là trạng thái mặc định, hoặc limiter đang bật và đã phân loại script của bạn là bot. Trước tiên, hãy thêm format, restart bằng docker compose restart core rồi thử lại. Nếu vẫn lỗi, hãy đặt limiter: false và kiểm soát quyền truy cập tại reverse proxy.
Tôi có cần Valkey container nếu tắt limiter không?
Hãy để container đó chạy. SearXNG vẫn hoạt động mà không cần nó, nhưng sau này không thể bật limiter nếu thiếu container này. Nó cũng lưu các state ngắn hạn khác. Container này nhỏ và chỉ lưu cached data, nên xóa nó tiết kiệm rất ít nhưng làm mất tùy chọn đó.
Làm cách nào để update SearXNG?
Chạy docker compose pull rồi docker compose up -d trong /opt/searxng. Compose sẽ tạo lại mọi container có image đã thay đổi và giữ nguyên directory core-config/, nên settings.yml vẫn được giữ lại. Vì use_default_settings: true merge các key của bạn lên default được phát hành kèm theo, các option mới từ upstream sẽ có giá trị hợp lý thay vì làm hỏng file.
Nhiều người có thể dùng chung một instance không?
Có. Đây là trường hợp bạn bật limiter và đặt public_instance: true. Preferences được lưu trong browser riêng của mỗi visitor, nên không cần quản lý account. Hãy monitor /stats trong 1 tuần sau khi mở quyền truy cập, vì các engine upstream sẽ bắt đầu từ chối server của bạn rất lâu trước khi bạn nhận ra kết quả bị thiếu.