Ý tưởng nào của tôi cũng bắt đầu giống nhau: nó chạy trên laptop, nó chạy được, và tôi muốn người khác xem. Một thời gian dài, bước tiếp theo luôn là một hóa đơn cloud. Một máy chủ, một cơ sở dữ liệu, một bucket, một tên miền trỏ vào đó, và một khoản phí hằng tháng cho thứ mà chỉ ba người ngó tới. Với một dự án phụ, hay tháng đầu tiên của một dự án thật, đó là sai thứ tự: bạn trả tiền cho production trước khi biết liệu có production hay không.
Bài này đi theo thứ tự ngược lại. Dịch vụ vẫn chạy trên chính cái máy đã dựng nên nó, và một Cloudflare Tunnel cho nó một tên miền HTTPS công khai. Không có gì trên laptop lắng nghe Internet: không chuyển tiếp cổng (port forwarding), không chỉnh router, không lộ địa chỉ IP. Máy chủ động gọi ra Cloudflare, và Cloudflare gửi request ngược xuống qua chính kết nối đó. Tunnel thì miễn phí, còn chứng chỉ TLS, chống DDoS và cache đi kèm với zone Cloudflare mà có lẽ bạn đã có sẵn.
Ví dụ ở đây là thật. Một sản phẩm của tôi chạy trên Fly.io, và ứng dụng di động của nó nhận bản cập nhật JavaScript qua mạng (OTA) từ backend (Expo Updates tự host). Tôi muốn có một bản sao thứ hai của máy chủ cập nhật đó chạy trên Mac cho các bản build phát triển, còn production thì giữ nguyên. Các kết quả bên dưới lấy từ buổi chiều hôm đó, kể cả đoạn nó không chạy.
Cloudflare Tunnel thực ra là gì#
Một máy chủ bình thường ngồi chờ kết nối. Muốn chạm tới laptop từ Internet, bạn phải mở một cổng trên router, chuyển tiếp nó về laptop, rồi cầu mong nhà mạng cấp cho bạn một địa chỉ IPv4 công khai (ở nhiều nơi, kể cả chỗ tôi, thì không). Sau đó mọi thứ lắng nghe trên cổng ấy đều bị mọi máy quét trên Internet chạm tới, cả ngày.
Tunnel đảo ngược chuyện đó. cloudflared chạy cạnh ứng dụng của bạn và mở các kết nối đi ra tới edge của Cloudflare, bốn kết nối tới hai trung tâm dữ liệu gần nhất. Một bản ghi DNS trong zone của bạn trỏ một tên miền vào tunnel. Khi có request tới tên miền đó, Cloudflare kết thúc TLS rồi chuyển request xuống một trong các kết nối ấy. cloudflared sau đó giao nó cho dịch vụ cục bộ mà cấu hình của bạn chỉ định.
Về bảo mật, điểm quan trọng là router của bạn không có quy tắc chiều vào nào và địa chỉ IP của bạn không bao giờ xuất hiện trong DNS. Ai quét đường mạng nhà bạn cũng chẳng thấy gì, vì không có gì trên đó đang lắng nghe.
Thiết lập, từ đầu đến cuối#
Bạn cần một tên miền trên Cloudflare (gói miễn phí là đủ) và cài cloudflared (brew install cloudflared trên Mac).
1. Đăng nhập một lần. Lệnh này mở trình duyệt, bạn chọn zone, và nó lưu một chứng chỉ cho phép máy này tạo tunnel và bản ghi DNS trong zone đó:
cloudflared tunnel login2. Tạo tunnel. Tôi để mọi thứ của dự án này trong một thư mục riêng, vì tệp credentials chính là danh tính của tunnel: ai có nó đều phục vụ được lưu lượng cho các tên miền của bạn.
mkdir -p ~/.cloudflared/myapp && cd ~/.cloudflared/myapp
cloudflared tunnel create --credentials-file ./creds.json myapp-local
chmod 600 creds.jsonTunnel credentials written to ./creds.json. Keep this file secret. To revoke these credentials, delete the tunnel.
Created tunnel myapp-local with id 1ade030c-…3. Trỏ tên miền vào nó. Mỗi tên miền một lệnh, tạo một CNAME có proxy tới <tunnel-id>.cfargotunnel.com:
cloudflared tunnel route dns myapp-local ota.example.com
cloudflared tunnel route dns myapp-local checkin-local.example.com4. Viết các quy tắc ingress. Đây là phần quyết định về bảo mật, nên nó có hẳn một mục riêng bên dưới.
5. Chạy nó. Ứng dụng của tôi vốn đã chạy bằng Docker Compose, nên cloudflared chỉ là thêm một container trên cùng mạng. Nó gọi ứng dụng bằng tên service, và ứng dụng hoàn toàn không cần publish cổng nào ra máy host:
# compose.tunnel.yml, used alongside the project's docker-compose.yml
services:
cloudflared:
image: cloudflare/cloudflared:2026.10.7
command: tunnel --config /etc/cloudflared/config.yml run
volumes:
- ${HOME}/.cloudflared/myapp:/etc/cloudflared:ro
restart: unless-stopped
depends_on: [api, edge]docker compose -f docker-compose.yml -f ~/.cloudflared/myapp/compose.tunnel.yml up -d cloudflaredINF Registered tunnel connection connIndex=0 … location=icn06 protocol=quic
INF Registered tunnel connection connIndex=1 … location=icn07 protocol=quic
INF Registered tunnel connection connIndex=2 … location=icn07 protocol=quic
INF Registered tunnel connection connIndex=3 … location=icn06 protocol=quicBốn kết nối tới Seoul, và laptop đã có mặt trên Internet, nhìn từ ngoài vào. restart: unless-stopped đưa nó trở lại sau khi khởi động lại máy, ngay khi Docker chạy.
Mở đường dẫn, đừng mở cả máy chủ#
Đây là lỗi tôi suýt mắc, và tôi đoán là lỗi phổ biến nhất. Cấu hình dễ nhất chỉ có một dòng: hostname → http://localhost:8000. Như thế là công khai toàn bộ máy chủ phát triển.
Máy chủ phát triển không được làm ra cho việc đó. Máy của tôi chạy với DEBUG=True, hiện đầy đủ traceback và settings mỗi khi có lỗi. Nó còn chạy với xác thực bị tắt (AUTH_MODE=disabled), nên bearer token nào cũng đăng nhập bạn thành một user test. Trên localhost thì vậy là đúng ý, còn trên một tên miền công khai thì là thảm họa. Máy của bạn chắc cũng có thứ tương tự: một trang admin với mật khẩu mặc định, một endpoint nạp dữ liệu mẫu, một debug toolbar, hay một API tin người gọi vì xưa nay chỉ có bạn gọi nó.
Vì vậy cấu hình tunnel là một danh sách cho phép (allowlist). cloudflared so mỗi request với các quy tắc ingress theo thứ tự, theo tên miền và theo một biểu thức chính quy trên đường dẫn. Quy tắc khớp đầu tiên thắng, và quy tắc cuối cùng hứng mọi thứ còn lại:
tunnel: myapp-local
credentials-file: /etc/cloudflared/creds.json
ingress:
# OTA: the two read-only endpoints the phone calls, and the files they link.
- hostname: ota.example.com
path: ^/api/expo-updates/(manifest|status)/$
service: http://api:8000
- hostname: ota.example.com
path: ^/media/
service: http://api:8000
# Device check-ins, authenticated by each device's own key.
- hostname: checkin-local.example.com
path: ^/c/$
service: http://edge:8081
# Everything else, including /admin/ and the publish endpoint: 404 at the edge.
- service: http_status:404Endpoint phát hành nằm trên cùng máy chủ, dưới cùng tiền tố (/api/expo-updates/import-update/), và nó không có trong danh sách. Trang admin hay phần còn lại của API cũng không. Những request đó không bao giờ ra khỏi Cloudflare.
Bạn không cần phải tin regex của mình; cloudflared sẽ cho bạn biết một URL khớp quy tắc nào:
cloudflared tunnel --config config.yml ingress rule https://ota.example.com/admin/https://ota.example.com/api/expo-updates/manifest/ service: http://api:8000
https://ota.example.com/api/expo-updates/import-update/ service: http_status:404
https://ota.example.com/admin/ service: http_status:404
https://ota.example.com/media/expo-updates/x.js service: http://api:8000
https://checkin-local.example.com/c/ service: http://edge:8081
https://checkin-local.example.com/health service: http_status:404Tôi sẽ đưa bước kiểm tra này vào một script và chạy mỗi lần cấu hình thay đổi. Nó mất vài giây, còn một regex khớp rộng hơn ý bạn là kiểu lỗi mà bạn chỉ biết được qua người khác.
Thêm hai nguyên tắc nữa:
- Thứ bạn mở ra phải tự xác thực. Các endpoint OTA phục vụ dữ liệu công khai, chỉ đọc. Check-in của thiết bị được ký bằng khóa riêng của từng thiết bị. Không cái nào dựa vào mạng để giữ an toàn. Nếu muốn cho ai đó xem một trang không có đăng nhập, hãy đặt Cloudflare Access trước tên miền đó (miễn phí tới 50 người dùng) thay vì tự dựng trang đăng nhập.
- Đừng để tunnel chạy ngầm như một phần quen thuộc của quy trình dev. Chạy nó có chủ đích, cho đúng tên miền bạn cần, và tắt khi xong việc.
docker compose stop cloudflaredgỡ mọi thứ khỏi Internet chỉ bằng một lệnh.
Cú redirect nuốt mất tên miền của tôi#
Tunnel đã kết nối, các quy tắc đã kiểm tra, tôi thử từ bên ngoài:
https://ota.example.com/api/expo-updates/status/?… 301
https://ota.example.com/api/expo-updates/manifest/ 301
https://ota.example.com/admin/ 301
https://checkin-local.example.com/c/ 301Đường dẫn nào cũng trả về cùng một mã 301, kể cả những đường dẫn mà tunnel lẽ ra phải trả 404. Nghĩa là request chưa bao giờ tới được cloudflared. Header Location ghi https://www.example.com/, và header server: cloudflare chốt lại: chính Cloudflare đang tự trả lời.
Tôi tự gây ra chuyện này. Mấy tháng trước tôi thêm một redirect rule ở cấp zone, “mọi subdomain khác → www”, làm hố chặn cho các máy quét dò những subdomain bịa ra. Redirect rule chạy trong pipeline xử lý request của Cloudflare trước khi request được gửi tới origin, mà tunnel thì cũng chỉ là một origin. Chính quy tắc đó đã tóm các môi trường preview của tôi trước khi Workers kịp thấy chúng. Cách sửa vẫn như lần trước: thêm một ngoại lệ vào biểu thức của quy tắc.
… and not http.host in {"ota.example.com" "checkin-local.example.com"}Nếu tên miền tunnel của bạn trả về một redirect hay một trang lỗi của Cloudflare thay vì ứng dụng của bạn, hãy kiểm tra Rules → Redirect Rules, Page Rules và mọi WAF custom rule của zone trước khi đi debug tunnel. cloudflared ghi log mọi request nó chuyển tiếp, nên log trống nghĩa là request chưa đi được tới đó.
Những gì nó không làm được#
- UDP từ Internet công khai. Tên miền tunnel công khai chỉ mang HTTP(S) (cùng WebSocket và gRPC). Thiết bị của sản phẩm tôi bình thường check-in bằng CoAP trên UDP, và thứ đó không đi qua tunnel được; các thiết bị trên bản sao cục bộ chuyển sang check-in qua HTTPS, mỗi lần tốn thêm vài giây. Nếu ý tưởng của bạn là game server hay VoIP, tunnel không phải công cụ phù hợp.
- Uptime. Đây là laptop của bạn. Gập máy là site sập; Cloudflare hiện trang lỗi 530. Với một bản demo, một bản beta mười người, hay một bản staging thì không sao. Với thứ gì có người phụ thuộc vào thì không ổn.
- Những thứ đã đóng cứng vào bản build. URL cập nhật của ứng dụng di động là cấu hình native, nên chỉ những bản build tạo với URL của tunnel mới dùng nó. Các bản build trên store của tôi vẫn nói chuyện với production, và đó là lý do thử cách này là an toàn.
Chi phí: Cloudflare Tunnel có miễn phí không?#
Một zone Cloudflare gói miễn phí, tunnel, chứng chỉ của nó và các bản ghi DNS: 0 đô. Máy thì vốn đã chạy sẵn. Chi phí thật duy nhất là giờ bạn chính là đội lo uptime.
Thứ nó tiết kiệm cho bạn là giai đoạn giữa “chạy được trên máy tôi” và “đáng bỏ tiền ra host”. Ngay chiều hôm đó bạn đã có thể gửi link cho một tester, một designer hay khách hàng đầu tiên. Nếu không ai quan tâm, bạn chẳng mất gì. Nếu có, chuyển sang một host thật chỉ là một thay đổi: trỏ CNAME sang chỗ khác.
