[{"data":1,"prerenderedAt":162},["ShallowReactive",2],{"seo-verification":3,"blog-cloudflare-tunnel-exposer-application-vps-en":6},{"google":4,"bing":5},"EycwPY2XMyTkVzas3n1ygeNJFGAH513qrMjfDljzsMQ","",{"id":7,"slug":8,"title":9,"excerpt":10,"readTime":11,"views":12,"isPinned":13,"publishedAt":14,"category":15,"categories":20,"featuredImage":22,"bgImage":23,"posterImage":24,"relatedSolution":22,"intro":25,"sections":26,"ctaTitle":118,"ctaBody":119,"ctaButton":120,"ctaUrl":121,"relatedPosts":122},248,"cloudflare-tunnel-exposer-application-vps","Cloudflare Tunnel: expose a VPS app without opening ports","Expose an application on your VPS via a Cloudflare Tunnel without opening any inbound port. Full guide with cloudflared, Docker Compose and a systemd service.",10,0,false,"2026-08-12T00:00:00+00:00",{"id":16,"name":17,"slug":18,"color":19,"icon":18},3,"Deployment","deploiement","bg-success\u002F10 text-success",[21],{"id":16,"name":17,"slug":18,"color":19,"icon":18},null,"\u002Fblog\u002Fcovers\u002Fbg.svg","\u002Fblog\u002Fcovers\u002Fcloudflare-tunnel-exposer-application-vps-poster.svg","You have just deployed an application on your VPS and find yourself stuck: dynamic IP, a corporate firewall blocking inbound port 443, or simply a refusal to expose your server directly to the Internet. A Cloudflare Tunnel solves this by reversing the connection: it is `cloudflared` that calls Cloudflare, not the other way around. The result is your application reachable over HTTPS on your domain, without opening any inbound port and without touching your DNS configuration.",[27,31,42,45,70,73,77,80,112,115],{"type":28,"title":29,"body":30},"h2","Why a Cloudflare Tunnel instead of opening a port","The classic setup — opening ports 80 and 443 on the firewall, pointing a DNS record to the server IP, installing a reverse proxy — works well when you control the network. But three situations break it: an ISP or corporate network that blocks inbound connections on port 443, a dynamic IP that invalidates your DNS records every 24 hours, or a VPS behind a strict NAT that allows no port forwarding.\n\nCloudflare Tunnel bypasses all three with the same mechanism: the `cloudflared` daemon establishes a persistent outbound connection to Cloudflare's points of presence. Your server accepts nothing; Cloudflare receives HTTPS requests and forwards them through this encrypted tunnel. Inbound traffic never reaches your server directly.",{"type":32,"title":33,"items":34},"ul","What Cloudflare Tunnel brings in practice",[35,36,37,38,39,40,41],"**Zero open ports** — the VPS firewall can block all inbound traffic (80 and 443 included) without affecting application accessibility.","**Automatic HTTPS** — Cloudflare manages the client-side TLS certificate: no Let's Encrypt to configure, no renewal to monitor.","**NAT and dynamic IP transparent** — the outbound connection from `cloudflared` traverses any NAT; the server IP can change without reconfiguring DNS.","**Corporate network or restrictive ISP** — if inbound port 443 is blocked, the tunnel keeps working because it relies on outbound HTTP\u002F2 or QUIC connections.","**Optional Zero Trust integration** — tunnels pair with Cloudflare Access to restrict access to authenticated users, without a VPN.","**Cloudflare protection included** — traffic passes through the Cloudflare network: DDoS mitigation, WAF and rate limiting apply without extra configuration.","**Free plan available** — a simple tunnel without load-balancing is usable without a paid Cloudflare subscription.",{"type":28,"title":43,"body":44},"Prerequisites","To follow this guide you need:\n\n**VPS with root access.** Installing `cloudflared` as a systemd service — the only way to guarantee automatic restart — requires root privileges. Shared hosting or an instance without root access does not allow this setup.\n\n**Minimum resources.** `cloudflared` uses less than 50 MB of RAM and negligible CPU. Count 1 vCPU and 512 MB of RAM as the strict minimum for the daemon alone; the real constraint comes from the application you are exposing.\n\n**A domain managed by Cloudflare.** The domain must be registered or transferred to Cloudflare (or NS delegation to Cloudflare). Without an active Cloudflare zone, a named tunnel cannot create DNS records automatically.\n\n**Docker Engine** (if you use the Docker Compose variant in this guide). Available on Ubuntu 22.04\u002F24.04, Debian 12 and RHEL-compatible distributions.\n\n**A free Cloudflare account.** No paid subscription is required for a single tunnel without load-balancing.",{"type":46,"title":47,"steps":48},"steps","Install and configure cloudflared on the VPS",[49,52,55,58,61,64,67],{"title":50,"body":51},"Install cloudflared via the Cloudflare repository","Cloudflare publishes `cloudflared` as `.deb` \u002F `.rpm` packages and as a static binary. To install via APT on Debian\u002FUbuntu:\n\n```bash\ncurl -fsSL https:\u002F\u002Fpkg.cloudflare.com\u002Fcloudflare-main.gpg | sudo tee \u002Fusr\u002Fshare\u002Fkeyrings\u002Fcloudflare-main.gpg > \u002Fdev\u002Fnull\necho \"deb [signed-by=\u002Fusr\u002Fshare\u002Fkeyrings\u002Fcloudflare-main.gpg] https:\u002F\u002Fpkg.cloudflare.com\u002Fcloudflared $(lsb_release -cs) main\" | sudo tee \u002Fetc\u002Fapt\u002Fsources.list.d\u002Fcloudflared.list\nsudo apt update && sudo apt install cloudflared\n```\n\nVerify the installation:\n\n```bash\ncloudflared --version\n```\n\nThe command should return a line such as `cloudflared version 2025.x.x (built ...)`. The exact version depends on the time of installation; refer to the \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fcloudflare\u002Fcloudflared\u002Freleases\">official cloudflare\u002Fcloudflared repository\u003C\u002Fa> for the current number.",{"title":53,"body":54},"Authenticate cloudflared with Cloudflare","On the VPS (or locally if you have graphical access), run:\n\n```bash\ncloudflared tunnel login\n```\n\nA link appears in the terminal. Open it in a browser, select the Cloudflare zone to authorize, then confirm. A certificate `~\u002F.cloudflared\u002Fcert.pem` is created on the machine.",{"title":56,"body":57},"Create a named tunnel","Create a tunnel with a descriptive name:\n\n```bash\ncloudflared tunnel create my-tunnel\n```\n\nCloudflare generates a UUID identifier and a credentials file `~\u002F.cloudflared\u002F\u003CUUID>.json`. Note the UUID; you will need it in the following steps.",{"title":59,"body":60},"Write the config.yml configuration file","Create `\u002Fetc\u002Fcloudflared\u002Fconfig.yml`:\n\n```bash\nsudo mkdir -p \u002Fetc\u002Fcloudflared\n```\n\nFile content (adapt `\u003CUUID>`, `your-domain.com` and your application port):\n\n```yaml\ntunnel: \u003CUUID>\ncredentials-file: \u002Fhome\u002F\u003Cuser>\u002F.cloudflared\u002F\u003CUUID>.json\n\ningress:\n  - hostname: app.your-domain.com\n    service: http:\u002F\u002Flocalhost:3000\n  - service: http_status:404\n```\n\nThe last rule — `service: http_status:404` without `hostname` — is **mandatory**: it acts as a catch-all rule. Without it, `cloudflared` refuses to start and returns the error `\"You must specify an ingress rule that matches all incoming requests\"`.",{"title":62,"body":63},"Create the DNS record and start the tunnel","Automatically register the subdomain in your Cloudflare zone:\n\n```bash\ncloudflared tunnel route dns my-tunnel app.your-domain.com\n```\n\nThen test the tunnel in foreground mode to validate the configuration:\n\n```bash\ncloudflared tunnel run my-tunnel\n```\n\nOpen `https:\u002F\u002Fapp.your-domain.com` in a browser. If the application responds, stop the process (`Ctrl+C`) and proceed to the next step.",{"title":65,"body":66},"Install cloudflared as a systemd service","To make the tunnel restart automatically on reboot, install it as a system daemon:\n\n```bash\nsudo cloudflared service install\nsudo systemctl enable cloudflared\nsudo systemctl start cloudflared\nsudo systemctl status cloudflared\n```\n\nThe systemd unit file created by Cloudflare is located at `\u002Fetc\u002Fsystemd\u002Fsystem\u002Fcloudflared.service`. Its content looks like:\n\n```ini\n[Unit]\nDescription=cloudflared\nAfter=network.target\n\n[Service]\nTimeoutStartSec=0\nType=notify\nExecStart=\u002Fusr\u002Fbin\u002Fcloudflared --no-autoupdate tunnel run\nRestart=on-failure\nRestartSec=5s\n\n[Install]\nWantedBy=multi-user.target\n```\n\nWith this service active, the tunnel is operational from VPS startup, without manual intervention.",{"title":68,"body":69},"Integrate cloudflared into an existing Docker Compose stack","If your application already runs in a Docker Compose stack, add a `cloudflared` service to the same file. The token approach (without a credentials file) is simplest for a container:\n\n```yaml\nservices:\n  app:\n    image: my-image\n    networks:\n      - internal\n\n  cloudflared:\n    image: cloudflare\u002Fcloudflared:latest\n    command: tunnel --no-autoupdate run\n    environment:\n      - TUNNEL_TOKEN=${TUNNEL_TOKEN}\n    networks:\n      - internal\n    restart: unless-stopped\n\nnetworks:\n  internal:\n```\n\nDefine `TUNNEL_TOKEN` in a `.env` file at the same level. The token is retrieved from the Cloudflare dashboard → Zero Trust → Networks → Tunnels → your tunnel → Configure → Docker connectors. The `cloudflared` service and your application share the `internal` network; point to the application by its Docker service name (`http:\u002F\u002Fapp:3000` instead of `http:\u002F\u002Flocalhost:3000`).",{"type":28,"title":71,"body":72},"Post-installation configuration","Once the tunnel is running, a few additional settings improve the robustness of the setup.\n\n**Retrieving the real client IP.** By default, your application receives requests from `127.0.0.1` or from the tunnel's internal IP. To get the real visitor IP, read the `CF-Connecting-IP` header that Cloudflare injects automatically. Configure your application or local reverse proxy to trust this header.\n\n**End-to-end encryption.** The tunnel encrypts the connection between `cloudflared` and Cloudflare. The connection between `cloudflared` and your local application is plain HTTP by default (loopback or internal Docker network). If your application exposes HTTPS locally, add `originServerName: app.your-domain.com` in the corresponding ingress rule so that `cloudflared` validates the certificate.\n\n**Multiple services, one tunnel.** A tunnel can expose multiple services on distinct subdomains: simply add additional entries to the `ingress` block in `config.yml`, before the catch-all rule.",{"type":74,"title":75,"body":76},"tip","Hardening: close inbound ports 80 and 443","The main advantage of this architecture is being able to close all inbound ports on the VPS. Once the tunnel is validated, apply these UFW rules:\n\n```bash\nsudo ufw default deny incoming\nsudo ufw default allow outgoing\nsudo ufw allow ssh\nsudo ufw enable\n```\n\nYour application remains accessible via the Cloudflare tunnel (which relies on outbound connections), and the SSH port stays open for administration. No direct connection on 80 or 443 reaches the server anymore.",{"type":28,"title":78,"body":79},"Cloudflare Tunnel vs Nginx \u002F Traefik: complementary approaches","A common objection: \"I already have Nginx and Traefik doing this job, why add a Cloudflare layer?\" The answer is that the two approaches do not solve the same problem.\n\nA local reverse proxy (Nginx, Traefik, Caddy) handles routing between services on the same network and SSL renewal — but it assumes that inbound traffic reaches the server. If port 443 is blocked by the upstream network, the reverse proxy is useless.\n\nCloudflare Tunnel solves exactly what a local reverse proxy cannot: traffic reaches Cloudflare regardless of the server's network connectivity. The two are complementary: you can easily place Traefik behind the tunnel for internal routing, while letting Cloudflare handle public TLS.",{"type":81,"title":82,"headers":83,"rows":87},"comparison","Cloudflare Tunnel vs local reverse proxy",[84,85,86],"Criterion","Cloudflare Tunnel","Local reverse proxy (Nginx\u002FTraefik)",[88,92,96,100,104,108],[89,90,91],"Inbound port required","No — outbound connection only","Yes — 80\u002F443 must be reachable",[93,94,95],"Public TLS","Managed by Cloudflare, automatic","Let's Encrypt via ACME (certbot, Traefik…)",[97,98,99],"Dynamic IP \u002F strict NAT","Transparent — no DNS update needed","Problematic — requires DynDNS or a fixed IP",[101,102,103],"Load balancing","Paid plan (Cloudflare Load Balancing)","Available natively (Traefik, Nginx upstream)",[105,106,107],"Latency","Slightly higher (routing via Cloudflare POP)","Minimal — direct traffic to the server",[109,110,111],"External dependency","Yes — Cloudflare must be reachable","No — works without a third party",{"type":28,"title":113,"body":114},"Troubleshooting: common error messages","**`You must specify an ingress rule that matches all incoming requests`**\nThe catch-all rule is missing or misplaced in `config.yml`. It must be the last entry in the `ingress` block, without `hostname`, with `service: http_status:404`.\n\n**`Unable to locate config file in default locations`**\n`cloudflared` looks for its configuration in `~\u002F.cloudflared\u002Fconfig.yml` or `\u002Fetc\u002Fcloudflared\u002Fconfig.yml`. Specify the path explicitly with `cloudflared tunnel --config \u002Fetc\u002Fcloudflared\u002Fconfig.yml run my-tunnel`.\n\n**`ERR connection to origin timed out` in the logs**\nThe target application is not reachable from `cloudflared`. Verify that the local service is running (`curl http:\u002F\u002Flocalhost:3000`) and that the port in `config.yml` matches. In a Docker Compose context, use the service name (`http:\u002F\u002Fapp:3000`) rather than `localhost`.\n\n**Expired token: `tunnel credentials file not found` or `token is expired`**\nTokens generated through the Cloudflare interface have a limited lifetime if the connector was never registered. Regenerate the token from Zero Trust → Networks → Tunnels → Configure → Connectors, then update the `TUNNEL_TOKEN` variable in your `.env`.\n\n**Free plan limitations: load-balancing and SSH via tunnel**\nThe free plan does not support load-balancing between multiple origins. SSH access via tunnel (`cloudflared access ssh`) on the free plan requires a specific Cloudflare Access configuration and is not enabled by default. The number of connectors per tunnel is limited to a few instances on the free plan.",{"type":28,"title":116,"body":117},"Cloudflare Tunnel as a reference architecture on VPS","Cloudflare Tunnel illustrates well what root access on a VPS makes possible: installing `cloudflared` as a system daemon, modifying firewall rules, managing systemd services. On shared hosting without root access, none of these steps is feasible — the daemon cannot be installed, the firewall is not under your control, and the service cannot be configured to start at boot.\n\nThis architecture is particularly suited to situations where network connectivity is constrained or uncertain: development labs, offices with strict corporate firewalls, edge servers, or simply a refusal to expose a public IP. It pairs naturally with local reverse proxies such as Traefik or Nginx Proxy Manager for internal routing, and with system hardening to close direct attack surfaces.","A VPS with root access to install cloudflared","The `cloudflared` daemon and its systemd service require root access — something shared hosting does not provide. A ServOrbit VPS gives you full control: OS choice, root access, dedicated IPv4, and Marketplace templates to get started quickly.","See Cloud VPS plans","\u002Fsolutions\u002Fdeveloppeurs",[123,134,149],{"id":124,"slug":125,"title":126,"excerpt":127,"readTime":11,"views":128,"isPinned":13,"publishedAt":129,"category":130,"categories":131,"featuredImage":22,"bgImage":23,"posterImage":133,"relatedSolution":22},37,"deployer-avec-nginx-proxy-manager","Deploy your applications with Nginx Proxy Manager on a VPS","Manage domains, SSL and reverse proxy for several services on a VPS with Nginx Proxy Manager: installation, Docker networks, 502 troubleshooting and configuration.",369,"2026-05-14T00:00:00+00:00",{"id":16,"name":17,"slug":18,"color":19,"icon":18},[132],{"id":16,"name":17,"slug":18,"color":19,"icon":18},"\u002Fblog\u002Fcovers\u002Fdeployer-avec-nginx-proxy-manager-poster.svg",{"id":135,"slug":136,"title":137,"excerpt":138,"readTime":11,"views":12,"isPinned":13,"publishedAt":139,"category":140,"categories":146,"featuredImage":22,"bgImage":23,"posterImage":148,"relatedSolution":22},228,"durcissement-serveur-linux-initial","Initial Linux Server Hardening","Create a sudo user, configure SSH with keys, enable UFW and fail2ban on Ubuntu 22.04 or Debian 12 in under an hour.","2026-08-06T00:00:00+00:00",{"id":141,"name":142,"slug":143,"color":144,"icon":145},8,"Security & Monitoring","securite-monitoring","bg-rose-500\u002F10 text-rose-400","security",[147],{"id":141,"name":142,"slug":143,"color":144,"icon":145},"\u002Fblog\u002Fcovers\u002Fdurcissement-serveur-linux-initial-poster.svg",{"id":150,"slug":151,"title":152,"excerpt":153,"readTime":11,"views":154,"isPinned":13,"publishedAt":155,"category":156,"categories":157,"featuredImage":22,"bgImage":23,"posterImage":159,"relatedSolution":160},41,"deployer-avec-coolify","Deploy Your Applications with Coolify on a VPS","Deploy Coolify on a VPS: installation, HTTPS configuration, Git connection, deploying your first application, databases, backups, and troubleshooting.",1640,"2026-05-10T00:00:00+00:00",{"id":16,"name":17,"slug":18,"color":19,"icon":18},[158],{"id":16,"name":17,"slug":18,"color":19,"icon":18},"\u002Fblog\u002Fcovers\u002Fdeployer-avec-coolify-poster.svg",{"categorySlug":18,"appSlug":161},"coolify",1786536086643]