Hướng dẫn tự host SearXNG trên VPS bằng Docker Compose
Tự cài đặt SearXNG trên VPS với Docker Compose để bảo mật dữ liệu. Bài viết hướng dẫn cấu hình settings.yml, Nginx TLS và cách gọi API tìm kiếm cho script cá nhân của bạn.
Bạn đang xây dựng cái gì
Tự host SearXNG mang lại cho bạn một công cụ tìm kiếm riêng tư chạy trên server của chính bạn. SearXNG là một công cụ metasearch: nó nhận truy vấn của bạn, gửi đến các công cụ khác như Google, Bing, DuckDuckGo và Wikipedia, sau đó gộp kết quả trả về vào một trang duy nhất. Không có hồ sơ nào được tạo và không có tracking cookie nào được thiết lập, vì máy duy nhất lưu giữ truy vấn của bạn chính là máy của bạn.
Stack này rất nhỏ gọn. Hai container, một file cấu hình, một reverse proxy. Quyết định thực sự nằm ở chỗ instance này là private, nghĩa là chỉ bạn và các script của bạn truy cập được, hay là public, nghĩa là bất kỳ ai trên internet cũng có thể truy vấn. Lựa chọn đó làm thay đổi các thiết lập bảo mật, vì vậy hãy quyết định trước khi bạn gõ bất cứ lệnh nào. Mặc định là private.
Có lý do thứ hai để chạy một instance. Một instance SearXNG hỗ trợ JSON, vì vậy bất kỳ script hoặc AI agent nào bạn viết đều có được một search API do bạn sở hữu, không cần key, không tính phí theo truy vấn và không bị giới hạn quota.
Cài đặt SearXNG với Docker Compose
Dự án phát hành một image container và một file Compose. Hãy tải cả hai về một server Ubuntu 24.04 mới đã cài sẵn Docker Engine và plugin Compose. Nếu bạn mới làm quen với Docker, hãy bắt đầu với kiến thức cơ bản về Docker Compose trên VPS rồi quay lại đây.
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 .envFile Compose định nghĩa hai service. core là chính SearXNG, còn valkey là kho lưu trữ dữ liệu trên bộ nhớ dùng để giới hạn tốc độ và lưu trạng thái ngắn hạn. Nó mount ./core-config/ vào /etc/searxng/ bên trong container, vì vậy mọi thứ bạn cấu hình đều nằm trong thư mục đó trên host.
Bây giờ hãy chỉnh sửa .env. Mọi dòng trong ví dụ mẫu đều bị comment, đó là lý do tại sao container khởi chạy trên port 8080 ở mọi địa chỉ. Hãy bỏ comment và thiết lập ba mục sau.
SEARXNG_VERSION=latest
SEARXNG_HOST=127.0.0.1
SEARXNG_PORT=8080SEARXNG_HOST=127.0.0.1 là mục quan trọng. Nó làm cho port được publish thành 127.0.0.1:8080:8080 thay vì [::]:8080:8080, nhờ đó container chỉ phản hồi trên địa chỉ loopback và internet không thể truy cập trực tiếp vào nó. Bỏ qua bước này và container sẽ bị lộ ngay khi khởi chạy, vì port Docker được publish sẽ được chèn vào trước các quy tắc firewall của bạn. Cái bẫy này rất đáng đọc kỹ: các port Docker được publish sẽ bỏ qua ufw.
SEARXNG_VERSION=latest là ổn khi bạn đang tìm hiểu. Trên một server quan trọng, hãy ghim tag. Tính đến tháng 7 năm 2026, các tag phát hành dựa trên ngày tháng và có dạng 2026.3.25-541c6c3cb, vì vậy một bản triển khai được ghim sẽ chỉ nâng cấp khi bạn quyết định, chứ không phải khi registry thay đổi.
settings.yml: các phần quan trọng
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 thiết lập mặc định của chính nó, sau đó chỉ áp dụng các khóa bạn đã viết. Nhờ đó, file của bạn vẫn ngắn gọn và không bị lỗi khi nâng cấp các tùy chọn mới.
Hãy tạo secret trước, vì giá trị này sẽ được đưa 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 là chuỗi ultrasecretkey. Nếu để nguyên, bất kỳ ai biết giá trị mặc định này đều có thể giả mạo token. Hãy thay thế một lần duy nhất, sau đó giữ nguyên: nếu thay đổi sau này, toàn bộ tùy chọn đã lưu sẽ bị mất.
base_url phải là địa chỉ HTTPS công khai, kèm theo dấu gạch chéo ở cuối. Đây là địa chỉ mà SearXNG ghi vào các liên kết mà nó hiển thị. Nếu để trỏ về localhost, liên kết "trang tiếp theo" trên trình duyệt từ xa sẽ trỏ về chính máy của người dùng và bị lỗi.
formats quyết định các loại đầu ra mà web endpoint sẽ tạo ra. json không nằm trong danh sách mặc định, vì vậy yêu cầu JSON sẽ trả về lỗi 403 cho đến khi bạn thêm nó vào. image_proxy: true định tuyến các ảnh thu nhỏ (thumbnail) kết quả thông qua server của bạn, giúp các trang web lưu trữ ảnh đó không bao giờ thấy địa chỉ IP của người truy cập.
valkey.url sử dụng hostname valkey vì đó là tên dịch vụ trong file Compose. Compose đặt cả hai container vào cùng một mạng, nơi các tên dịch vụ có thể phân giải lẫn nhau. Nếu trỏ nó về localhost, bộ giới hạn sẽ bị lỗi, vì bên trong container core, localhost chính là container đó.
Secret nằm trong một file văn bản thuần túy, vì vậy hãy bảo vệ thư mục chứa nó thay vì chỉ bảo vệ file. chmod 750 /opt/searxng giúp ngăn chặn các người dùng khác trên host truy cập. Đừng thắt chặt core-config/settings.yml xuống mode 600: container chạy dưới quyền một người dùng không đặc quyền, và nếu nó không thể đọc file, SearXNG sẽ không thể khởi động.
Khởi động stack và kiểm tra.
cd /opt/searxng
docker compose up -d
docker compose ps
curl -I http://127.0.0.1:8080/docker compose ps sẽ hiển thị cả hai container ở trạng thái running. curl sẽ phản hồi HTTP/1.1 200 OK. Nếu không có phản hồi, 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 phân tích cú pháp kèm theo số dòng.
Đặt nó phía sau nginx với TLS
Container chỉ lắng nghe trên loopback, vì vậy nginx là thành phần giúp nó có thể truy cập được, đồng thời cũng là thành phần bổ sung bảo mật tầng truyền tải (TLS). Hãy 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 tải lại. Certbot ghi đè lên cùng tệp đó để lắng nghe trên cổng 443 với chứng chỉ và thêm lệnh chuyển hướng từ cổng 80. Bản ghi DNS cho search.example.com phải trỏ đến máy chủ này từ trước, vì cơ quan cấp chứng chỉ xác minh quyền sở hữu bằng cách tải xuống một tệp qua HTTP. Hướng dẫn đầy đủ, bao gồm cả việc gia hạn, nằm trong hướng dẫn Certbot và nginx cho Ubuntu 24.04.
Hai header chuyển tiếp không phải là phần trang trí. Nếu thiếu X-Forwarded-For và X-Real-IP, mọi yêu cầu gửi đến SearXNG đều mang địa chỉ của proxy, khiến bộ giới hạn tốc độ (rate limiter) thấy rằng chỉ có một client tạo ra toàn bộ lưu lượng và không thể phân biệt được các khách truy cập.
Tại sao các script và agent cần một JSON search API
Với json trong formats, cùng một endpoint hiển thị trang web sẽ 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 lại một object với mảng results, trong đó mỗi mục chứa url, title, content và engine cung cấp kết quả đó, cùng với answers, infoboxes và suggestions. Thông tin đó đủ để cung cấp cho một bộ tóm tắt, trình kiểm tra liên kết hoặc một vòng lặp nghiên cứu.
Điều này quan trọng đối với bất kỳ thứ gì có hình thái agent. Một language model có thời điểm cắt dữ liệu huấn luyện, vì vậy 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, và các search API thương mại tính phí theo mỗi truy vấn cũng như giới hạn tốc độ rất nghiêm ngặt. Một instance cục bộ chỉ tốn một container trên server mà bạn đã trả phí, và các truy vấn không bao giờ rời khỏi đó. Nếu bạn đang kết nối các công cụ vào một model, lý do tương tự thúc đẩy việc chạy các MCP server trên một VPS, nơi công cụ tìm kiếm thường là công cụ đầu tiên mọi người thêm vào.
Hai quy tắc khi sử dụng API. Hãy giữ cho instance ở chế độ riêng tư, vì vậy hãy bind phía API vào địa chỉ loopback hoặc vào một mạng nội bộ và chỉ cho phép các host của riêng bạn truy cập nó. Sau đó, hãy truy vấn một cách nhẹ nhàng. SearXNG chuyển tiếp yêu cầu của bạn đến các công cụ tìm kiếm thực tế, vì vậy một script chạy hàng trăm truy vấn mỗi giây chính là đang yêu cầu Google chặn server của bạn.
Bộ giới hạn và những thay đổi đối với một instance công khai
Bộ giới hạn là cơ chế phòng thủ bot của SearXNG. Nó giám sát các header yêu cầu, địa chỉ và tốc độ yêu cầu, sau đó loại bỏ lưu lượng truy cập có dấu hiệu tự động. Nó cần Valkey để lưu trữ trạng thái đó, đó là lý do tại sao tệp Compose bao gồm thành phần này.
Trên một instance riêng tư, hãy giữ limiter: false. Các tập lệnh của chính bạn theo định nghĩa là lưu lượng truy cập tự động, vì vậy bộ giới hạn sẽ chặn chính xác các lệnh gọi JSON mà bạn đã xây dựng instance để phục vụ. Thay vào đó, việc kiểm soát truy cập là nhiệm vụ của reverse proxy: một cặp allow và deny trong location của nginx, xác thực HTTP cơ bản hoặc một firewall chỉ cho phép các máy chủ khác của bạn truy cập.
Nếu bạn xuất bản instance cho người khác sử dụng, hãy bật cả hai công tắc này.
server:
limiter: true
public_instance: trueKhả năng kiểm soát chi tiết hơn nằm trong core-config/limiter.toml, tệp mà container đọc tại /etc/searxng/limiter.toml. Bạn chỉ cần viết các khóa mà bạn muốn thay đổi. Khi đứng sau một proxy, bạn phải khai báo proxy đó, nếu không bộ giới hạn sẽ coi địa chỉ nginx của bạn là một 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 trình duyệt thực mới có thể lấy được, điều này ngăn chặn hầu hết các trình thu thập dữ liệu đơn giản. Hãy dự đoán rằng một instance công khai sẽ thu hút chúng trong vòng vài ngày. Hãy dự đoán cả các lỗi engine, vì lưu lượng truy cập bạn chuyển tiếp càng nhiều, các engine thượng nguồn càng sớm bắt đầu trả về CAPTCHA cho địa chỉ máy chủ của bạn. Một instance SearXNG công khai là một công việc cần duy trì liên tục. Một instance riêng tư thì không, đó là lý do tại sao nó nằm trong hầu hết các danh sách ngắn về những thứ đáng để tự lưu trữ vào năm 2026.
Tại sao tìm kiếm không trả về kết quả
Mở /stats trên instance của bạn. Nó liệt kê mọi engine cùng với 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ả tìm kiếm có vẻ thiếu hụt.
Một engine hiển thị lỗi "Access denied" hoặc "CAPTCHA" nghĩa là địa chỉ server của bạn đã bị chặn. Điều này phổ biến với các địa chỉ nằm trong dải IP của trung tâm dữ liệu, vì các công cụ tìm kiếm mặc định rằng đó là các trình thu thập dữ liệu (scrapers). SearXNG sau đó sẽ tạm dừng engine bị lỗi trong một khoảng thời gian thay vì thử lại, vì vậy một engine bị chặn sẽ âm thầm bị loại khỏi kết quả của bạn. Hãy vô hiệu hóa nó trong settings.yml hoặc chấp nhận việc mất kết quả từ engine đó. Các engine còn lại vẫn sẽ phản hồi.
Nếu mọi engine đều lỗi cùng lúc, container không có khả năng phân giải tên miền (name resolution) ra bên ngoài hoặc không có đường truyền đến internet. Hãy kiểm tra điều đó từ bên trong container.
docker compose exec core wget -qO- https://duckduckgo.com > /dev/null && echo okFAQ
SearXNG có làm cho các tìm kiếm của tôi ẩn danh không?
Nó ẩn danh tính của bạn khỏi các công cụ tìm kiếm mà nó truy vấn, vì các công cụ đó thấy máy chủ của bạn thực hiện yêu cầu thay vì trình duyệt của bạn. Nó không ẩn truy vấn khỏi máy chủ của bạn và không ẩn máy chủ của bạn khỏi các công cụ đó. Trên một instance chỉ có một người dùng, tất cả lưu lượng truy cập từ địa chỉ đó đều là của bạn, vì vậy bản thân địa chỉ đó trở thành định danh. Lưu lượng truy cập giữa trình duyệt và instance của bạn được bảo vệ bởi chứng chỉ TLS.
Tại sao một yêu cầu JSON lại trả về lỗi 403 Forbidden?
Có hai nguyên nhân và cả hai đều liên quan đến cấu hình. Hoặc là json bị thiếu trong danh sách formats nằm dưới search: trong settings.yml, đây là trạng thái mặc định, hoặc bộ giới hạn (limiter) đang bật và đã phân loại script của bạn là bot. Hãy thêm định dạng vào trước, khởi động lại bằng docker compose restart core, sau đó thử lại. Nếu vẫn lỗi, hãy thiết lập limiter: false và kiểm soát truy cập tại reverse proxy thay thế.
Tôi có cần container Valkey nếu tôi tắt bộ giới hạn không?
Hãy cứ để nó chạy. SearXNG vẫn hoạt động mà không cần nó, nhưng bộ giới hạn sẽ không thể bật lại sau này nếu thiếu nó, và nó cũng lưu trữ các trạng thái tồn tại ngắn hạn khác. Container này rất nhẹ và chỉ lưu dữ liệu cache, vì vậy việc xóa nó không tiết kiệm được bao nhiêu nhưng lại làm mất đi tùy chọn của bạn.
Làm thế nào để cập nhật SearXNG?
Chạy docker compose pull sau đó là docker compose up -d trong /opt/searxng. Compose sẽ tạo lại bất kỳ container nào có image đã thay đổi và giữ nguyên thư mục core-config/ của bạn, vì vậy settings.yml vẫn được bảo toàn. Vì use_default_settings: true hợp nhất các khóa của bạn với các giá trị mặc định được cung cấp, các tùy chọn mới được thêm vào từ upstream sẽ có giá trị hợp lý thay vì làm hỏng file cấu hình.
Nhiều người có thể dùng chung một instance không?
Có, và đó là trường hợp bạn nên bật bộ giới hạn và thiết lập public_instance: true. Tùy chọn cá nhân được lưu trong trình duyệt của mỗi người truy cập, vì vậy không cần quản lý tài khoản. Hãy theo dõi /stats trong một tuần sau khi mở quyền truy cập, vì các công cụ tìm kiếm upstream sẽ bắt đầu từ chối máy chủ của bạn từ rất lâu trước khi bạn nhận thấy kết quả bị thiếu.