<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Cloudflared · Nhân Tài Đức</title>
    <link>https://jared.lynskey.co.nz/vi/tags/cloudflared/</link>
    <description>Những câu chuyện ngắn về kỹ thuật phần mềm và lãnh đạo đội ngũ kỹ sư — performance review, tuyển dụng và mở rộng team, CI/CD, giám sát DevOps và AI agent. Viết từ Seoul.</description>
    <generator>Hugo</generator>
    <language>vi</language>
    <copyright>© 2026 Nhân Tài Đức</copyright>
    <lastBuildDate>Wed, 11 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/vi/tags/cloudflared/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Self host ngay trên laptop: public localhost miễn phí bằng Cloudflare Tunnel</title>
      <link>https://jared.lynskey.co.nz/vi/posts/2026/self-host-behind-cloudflare-tunnel/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/vi/posts/2026/self-host-behind-cloudflare-tunnel/</guid>
      <pubDate>Wed, 11 Nov 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>Cloudflare</category>
      <category>Cloudflare Tunnel</category>
      <category>cloudflared</category>
      <category>Self-Hosting</category>
      <category>Expose Localhost</category>
      <category>Docker Compose</category>
      <category>Django</category>
      <category>Expo</category>
      <category>OTA Updates</category>
      <category>Security</category>
      <category>Dev Costs</category>
      <description>Cloudflare Tunnel là gì và có miễn phí không? Cách public localhost ra Internet với tên miền HTTPS thật, không mở cổng nào, dùng allowlist trong ingress và Docker Compose.</description>
      <content:encoded><![CDATA[<p>Ý 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.</p>
<p>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.</p>
<p>Ví dụ ở đây là thật. Một <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-07-22-my-current-stack/" >sản phẩm của tôi</a> 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.</p>

<h2 class="relative group">Cloudflare Tunnel thực ra là gì
    <div id="cloudflare-tunnel-thực-ra-là-gì" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#cloudflare-tunnel-th%e1%bb%b1c-ra-l%c3%a0-g%c3%ac" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>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.</p>
<p>Tunnel đảo ngược chuyện đó. <code>cloudflared</code> 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. <code>cloudflared</code> sau đó giao nó cho dịch vụ cục bộ mà cấu hình của bạn chỉ định.</p>
<figure class="diagram">
  <div role="img" aria-label="Một request đi qua HTTPS tới Cloudflare, qua các quy tắc của nó, tới điểm cuối của tunnel, rồi xuống một kết nối đi ra mà cloudflared đã mở từ máy bạn; cloudflared gửi nó tới API, edge, hoặc trả 404 tùy theo tên miền và đường dẫn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 520" aria-hidden="true" focusable="false">
<defs><marker id="tu-ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="d-head" d="M0,0 L10,5 L0,10 z"/></marker></defs>
<text class="d-title" x="160" y="60" text-anchor="middle" font-size="30">Internet</text>
<text class="d-title" x="560" y="60" text-anchor="middle" font-size="30">Cloudflare</text>
<text class="d-title" x="1020" y="60" text-anchor="middle" font-size="30">Máy của bạn</text>
<rect class="d-box" x="40" y="200" width="240" height="100" rx="14"/>
<text class="d-label" x="160" y="245" text-anchor="middle" font-size="19">Điện thoại / trình duyệt</text>
<text class="d-sub" x="160" y="278" text-anchor="middle" font-size="18">ota.example.com</text>
<rect class="d-panel" x="400" y="110" width="320" height="300" rx="30"/>
<rect class="d-box" x="430" y="140" width="260" height="80" rx="14"/>
<text class="d-label" x="560" y="188" text-anchor="middle" font-size="19">TLS, quy tắc redirect, WAF</text>
<rect class="d-box" x="430" y="290" width="260" height="80" rx="14"/>
<text class="d-label" x="560" y="338" text-anchor="middle" font-size="22">Điểm cuối tunnel</text>
<rect class="d-group" x="820" y="110" width="400" height="360" rx="30"/>
<rect class="d-box" x="850" y="290" width="160" height="80" rx="14"/>
<text class="d-label" x="930" y="338" text-anchor="middle" font-size="22">cloudflared</text>
<rect class="d-box" x="1050" y="160" width="140" height="70" rx="14"/>
<text class="d-label" x="1120" y="202" text-anchor="middle" font-size="20">api:8000</text>
<rect class="d-box" x="1050" y="290" width="140" height="70" rx="14"/>
<text class="d-label" x="1120" y="332" text-anchor="middle" font-size="20">edge:8081</text>
<rect class="d-box" x="1050" y="380" width="140" height="70" rx="14"/>
<text class="d-label" x="1120" y="422" text-anchor="middle" font-size="20">404</text>
<path class="d-line" d="M280,250 L428,190" marker-end="url(#tu-ah)"/>
<text class="d-sub" x="350" y="200" text-anchor="middle" font-size="18">HTTPS</text>
<path class="d-line" d="M560,220 L560,288" marker-end="url(#tu-ah)"/>
<path class="d-line" d="M848,330 L692,330" marker-end="url(#tu-ah)"/>
<text class="d-sub" x="770" y="315" text-anchor="middle" font-size="16">QUIC đi ra</text>
<text class="d-sub" x="770" y="360" text-anchor="middle" font-size="16">(không mở cổng)</text>
<path class="d-line" d="M1010,315 L1048,200" marker-end="url(#tu-ah)"/>
<path class="d-line" d="M1010,330 L1048,325" marker-end="url(#tu-ah)"/>
<path class="d-line" d="M1010,345 L1048,410" marker-end="url(#tu-ah)"/>
<text class="d-sub" x="1020" y="500" text-anchor="middle" font-size="18">quy tắc ingress: allowlist tên miền + đường dẫn</text>
</svg>
</div><figcaption>Máy chỉ kết nối đi ra. Các quy tắc của zone Cloudflare chạy trước tunnel; các quy tắc ingress quyết định thứ gì chạm tới được.</figcaption>
</figure>

<p>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.</p>

<h2 class="relative group">Thiết lập, từ đầu đến cuối
    <div id="thiết-lập-từ-đầu-đến-cuối" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#thi%e1%ba%bft-l%e1%ba%adp-t%e1%bb%ab-%c4%91%e1%ba%a7u-%c4%91%e1%ba%bfn-cu%e1%bb%91i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Bạn cần một tên miền trên Cloudflare (gói miễn phí là đủ) và cài <code>cloudflared</code> (<code>brew install cloudflared</code> trên Mac).</p>
<p><strong>1. Đăng nhập một lần.</strong> 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 đó:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cloudflared tunnel login</span></span></code></pre></div></div>
<p><strong>2. Tạo tunnel.</strong> 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.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p ~/.cloudflared/myapp <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/.cloudflared/myapp
</span></span><span class="line"><span class="cl">cloudflared tunnel create --credentials-file ./creds.json myapp-local
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> creds.json</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Tunnel credentials written to ./creds.json. Keep this file secret. To revoke these credentials, delete the tunnel.
</span></span><span class="line"><span class="cl">Created tunnel myapp-local with id 1ade030c-…</span></span></code></pre></div></div>
<p><strong>3. Trỏ tên miền vào nó.</strong> Mỗi tên miền một lệnh, tạo một CNAME có proxy tới <code>&lt;tunnel-id&gt;.cfargotunnel.com</code>:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cloudflared tunnel route dns myapp-local ota.example.com
</span></span><span class="line"><span class="cl">cloudflared tunnel route dns myapp-local checkin-local.example.com</span></span></code></pre></div></div>
<p><strong>4. Viết các quy tắc ingress.</strong> Đâ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.</p>
<p><strong>5. Chạy nó.</strong> Ứng dụng của tôi vốn đã chạy bằng Docker Compose, nên <code>cloudflared</code> 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:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># compose.tunnel.yml, used alongside the project&#39;s docker-compose.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cloudflared</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare/cloudflared:2026.10.7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">tunnel --config /etc/cloudflared/config.yml run</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">${HOME}/.cloudflared/myapp:/etc/cloudflared:ro</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">api, edge]</span></span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose -f docker-compose.yml -f ~/.cloudflared/myapp/compose.tunnel.yml up -d cloudflared</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">INF Registered tunnel connection connIndex=0 … location=icn06 protocol=quic
</span></span><span class="line"><span class="cl">INF Registered tunnel connection connIndex=1 … location=icn07 protocol=quic
</span></span><span class="line"><span class="cl">INF Registered tunnel connection connIndex=2 … location=icn07 protocol=quic
</span></span><span class="line"><span class="cl">INF Registered tunnel connection connIndex=3 … location=icn06 protocol=quic</span></span></code></pre></div></div>
<p>Bốn kết nối tới Seoul, và laptop đã có mặt trên Internet, nhìn từ ngoài vào. <code>restart: unless-stopped</code> đưa nó trở lại sau khi khởi động lại máy, ngay khi Docker chạy.</p>

<h2 class="relative group">Mở đường dẫn, đừng mở cả máy chủ
    <div id="mở-đường-dẫn-đừng-mở-cả-máy-chủ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#m%e1%bb%9f-%c4%91%c6%b0%e1%bb%9dng-d%e1%ba%abn-%c4%91%e1%bb%abng-m%e1%bb%9f-c%e1%ba%a3-m%c3%a1y-ch%e1%bb%a7" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Đâ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: <code>hostname → http://localhost:8000</code>. Như thế là công khai <strong>toàn bộ máy chủ phát triển</strong>.</p>
<p>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 <code>DEBUG=True</code>, 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 (<code>AUTH_MODE=disabled</code>), nên bearer token nào cũng đăng nhập bạn thành một user test. Trên <code>localhost</code> 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ó.</p>
<p>Vì vậy cấu hình tunnel là một danh sách cho phép (allowlist). <code>cloudflared</code> 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:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">tunnel</span><span class="p">:</span><span class="w"> </span><span class="l">myapp-local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">credentials-file</span><span class="p">:</span><span class="w"> </span><span class="l">/etc/cloudflared/creds.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># OTA: the two read-only endpoints the phone calls, and the files they link.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">ota.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">^/api/expo-updates/(manifest|status)/$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://api:8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">ota.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">^/media/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://api:8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Device check-ins, authenticated by each device&#39;s own key.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">checkin-local.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">^/c/$</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http://edge:8081</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Everything else, including /admin/ and the publish endpoint: 404 at the edge.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">service</span><span class="p">:</span><span class="w"> </span><span class="l">http_status:404</span></span></span></code></pre></div></div>
<p>Endpoint phát hành nằm trên cùng máy chủ, dưới cùng tiền tố (<code>/api/expo-updates/import-update/</code>), 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.</p>
<p>Bạn không cần phải tin regex của mình; <code>cloudflared</code> sẽ cho bạn biết một URL khớp quy tắc nào:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cloudflared tunnel --config config.yml ingress rule https://ota.example.com/admin/</span></span></code></pre></div></div>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">https://ota.example.com/api/expo-updates/manifest/        service: http://api:8000
</span></span><span class="line"><span class="cl">https://ota.example.com/api/expo-updates/import-update/   service: http_status:404
</span></span><span class="line"><span class="cl">https://ota.example.com/admin/                            service: http_status:404
</span></span><span class="line"><span class="cl">https://ota.example.com/media/expo-updates/x.js           service: http://api:8000
</span></span><span class="line"><span class="cl">https://checkin-local.example.com/c/                      service: http://edge:8081
</span></span><span class="line"><span class="cl">https://checkin-local.example.com/health                  service: http_status:404</span></span></code></pre></div></div>
<p>Tô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.</p>
<p>Thêm hai nguyên tắc nữa:</p>
<ul>
<li><strong>Thứ bạn mở ra phải tự xác thực.</strong> 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 <a href="https://developers.cloudflare.com/cloudflare-one/policies/access/"  target="_blank" rel="noreferrer">Cloudflare Access</a> 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.</li>
<li><strong>Đừng để tunnel chạy ngầm như một phần quen thuộc của quy trình dev.</strong> Chạy nó có chủ đích, cho đúng tên miền bạn cần, và tắt khi xong việc. <code>docker compose stop cloudflared</code> gỡ mọi thứ khỏi Internet chỉ bằng một lệnh.</li>
</ul>

<h2 class="relative group">Cú redirect nuốt mất tên miền của tôi
    <div id="cú-redirect-nuốt-mất-tên-miền-của-tôi" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#c%c3%ba-redirect-nu%e1%bb%91t-m%e1%ba%a5t-t%c3%aan-mi%e1%bb%81n-c%e1%bb%a7a-t%c3%b4i" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>Tunnel đã kết nối, các quy tắc đã kiểm tra, tôi thử từ bên ngoài:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">https://ota.example.com/api/expo-updates/status/?…   301
</span></span><span class="line"><span class="cl">https://ota.example.com/api/expo-updates/manifest/   301
</span></span><span class="line"><span class="cl">https://ota.example.com/admin/                       301
</span></span><span class="line"><span class="cl">https://checkin-local.example.com/c/                 301</span></span></code></pre></div></div>
<p>Đườ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 <code>cloudflared</code>. Header <code>Location</code> ghi <code>https://www.example.com/</code>, và header <code>server: cloudflare</code> chốt lại: chính Cloudflare đang tự trả lời.</p>
<p>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, &ldquo;mọi subdomain khác → www&rdquo;, làm <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-09-09-scale-to-zero-504s/#the-spike-was-a-red-herring" >hố chặn cho các máy quét dò những subdomain bịa ra</a>. 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. <a href="https://jared.lynskey.co.nz/vi/posts/2026/2026-09-19-ephemeral-dev-environments-on-fly/" >Chính quy tắc đó đã tóm các môi trường preview của tôi</a> 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.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">… and not http.host in {&#34;ota.example.com&#34; &#34;checkin-local.example.com&#34;}</span></span></code></pre></div></div>
<p>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 <strong>Rules → Redirect Rules</strong>, Page Rules và mọi WAF custom rule của zone trước khi đi debug tunnel. <code>cloudflared</code> 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 đó.</p>

<h2 class="relative group">Những gì nó không làm được
    <div id="những-gì-nó-không-làm-được" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#nh%e1%bb%afng-g%c3%ac-n%c3%b3-kh%c3%b4ng-l%c3%a0m-%c4%91%c6%b0%e1%bb%a3c" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<ul>
<li><strong>UDP từ Internet công khai.</strong> 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.</li>
<li><strong>Uptime.</strong> Đâ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.</li>
<li><strong>Những thứ đã đóng cứng vào bản build.</strong> 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.</li>
</ul>

<h2 class="relative group">Chi phí: Cloudflare Tunnel có miễn phí không?
    <div id="chi-phí-cloudflare-tunnel-có-miễn-phí-không" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#chi-ph%c3%ad-cloudflare-tunnel-c%c3%b3-mi%e1%bb%85n-ph%c3%ad-kh%c3%b4ng" aria-label="Cọc dấu">#</a>
    </span>
    
</h2>
<p>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.</p>
<p>Thứ nó tiết kiệm cho bạn là giai đoạn giữa &ldquo;chạy được trên máy tôi&rdquo; và &ldquo;đáng bỏ tiền ra host&rdquo;. 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.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/self-host-behind-cloudflare-tunnel/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
