The Laravel & PHP Ecosystem: Expose vs. ngrok

Current comparison
Looking for the main ngrok alternative guide?
We keep the latest ngrok alternative comparison, CLI commands, pricing notes, and webhook examples on one canonical page.
Open the InstaTunnel ngrok alternative guideQuick answer
Expose vs ngrok: Self-Hosting a PHP Reverse Proxy for Larave: quick answer
If free tunnel limits interrupt your workflow, compare session length, stable URLs, concurrent tunnels, and paid-plan pricing before choosing a localhost tunnel tool.
What free tunnel limits should developers check first?
Check session duration, URL stability, concurrent tunnels, custom subdomains, bandwidth or request limits, and whether webhook callbacks survive restarts.
How does InstaTunnel handle longer development sessions?
InstaTunnel Free is designed around 24-hour sessions, with Pro available for higher limits and MCP endpoint tunnel workflows.
As a modern PHP or Laravel developer, local development is a well-oiled machine. You likely rely on tools like Laravel Herd, Valet, or Sail to serve your applications on a .test domain. But what happens when you need to share that local environment with the outside world — demoing a work-in-progress feature to a client, testing webhooks from Stripe or GitHub, or previewing a responsive layout on a real phone? You need a tunnel.
For over a decade, the default answer has been ngrok. It’s robust and works with any stack, but its free tier comes with trade-offs that can disrupt a workflow. One alternative worth knowing about is Expose, an open-source, PHP-native tunneling tool built by Marcel Pociot at Beyond Code — the team behind Laravel Herd and Tinkerwell.
This guide compares the two tools as they actually work today, then walks through self-hosting your own Expose server on a budget VPS.
The ngrok free tier, accurately described
ngrok has grown well past a simple tunneling utility into a full ingress platform. Its free plan is more generous than a lot of comparison content suggests, but it does have real limits, and a couple of commonly repeated claims about it don’t hold up.
What’s actually capped on the free plan, per ngrok’s current documentation:
| Resource | Free tier limit |
|---|---|
| Data transfer out | 1 GB/month |
| HTTP/S requests | 20,000/month |
| Online endpoints | Up to 3 |
| Concurrent agents | 3 |
| TCP connections | 5,000/month |
| HTTP request rate | 4,000/minute |
| TCP connection rate | 100/minute |
| Custom/branded domains | Not available on Free (Hobbyist adds ngrok-branded domains; bring-your-own-domain requires Pay-as-you-go) |
Two points worth being precise about, since they’re the most common source of confusion:
- There’s no session timeout on the free tier. ngrok’s docs state this explicitly — free endpoints can run indefinitely as a background service. There’s no “restart every few hours” requirement.
- You don’t have to get a new random subdomain every time. Since 2023, every ngrok account — including free ones — can claim one free static domain (something like
panda-new-kit.ngrok-free.app) that stays yours permanently and doesn’t change on restart. It’s not a fully custom/branded domain (that’s still paid), but it does solve the “I have to update my Stripe webhook URL every time I restart the tunnel” problem, as long as you’ve claimed it. Without claiming a static domain, you do still get a fresh random one on each connection. - Free-tier HTTP/S traffic does get an interstitial “click through to continue” warning page in the browser, mainly to deter phishing abuse. It doesn’t affect programmatic requests (like an actual webhook POST from Stripe), and it can be bypassed even on the free tier by sending a custom
User-Agentor thengrok-skip-browser-warningheader.
Above Free, current plans are Hobbyist ($8–10/month depending on annual vs. monthly billing, ngrok-branded domains, no interstitial), Pay-as-you-go ($20/month base plus metered usage, unlimited endpoints, bring-your-own-domain), and Enterprise (custom pricing, SSO/SCIM/compliance).
Expose: a PHP-native, open-source alternative
Expose shares HTTP/HTTPS traffic only — it’s not a generic TCP/UDP transport the way ngrok is, so it won’t tunnel a raw database connection or an SSH session. In exchange, it’s scoped tightly to the web development workflow and its core is fully open source (MIT-licensed) and written in PHP, so if something breaks, you can actually read the code that’s running.
As of this writing the project is active: the exposedev/expose repository has around 4,500+ stars and 300+ forks on GitHub, with its latest release (3.2.2) shipped in March 2026. The current client requires PHP 8.2 or newer — this is a hard requirement in the package’s composer.json, not just a recommendation, so older “PHP 7.4+” guidance floating around online is out of date.
Installing the client
Three supported methods:
Laravel Herd — if you already use Herd, Expose ships with it; just set your token in Herd’s settings.
PHP Archive (PHAR) — the primary documented method:
curl https://github.com/exposedev/expose/raw/master/builds/expose -L --output expose
chmod +x expose
sudo mv expose /usr/local/bin/expose
Via Composer:
composer global require exposedev/expose
(Note: the package was historically published as beyondcode/expose; that name still resolves on Packagist, but current docs use exposedev/expose.) Make sure your global Composer bin directory is on your PATH.
Sharing a site
expose token YOUR_TOKEN
Then, from a .test project directory (Herd/Valet convention), just run expose with no arguments, or share an explicit URL:
expose share http://localhost:8000
A local request dashboard at http://127.0.0.1:4040 lets you inspect and replay incoming requests, similar in spirit to ngrok’s inspector.
One correction to a common claim: custom subdomains are not available on Expose’s free hosted tier. Passing --subdomain requires either an Expose Pro/Team account or your own self-hosted server:
expose share my-site.test --subdomain=my-site
On the free sharedwithexpose.com server (hosted by Beyond Code in Germany), every connection gets a random subdomain and connections are time-limited. This is exactly the pain point that makes self-hosting worth it if you want stable, custom URLs without paying per seat.
Current Expose pricing (expose.dev)
| Tier | Price | Includes |
|---|---|---|
| Hobby | Free | TLS/SSL, time-limited connections, random URLs, single EU server |
| Pro | $79/user/year | No time limit, persistent URLs, custom domains and subdomains, global server network |
| Team | $229/team/year (up to 10 users) | Everything in Pro, plus priority support |
Laravel Valet and Herd integration
Laravel Valet (Taylor Otwell’s separate, community-maintained local dev tool) supports three share tools as of current versions: ngrok, Expose, and Cloudflare’s cloudflared — selected via valet share-tool ngrok|expose|cloudflared. Valet will prompt to install whichever you pick (Homebrew for ngrok/cloudflared, Composer for Expose) and then valet share uses it.
Laravel Herd — Beyond Code’s own local environment — has Expose built in natively, configurable straight from Herd’s settings UI, with no separate install step.
Feature comparison
| ngrok (Free/Hobbyist) | Expose (Hobby/Pro) | |
|---|---|---|
| Primary use case | Generic network ingress; enterprise API/K8s | Web dev local environments, HTTP/S only |
| Language | Go, closed-source client/server | PHP, open source (MIT) |
| Protocols | HTTP/S, TCP, TLS | HTTP/S only |
| Free-tier stable URL | Yes, if you claim your free static domain | No — random subdomain every connection |
| Custom subdomains | Paid only | Paid (Pro) or free via self-hosting |
| Request inspector | Yes (localhost:4040-style dashboard) |
Yes |
| Self-hosting | Not applicable (managed only) | Yes, fully documented and open source |
Self-hosting Expose on a budget VPS
If you’re PHP/Laravel-centric and want stable custom subdomains without an annual per-seat fee, self-hosting is the real differentiator. Here’s the current, correct process — note that this differs in a few important ways from older guides (including earlier drafts of this piece): the self-hosted server is now a separate package from the client, with its own binary and its own repository.
Step 1: Provision the server and configure DNS
A basic DigitalOcean Droplet now starts at $4/month (512 MiB RAM, 1 shared vCPU, 10 GiB SSD, 500 GiB transfer) — plenty for a personal or small-team Expose server. Ubuntu 24.04 LTS is a solid choice.
Add two DNS A records pointing at your Droplet’s IP:
- expose (or whatever subdomain you want as the base, e.g. expose.yourdomain.com)
- *.expose (the wildcard, so every tunnel subdomain resolves)
Step 2: Install PHP and Composer
Expose’s server package requires PHP 8.2+:
sudo apt update
sudo apt install php-cli php-curl php-mbstring php-xml php-zip php-sqlite3 unzip git -y
curl -sS https://getcomposer.org/installer -o composer-setup.php
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
Step 3: Install the Expose server (not the client)
The self-hosted server lives in its own repository, exposedev/server, separate from the exposedev/expose client package. Clone it and install dependencies:
git clone https://github.com/exposedev/server.git
cd server
composer install
The open-source server core gives you the tunneling infrastructure itself. It does not include the hosted Expose Pro dashboard for team/billing management — that’s specific to the managed expose.dev platform.
Step 4: Start the server
php expose-server serve expose.yourdomain.com
This listens on port 8080 by default. You can change the port with --port=3000, and restrict connections to holders of a valid auth token with --validateAuthTokens (tokens are then issued/managed through the server’s admin interface).
Step 5: Put Nginx (or Apache) in front for TLS
The Expose server doesn’t terminate SSL itself — you need a reverse proxy. Obtain a wildcard certificate first (Certbot with a DNS challenge works well for *.expose.yourdomain.com), then configure Nginx:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name expose.yourdomain.com *.expose.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/expose.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/expose.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
# Required for WebSockets — Expose's tunnel connection depends on this
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/expose /etc/nginx/sites-enabled/
sudo systemctl restart nginx
(Apache and HAProxy configs following the same pattern are in Expose’s official docs if you’d rather not use Nginx.)
Step 6: Keep it running with Supervisor
sudo apt install supervisor -y
sudo nano /etc/supervisor/conf.d/expose.conf
[program:expose]
command=/usr/bin/php /home/expose/server/expose-server serve expose.yourdomain.com
numprocs=1
autostart=true
autorestart=true
user=expose
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start expose
(Alternative: Expose’s server repo ships a docker-compose.yaml. Copying .env-example to .env, setting PORT, DOMAIN, ADMIN_USERNAME, and ADMIN_PASSWORD, then running docker-compose up -d gets you the same result with less manual setup, at the cost of a bit less transparency into what’s running.)
Step 7: Point your local client at your server
On your local machine, with the Expose client installed:
expose default-server expose.yourdomain.com
expose token YOUR_TOKEN
Then share a site with a fixed subdomain — something you can’t do on the free public server:
expose share http://ecommerce.test --subdomain=checkout
Your app is now live at https://checkout.expose.yourdomain.com, on infrastructure you control, with no per-connection time limit and no per-seat fee.
Which one actually fits
Stick with ngrok if you need raw TCP/TLS tunneling (databases, SSH, game servers), Kubernetes ingress, org-level SSO, or compliance documentation — none of which Expose does.
Consider Expose if your traffic is HTTP/HTTPS only, you’re already in the Laravel/Herd ecosystem, and you’d rather run a $4/month VPS than pay a per-seat annual fee for stable custom subdomains — or you just want to be able to read the tunneling tool you depend on.
Changelog
Corrections and additions made to the original draft, checked against ngrok’s and Expose’s current documentation, GitHub/Packagist metadata, and DigitalOcean’s pricing page:
- ngrok session timeouts — Original draft claimed ngrok’s free tier imposes session timeouts requiring frequent restarts. ngrok’s documentation states free-tier endpoints have no timeout and can run indefinitely as a background service. Source: ngrok Free Plan Limits documentation.
- ngrok “random subdomain every restart” — Original draft treated this as unconditional. Since 2023, every ngrok account (including free) can claim one permanent static domain that doesn’t change on restart; only accounts that haven’t claimed one still get a new random subdomain each time. Source: ngrok’s “Static domains for all ngrok users” post; ngrok domains documentation.
- ngrok rate limit figure — Original draft cited a flat “40 connections per minute.” Current documented free-tier limits are 100 TCP connection attempts/minute and 4,000 HTTP requests/minute (separate figures, not one). Source: ngrok Free Plan Limits / Pricing and Limits documentation.
- DigitalOcean droplet price — Original draft used $5/month. DigitalOcean’s cheapest Basic Droplet is now $4/month (512 MiB RAM, 1 vCPU, 10 GiB SSD, 500 GiB transfer). Source: DigitalOcean Droplet pricing page.
- Composer package name — Original draft used
composer global require beyondcode/expose. Current official installation docs specifyexposedev/expose;beyondcode/exposestill exists on Packagist as the legacy name pointing at the same project. Source: Expose Installation documentation; Packagist. - PHP version requirement — Original draft said “PHP 7.4 or higher (recommend 8.2+).” The current client’s
composer.jsonhard-requires PHP^8.2— this isn’t just a recommendation anymore. Source: exposedev/expose composer.json. - Client/server architecture — This is the most substantial correction. The original draft assumed a single
beyondcode/exposepackage provides both the client and a self-hostable server viaexpose serve/expose publish. As of the current release, the self-hosted server is a separate package (exposedev/server, installed viagit clone+composer install, run withexpose-server serve <domain>), distinct from the client package (exposedev/expose). The old unifiedexpose servecommand on the client now returns an error directing users to the separateexpose-serverbinary. The self-hosting steps in this guide were rewritten around the current two-package structure. Source: Expose Server documentation (starting-the-server); exposedev/server GitHub issue #1 confirming the current client no longer acceptsserve. - Free-tier custom subdomains — Original draft implied you could request a custom subdomain on Beyond Code’s free shared server. Current docs state
--subdomainrequires either Expose Pro/Team or a self-hosted server — the freesharedwithexpose.comtier always assigns a random subdomain per connection. Source: Expose Sharing documentation. - Expose pricing — Original draft didn’t give concrete Expose Pro pricing. Added current figures: Pro $79/user/year, Team $229/team/year (up to 10 users), plus the actual restrictions of the free Hobby tier (time-limited connections, single EU server, random URLs). Source: expose.dev pricing page.
- Nginx config — Original draft’s Nginx block listened on port 80 and left SSL to a separate, unspecified Certbot step. Replaced with Expose’s own documented Nginx config, which terminates SSL directly on port 443. Source: Expose Server SSL Support documentation.
- Supervisor config — Original draft’s
command=path assumed the old unified binary layout. Corrected to point at the separateexpose-serverbinary with the domain argument, matching the current package structure (Expose’s own supervisor example in the docs omits the domain argument, which is likely a documentation gap — it’s included here since the server needs it to know which domain to serve). Source: Expose Server “Keeping the server running with supervisord” documentation. - Laravel integration scope — Original draft only covered Laravel Valet. Added that Valet now supports a third share tool,
cloudflared, alongside ngrok and Expose, and that Laravel Herd (Beyond Code’s own environment) ships with Expose built in natively via its settings UI — a more directly relevant integration point than Valet, since Beyond Code builds both tools. Source: Laravel Valet documentation (12.x/13.x); Expose Installation documentation. - Project activity — Added current GitHub stats (roughly 4,500+ stars, 300+ forks, MIT license, latest release 3.2.2, March 2026) to establish the project is actively maintained, which the original draft didn’t substantiate. Source: Packagist (exposedev/expose); GitHub.
- Interstitial page nuance — Added, since it wasn’t in the original draft: ngrok’s free-tier browser warning page doesn’t affect programmatic/webhook traffic and can be bypassed on the free tier via a custom User-Agent or header, without upgrading. Source: ngrok Free Plan Limits documentation.
- Removed unverifiable/promotional language from the original draft (e.g., unqualified claims that Expose is simply “the ultimate Laravel Valet share alternative”) in favor of neutral, sourced comparisons; stripped file metadata for a clean Markdown deliverable.
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.