Composer bin that wraps cloudflared so any local HTTP server gets a public HTTPS URL in one command.
vendor/bin/cloudflare-tunnel- Features
- Requirements
- Installation
- Configuration
- Testing
- Why
- How it works
- Use cases
- Laravel notes
- License
- Composer
binentry point —vendor/bin/cloudflare-tunnelafter arequire-devinstall - Quick tunnels by default — zero-setup
https://….trycloudflare.comvia Cloudflare’s trycloudflare flow - Named tunnels — set
TUNNEL_NAME(orCLOUDFLARE_TUNNEL_NAME) to run a tunnel from~/.cloudflared/config.yml(orCLOUDFLARED_CONFIG) - Local URL resolution — env → CLI → auto-detect → default
:8000, with a printed reason for the choice - Auto-detect — reads local
APP_URLfrom./.envwhen it ishttp://127.0.0.1:PORT/localhost:PORT, else probes listening ports 8000, 8080, 3000, 80 (lsofornc) - Extra
cloudflaredargs — optionalCLOUDFLARED_EXTRA_ARGS(space-separated) - Foreground helper UX — streams
cloudflaredoutput, highlights the public URL when found, and reminds you to setAPP_URL - Framework-agnostic — Laravel, Node, static files, or anything listening on HTTP (not a Laravel package; no service provider)
cloudflaredonPATH- Bash 3.2+ (macOS system Bash is fine)
- Composer (to install the package and expose
vendor/bin/cloudflare-tunnel) - PHP ≥ 8.0 (Composer package constraint only; the binary is Bash)
composer require --dev solution-forest/cloudflare-tunnel:^1.0Or in composer.json:
{
"require-dev": {
"solution-forest/cloudflare-tunnel": "^1.0"
}
}composer update solution-forest/cloudflare-tunnelNo repositories entry is required — the package is on Packagist.
Alternatives (path / VCS)
Path (local package development):
{
"repositories": [
{
"type": "path",
"url": "packages/cloudflare-tunnel",
"options": { "symlink": true }
}
],
"require-dev": {
"solution-forest/cloudflare-tunnel": "*"
}
}VCS (without Packagist):
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/solutionforest/cloudflare-tunnel"
}
],
"require-dev": {
"solution-forest/cloudflare-tunnel": "^1.0"
}
}brew install cloudflare/cloudflare/cloudflaredOther platforms: Cloudflare downloads.
Start your local app, then:
vendor/bin/cloudflare-tunnelOptional project script (this package does not ship Composer scripts; add them in the consuming app if you want):
{
"scripts": {
"tunnel": [
"Composer\\Config::disableProcessTimeout",
"vendor/bin/cloudflare-tunnel"
]
}
}composer run tunnelWhen the public URL appears, set your app’s public base URL to that HTTPS origin (e.g. APP_URL), restart the local server, and clear any config cache.
| Flag | Description |
|---|---|
--url=URL |
Local origin to expose (e.g. http://127.0.0.1:9000) |
--port=PORT |
Local port; host from HOST or 127.0.0.1 |
-h, --help |
Show help |
vendor/bin/cloudflare-tunnel
vendor/bin/cloudflare-tunnel --port=9000
vendor/bin/cloudflare-tunnel --url=http://127.0.0.1:9000| Variable | Default | Purpose |
|---|---|---|
TUNNEL_LOCAL_URL |
(auto / default) | Full local origin (highest priority) |
HOST / PORT |
127.0.0.1 / (unset) |
Used when TUNNEL_LOCAL_URL / --url unset; PORT must be set to take this path |
TUNNEL_NAME |
(empty) | Named tunnel; alias CLOUDFLARE_TUNNEL_NAME |
CLOUDFLARED_CONFIG |
~/.cloudflared/config.yml |
Named-tunnel config path |
CLOUDFLARED_EXTRA_ARGS |
(empty) | Extra args passed to cloudflared (space-separated) |
TUNNEL_LOCAL_URL=http://127.0.0.1:8000 vendor/bin/cloudflare-tunnel
TUNNEL_NAME=my-app vendor/bin/cloudflare-tunnelThe script prints which URL was chosen and why.
| Priority | Source | Notes |
|---|---|---|
| 1 | TUNNEL_LOCAL_URL |
Full URL |
| 2 | --url= |
CLI full URL |
| 3 | PORT / --port= |
Host from HOST or 127.0.0.1 |
| 4 | Auto-detect | Heuristic below |
| 5 | Default | http://127.0.0.1:8000 |
Auto-detect:
- If
./.envhasAPP_URL=http://127.0.0.1:PORTorhttp://localhost:PORT(explicit port), use that host/port. - Else probe listening ports via
lsof(ornc): 8000, 8080, 3000, 80. - Otherwise fall back to
http://127.0.0.1:8000.
Port 5173 alone is treated as Vite, not the app — auto-detect does not prefer it as the tunnel target.
| Mode | How | Notes |
|---|---|---|
| Quick (default) | No TUNNEL_NAME |
Random https://….trycloudflare.com; fine for short-lived OAuth/webhook labs |
| Named | TUNNEL_NAME=… |
Requires config at CLOUDFLARED_CONFIG; DNS/ingress must match your public hostname and local target |
This package ships no automated test suite and no dry-run / mock mode for cloudflared. Verify changes with the checks below.
From the package root:
bash -n bin/cloudflare-tunnelExit 0 means the script parses under Bash.
bin/cloudflare-tunnel --help
# With cloudflared temporarily off PATH, expect a clear install error:
PATH=/usr/bin:/bin bin/cloudflare-tunnel- Start a local HTTP server on a known port (e.g.
php -S 127.0.0.1:8000orphp artisan serve). - In another terminal:
vendor/bin/cloudflare-tunnel(orbin/cloudflare-tunnel --port=8000from the package tree). - Wait for a
https://….trycloudflare.comURL in the output. - Open that URL in a browser and confirm it reaches the local app.
- Stop with Ctrl+C.
The package composer.json defines no scripts. Consuming projects may add a tunnel script (see Installation) and run composer run tunnel as a convenience wrapper — that is project-local, not part of this package’s release verification.
Localhost is enough for most day-to-day work. It is not enough when something on the public internet must call you back:
- OAuth providers that require an
https://redirect URI - Webhooks from Stripe, GitHub, Slack, and similar services
- Sharing a WIP page with a teammate or testing on a phone
Alternatives exist. Herd Expose (herd share) is polished for Laravel Herd + Vite. ngrok is widely known and feature-rich. This package is a thin, Composer-installable wrapper around Cloudflare’s free quick tunnel (and optional named tunnels) — useful when you already have (or prefer) cloudflared, want a vendor/bin entry point, and do not need Herd or an ngrok account for the job.
It does not replace those tools; it is another path with the same job: local app → public HTTPS.
flowchart LR
subgraph local [Your machine]
App["Local HTTP app<br/>e.g. :8000 / :3000"]
Bin["vendor/bin/cloudflare-tunnel"]
Cf["cloudflared"]
end
subgraph edge [Cloudflare]
Quick["trycloudflare.com<br/>quick tunnel"]
Named["Named hostname<br/>TUNNEL_NAME + config.yml"]
end
Browser["Browser / OAuth / webhook"]
App -->|"HTTP to local URL"| Cf
Bin -->|"resolves local URL<br/>then starts"| Cf
Cf -->|"outbound tunnel"| Quick
Cf -->|"outbound tunnel"| Named
Quick -->|"HTTPS"| Browser
Named -->|"HTTPS"| Browser
- The script resolves a local origin (env, CLI flags, auto-detect, or default
:8000). - With no
TUNNEL_NAME, it runs a quick tunnel and prints ahttps://….trycloudflare.comURL. - With
TUNNEL_NAMEset, it runs that named tunnel from~/.cloudflared/config.yml(orCLOUDFLARED_CONFIG). - You point OAuth redirect URIs / webhooks /
APP_URLat the public HTTPS origin — nothttp://localhost.
| Scenario | Why a tunnel helps |
|---|---|
| OAuth callbacks | Providers reject http://localhost or require HTTPS |
| Webhooks | External services POST to a reachable URL |
| Share WIP | Teammate opens your branch without VPN |
| Mobile testing | Phone hits the same app over HTTPS |
| Laravel + Google OAuth | Lab Connect flows need a stable-enough HTTPS APP_URL |
| Non-Laravel (Node, etc.) | Same bin; no framework coupling |
This package is not Laravel-specific. When you use it with Laravel:
composer run dev / php artisan dev typically serves the app and Vite on localhost:5173. The tunnel can reach your app origin, but the browser on https://….trycloudflare.com still tries to load assets from http://localhost:5173 → mixed content / broken CSS & JS.
- Build assets:
npm run build - Serve the app only:
php artisan serve(or Herd’s PHP site) - In another terminal:
vendor/bin/cloudflare-tunnel(orcomposer run tunnel) - Set
APP_URLto the printed public HTTPS URL, thenphp artisan config:clearand restart the app server - Point OAuth redirect URIs at
${APP_URL}/…
Do not fold the tunnel into default composer run dev: quick tunnels mint a new hostname every start (breaking APP_URL / redirect URIs), and public exposure should stay opt-in.
Herd users may prefer herd share for Vite-aware public sharing instead of (or alongside) this helper.
MIT © Solution Forest