[{"data":1,"prerenderedAt":28},["ShallowReactive",2],{"$fgEXAapb-czduzn6Yo6d-JGHXV2nVDxLCpE8KJ2G84OU":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":12,"readingMinutes":13,"html":14,"related":15},"cloudflare-tunnel","Cloudflare Tunnel: Put a Server Online With No Open Ports","Cloudflare Tunnel checked November 2026: quick tunnels vs named ones, config.yml ingress rules, running cloudflared as a service, 1033 vs 502, and the limits.","2026-11-09",[9,10,11],"cloudflare","devops","networking",false,5,"\u003Cp>Cloudflare Tunnel lets you publish a web app from a machine that has no public IP and no open inbound ports. A small daemon, \u003Ccode>cloudflared\u003C/code>, runs next to your app and dials out to Cloudflare. Visitors hit your hostname on Cloudflare, and Cloudflare sends the request back down that outbound connection. Your firewall can drop every inbound packet and the site still works.\u003C/p>\n\u003Cp>It&#39;s available on every Cloudflare plan, the Free one included. I checked everything below on November 9, 2026 against Cloudflare&#39;s \u003Ca href=\"https://developers.cloudflare.com/tunnel/\">Tunnel docs\u003C/a>, the \u003Ca href=\"https://developers.cloudflare.com/tunnel/features/locally-managed-tunnels/configuration-file/\">configuration file reference\u003C/a>, the \u003Ca href=\"https://developers.cloudflare.com/tunnel/troubleshooting/\">troubleshooting page\u003C/a> and the \u003Ca href=\"https://developers.cloudflare.com/cloudflare-one/account-limits/\">Zero Trust account limits\u003C/a>.\u003C/p>\n\u003Ch2>How it actually connects\u003C/h2>\n\u003Cp>Each running \u003Ccode>cloudflared\u003C/code> keeps four long-lived connections open to two Cloudflare data centers. That&#39;s the redundancy you get for free. If you run a second copy of \u003Ccode>cloudflared\u003C/code> for the same tunnel (a replica), it opens four more. Cloudflare allows up to 25 replicas per tunnel, so 100 connections, and sends each visitor to the geographically closest one.\u003C/p>\n\u003Cp>One thing people assume and shouldn&#39;t: replicas are not a load balancer. There&#39;s no health-based steering between them. If you want failover between two different servers with checks, Cloudflare points you to its Load Balancing product instead.\u003C/p>\n\u003Cimg src=\"/blog/cloudflare-tunnel/tunnel-map.webp\" alt=\"Map of a Cloudflare Tunnel: cloudflared dials out to two Cloudflare data centers, and where errors 1033, 502 and 413 come from\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Ch2>Quick tunnels, for five minutes of sharing\u003C/h2>\n\u003Cp>If you just want to show someone what&#39;s running on your laptop, you don&#39;t need an account or a domain:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cloudflared tunnel --url http://localhost:8080\n\u003C/code>\u003C/pre>\n\u003Cp>It prints a random \u003Ccode>trycloudflare.com\u003C/code> URL that works until you stop the process. They&#39;re the right tool for testing webhooks against a local server, the kind of thing I wrote about in the \u003Ca href=\"/blog/stripe-webhooks\">Stripe webhooks post\u003C/a>, and for letting a client click through a build without deploying it anywhere.\u003C/p>\n\u003Cp>Newer \u003Ccode>cloudflared\u003C/code> versions can also gate a quick tunnel by email: add \u003Ccode>--allowed-mail client@example.com\u003C/code> (repeat it, or use \u003Ccode>&#39;*@example.com&#39;\u003C/code> for a whole domain) and visitors get a one-time PIN before they see anything. It needs a real browser, so it won&#39;t help with webhooks.\u003C/p>\n\u003Cp>Know the limits before you lean on one. Quick tunnels have no uptime guarantee, cap out at 200 in-flight requests (anything past that gets a \u003Ccode>429\u003C/code>), and don&#39;t support Server-Sent Events at all. If your app streams with SSE, a quick tunnel will look broken when it isn&#39;t.\u003C/p>\n\u003Ch2>Named tunnels, for anything that stays up\u003C/h2>\n\u003Cp>For a real hostname you create a tunnel in your account. There are two ways to manage it.\u003C/p>\n\u003Cp>\u003Cstrong>Remotely managed:\u003C/strong> you create the tunnel in the dashboard (Networking, then Tunnels), add public hostnames there, and run \u003Ccode>cloudflared\u003C/code> on the server with the token it gives you. The configuration lives in Cloudflare. This is the easier one, and I&#39;d use it for a single app on a single box.\u003C/p>\n\u003Cp>\u003Cstrong>Locally managed:\u003C/strong> you run \u003Ccode>cloudflared tunnel create &lt;name&gt;\u003C/code>, which writes a credentials JSON file, and you describe the routes in a \u003Ccode>config.yml\u003C/code>. I prefer this one on servers with several services, because the routing sits in a file I can read, diff and keep under version control (the routing, never the credentials file).\u003C/p>\n\u003Cp>A config for two services looks like this:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-yml\">tunnel: 6ff42ae2-765d-4adf-8112-31c55c1551ef\ncredentials-file: /home/deploy/.cloudflared/6ff42ae2-765d-4adf-8112-31c55c1551ef.json\n\ningress:\n  - hostname: app.example.com\n    service: http://localhost:3000\n  - hostname: api.example.com\n    service: http://localhost:8000\n  - service: http_status:404\n\u003C/code>\u003C/pre>\n\u003Cp>That last line is not optional. Any config with ingress rules must end in a catch-all rule, and \u003Ccode>http_status:404\u003C/code> is the sensible one. Rules are checked top to bottom, and the path is forwarded to your app untouched, so a rule matching \u003Ccode>/api\u003C/code> sends \u003Ccode>/api/users\u003C/code> as \u003Ccode>/api/users\u003C/code>. If you need the prefix stripped, do it with a Cloudflare rewrite rule or a local Nginx.\u003C/p>\n\u003Cp>Two commands I run before every restart:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">cloudflared tunnel ingress validate\ncloudflared tunnel ingress rule https://api.example.com\n\u003C/code>\u003C/pre>\n\u003Cp>The first checks the file. The second tells you which rule a URL would hit, which settles most &quot;why is this going to the wrong app&quot; arguments in about a second.\u003C/p>\n\u003Ch3>Running it as a service\u003C/h3>\n\u003Cp>On Linux, \u003Ccode>cloudflared service install\u003C/code> sets up a systemd unit. There&#39;s a small trap here. You&#39;ll run it with \u003Ccode>sudo\u003C/code>, and then \u003Ccode>$HOME\u003C/code> is \u003Ccode>/root\u003C/code>, so it can&#39;t find the \u003Ccode>config.yml\u003C/code> you made in your own home directory. Pass it explicitly:\u003C/p>\n\u003Cpre>\u003Ccode class=\"language-sh\">sudo cloudflared --config /home/deploy/.cloudflared/config.yml service install\nsystemctl start cloudflared\n\u003C/code>\u003C/pre>\n\u003Cp>After you change the config, \u003Ccode>systemctl restart cloudflared\u003C/code>. Long-lived connections like WebSockets drop on that restart. If that matters, Cloudflare&#39;s suggestion is to start a replica with the new config first, wait for it to connect, then stop the old one.\u003C/p>\n\u003Ch2>The errors, and what each one means\u003C/h2>\n\u003Cp>\u003Cstrong>1033\u003C/strong> means Cloudflare can&#39;t find a healthy \u003Ccode>cloudflared\u003C/code> for that tunnel. The connector isn&#39;t running, never ran, or the DNS record points at the wrong tunnel ID. Look at the server, not the app.\u003C/p>\n\u003Cp>\u003Cstrong>502 Bad Gateway\u003C/strong> is the opposite: the tunnel is connected, but \u003Ccode>cloudflared\u003C/code> can&#39;t reach your app. The app is down, the port is wrong, or the protocol is wrong (\u003Ccode>http://\u003C/code> in the rule when the app only speaks HTTPS, or the reverse).\u003C/p>\n\u003Cp>\u003Cstrong>ERR_TOO_MANY_REDIRECTS\u003C/strong> usually means your app redirects HTTP to HTTPS and the rule sends it plain HTTP. Every request gets the same redirect, forever. Either point the rule at the HTTPS port or stop the app from redirecting, since Cloudflare already terminates TLS for visitors.\u003C/p>\n\u003Cp>\u003Cstrong>x509: certificate signed by unknown authority\u003C/strong> happens when the app uses a self-signed cert. Set \u003Ccode>originServerName\u003C/code> or \u003Ccode>caPool\u003C/code> under \u003Ccode>originRequest\u003C/code>. \u003Ccode>noTLSVerify: true\u003C/code> works too, and I&#39;d only leave it on for as long as it takes to fix the certificate.\u003C/p>\n\u003Ch2>Limits that bite later\u003C/h2>\n\u003Cp>Uploads. Tunnel hostnames go through Cloudflare&#39;s proxy like any orange-clouded record, so the plan&#39;s request body limit applies: 100 MB on Free and Pro, 200 MB on Business. A user uploading a 300 MB video gets a \u003Ccode>413\u003C/code>, and no setting in \u003Ccode>cloudflared\u003C/code> changes that. Chunk the upload or send big files straight to object storage with a presigned URL; my \u003Ca href=\"/blog/s3-presigned-url\">S3 presigned URL post\u003C/a> has the pattern.\u003C/p>\n\u003Cp>Streaming. The tunnel buffers responses unless your app sends \u003Ccode>Content-Type: text/event-stream\u003C/code>. If tokens from an LLM arrive all at once at the end, that header is usually the reason.\u003C/p>\n\u003Cp>Workers. If a Worker route also matches the tunnel&#39;s hostname, the Worker gets the request first. Exclude the hostname or have the Worker pass the request through.\u003C/p>\n\u003Cp>And account size: 1,000 tunnels per account before you need Cloudflare&#39;s separate platform offering. Most of us will never notice.\u003C/p>\n\u003Cp>Last, lock down the box. The point of a tunnel is that nothing listens publicly, so bind the app to \u003Ccode>localhost\u003C/code>, close the inbound ports in the firewall, and check from outside that the server&#39;s IP really doesn&#39;t answer. If it still does, you have a second way in that Cloudflare never sees.\u003C/p>\n\u003Cp>If you want a server set up this way, or you&#39;ve inherited one that half uses a tunnel and half doesn&#39;t, \u003Ca href=\"https://calendly.com/director-norsky/meeting\">book a free call\u003C/a>. All consultation is free.\u003C/p>\n",[16,20,24],{"slug":17,"title":18,"description":19},"node-cron","node-cron in Production: Overlaps, Replicas and DST","node-cron 4 in Node.js, checked November 2026: noOverlap, running on one replica, the day-of-week AND rule, DST gaps, and when systemd or a queue is better.",{"slug":21,"title":22,"description":23},"nginx-vs-caddy","Nginx vs Caddy: Which Reverse Proxy I'd Pick in 2026","Nginx vs Caddy for a Node, Laravel or Nuxt app on one server: HTTPS, WebSockets, headers, the new nginx ACME module, and when each one wins.",{"slug":25,"title":26,"description":27},"pgbouncer","PgBouncer: What Transaction Mode Breaks, and the Fix","PgBouncer in transaction mode lets 1,000 clients share 20 Postgres connections. What it breaks (SET, LISTEN, advisory locks), and what 1.24 fixed.",1794181975300]