Dev Log
Shopify App Dev
November 5, 2025

How to Self‑Host a Stable Shopify App Dev Tunnel with FRP + Cloudflared (Fixed Port Mode)

Shawn Shen avatar
Shawn Shen
Founder of Selofy
min read
Build a reliable, fixed‑port Shopify dev tunnel using FRP + Cloudflared. Step‑by‑step, beginner‑friendly. Avoid random ports, fix NGROK issues in restrictive networks, and speed up CLI startup.

Watch Video Tutorial

How to Self‑Host a Stable Shopify App Dev Tunnel with FRP + Cloudflared (Fixed Port Mode)

This guide shows—step by step and beginner‑friendly—how to run your Shopify app in “fixed port” tunnel mode using FRP (Fast Reverse Proxy) plus Cloudflared. We’ll walk through the trial‑and‑error we used to reach the final working setup, explain common pitfalls, and give you reliable steps you can follow even if you don’t write code.

Why this? Shopify CLI’s default tunnel can assign random local ports. In some corporate, school, or ISP‑restricted networks, NGROK and random ports make reverse proxies unreliable. Our FRP + Cloudflared setup keeps a stable, fixed local dev port and a predictable public domain so your app preview remains accessible.

At a glance (the traffic path):

  • Browser → Cloudflare Edge → Cloudflared (on your server) → FRPS (server side of FRP) → FRPC (on your laptop/desktop) → Shopify CLI dev proxy → Vite/React Router dev server (port 3001)

We’ll use:

  • A public server (e.g., a VPS in the US) for FRPS and Cloudflared
  • Your local Windows/macOS/Linux machine for FRPC and the Shopify app dev server
  • A domain on Cloudflare (e.g., tunnel.example.dev) for clean HTTPS

Note: We keep FRPS deployment brief here; we have a separate deep‑dive post you can follow for the server side.

1) FRPS (Server‑Side FRP) — Brief Setup and Link

FRPS is the server component of FRP. You run FRPS on your public server; it accepts connections from your local FRPC and forwards traffic.

  • Control port: FRP 0.65 defaults to 7000 (can be TLS or non‑TLS; we’ll cover matching settings later)
  • Data ports: You can expose 8080 (HTTP) and 443 (TCP/HTTPS) at FRPS to forward to your local FRPC

We have a separate article with full FRPS setup, systemd service, firewall rules, and hardening:

For this tutorial, assume:

  • FRPS is reachable at your public IP (e.g., 145.79.2.104)
  • FRPS control port is 7000
  • FRPS defines or allows proxies on 8080 and 443 that we will bind via FRPC
  • FRPS uses a shared token for auth

2) Cloudflared (Cloudflare Tunnel) — Latest, Clear, Click‑By‑Click

shopify blog screeshot

Goal: Route https://tunnel.example.dev through Cloudflare to your server’s FRPS on 8080, which will forward the traffic to your local machine via FRPC.

Prerequisites:

  • You have a Cloudflare account
  • Your domain is added to Cloudflare DNS (nameservers pointing to Cloudflare)
  • You can access the Cloudflare Zero Trust dashboard

Step‑by‑step (2024/2025 UI layout):

  1. Open Cloudflare Dashboard
    • Go to dash.cloudflare.com and select your account
    • Ensure your target domain (e.g., example.dev) is in Cloudflare
  2. Open Zero Trust → Networks → Tunnels
    • In the left sidebar, click “Zero Trust”
    • In Zero Trust, locate “Networks” → “Tunnels” (Cloudflare Tunnel)
  3. Create a Named Tunnel
    • Click “Create a tunnel”
    • Name it, e.g., shopify-dev-fixport
    • Choose “cloudflared” connector
    • Click “Next”
  4. Install & Run cloudflared on your server
    • On Ubuntu/Debian:
      • sudo apt update && sudo apt install cloudflared
    • In the Tunnel creation screen, Cloudflare shows a one‑line command with a token. Copy that exact command and run it on your server:
      • Example: cloudflared service install <YOUR_TOKEN>
      • Or: cloudflared tunnel --no-autoupdate run --token <YOUR_TOKEN>
    • After a few seconds, the dashboard should show your connector as “Healthy”
  5. Add a Public Hostname route to FRPS HTTP port
    • In the tunnel’s details page, click “Add public hostname”
    • Hostname: tunnel.example.dev
    • Type: HTTP
    • Path: /
    • Service: http://127.0.0.1:8080 (this is on your server, where FRPS listens for HTTP)
    • Save
    • Cloudflare will auto‑create a proxied DNS record (a CNAME to the tunnel). No manual DNS needed.
  6. SSL/TLS Mode on Cloudflare
    • If your origin (server’s FRPS) does not serve TLS at 8080, use “Flexible” SSL mode
    • If you need “Full/Strict”, you must terminate TLS on the origin (e.g., a reverse proxy like Nginx/Caddy in front of FRPS, or FRPS/FRPC configured for TLS where appropriate)
  7. Quick test on your server
    • Run on the server: curl -sv http://127.0.0.1:8080/ -H "Host: tunnel.example.dev"
    • Expected: HTML or a 302 from your app (once FRPC is connected). If you get “Empty reply”, FRPC probably isn’t yet forwarding to your local dev port—continue to step 3.

Why this works: Cloudflared keeps a persistent, outbound tunnel to Cloudflare’s edge. Requests to tunnel.example.dev enter the tunnel and reach http://127.0.0.1:8080 on your server. FRPS then forwards the TCP stream to your local machine via FRPC.

3) FRPC (Local FRP Client) — Exact Configuration & Troubleshooting

Goal: Map FRPS’s 8080 and 443 to your local fixed dev port (we use 3001).

Download & verify FRP client (Windows):

  1. Download FRP 0.65.0 Windows AMD64 ZIP from the official releases
  2. Extract it somewhere in your project (e.g., frp_0.65.0_windows_amd64/)
  3. If Windows blocks the EXE:
    • Open PowerShell in the folder and run: Unblock-File -Path .\frpc.exe
  4. Verify version: ./frpc.exe -v → expect 0.65.0

Create/edit frpc.toml (client config):

serverAddr = "<YOUR_FRPS_PUBLIC_IP>"   # e.g., 145.79.2.104
serverPort = 7000                      # FRPS control port (default 7000)

[transport]
tls.enable = false                     # set true if FRPS control port uses TLS

[auth]
method = "token"
token = "<YOUR_SHARED_TOKEN>"

[[proxies]]
name = "dev-http"
type = "tcp"
localIP = "::1"                        # or "127.0.0.1" (see IPv6 note below)
localPort = 3001                       # your local dev server port
remotePort = 8080                      # FRPS HTTP data port

[[proxies]]
name = "dev-https"
type = "tcp"
localIP = "::1"
localPort = 3001
remotePort = 443                       # FRPS TCP/HTTPS data port

IPv4 vs IPv6 (important):

  • Some dev servers only listen on IPv6 loopback ::1 and not on IPv4 127.0.0.1. If FRPC tries 127.0.0.1:3001 and you see “actively refused”, it might be IPv6‑only.
  • Windows: check with PowerShell → Get-NetTCPConnection -LocalPort 3001 -State Listen
    • If LocalAddress is ::1, set localIP = "::1" in frpc.toml
  • Linux/macOS: ss -ltnp | grep 3001 or netstat -lntp | grep 3001

Start FRPC:

  • From the folder: ./frpc.exe -c .\frp_0.65.0_windows_amd64\frpc.toml
  • Logs should show: “login to server success” and “start proxy success” for dev-http and dev-https

Troubleshooting common errors:

  • tls: first record does not look like a TLS handshake → Your FRPS control port isn’t TLS. Set tls.enable = false (or enable TLS on FRPS and set true here).
  • token mismatch → The token in FRPC must match FRPS’s configured token.
  • actively refused to 127.0.0.1:3001 → Your dev server isn’t listening there. Check IPv6 vs IPv4 or confirm the port.

4) Shopify CLI & Vite — Fix the Dev Port, Make CLI Happy

We want the local dev server to always start on a fixed port (e.g., 3001).

  1. Vite config (React Router project)

Update vite.config.ts to fix port and allow your tunnel host:

import { reactRouter } from "@react-router/dev/vite";

import { defineConfig, type UserConfig } from "vite";

import tsconfigPaths from "vite-tsconfig-paths";



const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost").hostname;



let hmrConfig;

if (host === "localhost") {

  hmrConfig = { protocol: "ws", host: "localhost", port: 64999, clientPort: 64999 };

} else {

  hmrConfig = { protocol: "wss", port: 3001, clientPort: 443 };

}



export default defineConfig({

  server: {

    allowedHosts: [host],

    port: 3001,

    strictPort: true,

    hmr: hmrConfig,

  },

  plugins: [reactRouter(), tsconfigPaths()],

}) satisfies UserConfig;
  1. Environment
  • Set SHOPIFY_APP_URL=https://tunnel.example.dev in your .env for local dev
  • Shopify CLI uses this for its preview URL and host checks
  1. Shopify CLI command (dev)
  • Use the tunnel URL only:
    • shopify app dev --tunnel-url=https://tunnel.example.dev:443 --verbose
  • Don’t pass --localhost-port. When you use a custom tunnel, the CLI picks the right local proxy behavior; forcing --localhost-port can conflict with the tunnel.
  1. Optional: speed up startup by trimming pre‑dev/migrations

In shopify.web.toml:

[commands]

predev = "node scripts/dev-pre.cjs"

dev = "npm exec react-router dev"

And add scripts/dev-pre.cjs:

// Only run `prisma generate` when schema changed or client missingconst { spawnSync } = require("child_process");

const fs = require("fs");

const path = require("path");



const schemaPath = path.resolve(process.cwd(), "prisma", "schema.prisma");

const clientIndexPath = path.resolve(process.cwd(), "node_modules", "@prisma", "client", "index.js");



function exists(p) { try { fs.accessSync(p, fs.constants.F_OK); return true; } catch { return false; } }



function needsGenerate() {

  if (!exists(schemaPath)) return false;

  if (!exists(clientIndexPath)) return true;

  const schemaMtime = fs.statSync(schemaPath).mtimeMs;

  const clientMtime = fs.statSync(clientIndexPath).mtimeMs;

  return schemaMtime > clientMtime;

}



if (needsGenerate()) {

  spawnSync("npx", ["prisma", "generate"], { stdio: "inherit", shell: true, env: process.env });

}

Why: running prisma migrate deploy every dev boot adds 10–60s even if there are no migrations. Generating Prisma client only when needed also saves time.

Startup sequence we recommend:

  1. Run: shopify app dev --tunnel-url=https://tunnel.example.dev:443 --verbose
  2. Ensure your local dev server shows “Local: http://localhost:3001/”
  3. Start FRPC: ./frpc.exe -c .frpc.toml
  4. Open https://tunnel.example.dev and log in to your app preview

5) Improve Startup Speed Under Shopify Tunnel Mode

  • Remove dev‑time migrations: don’t run prisma migrate deploy on every boot
  • Conditional Prisma generate: only regenerate when schema changed
  • Pre‑bundle common deps: in Vite optimizeDeps.include, add react, react-dom, react-router-dom, @shopify/shopify-app-react-router if cold starts feel slow
  • Antivirus exclusions (Windows): exclude your project folder to avoid heavy scanning during boot
  • GraphiQL: if unused, consider avoiding starting it to reduce noise; Shopify CLI may start it automatically, but you can ignore it if not needed

Verification Checklist (Quick)

  • Server: curl -sv http://127.0.0.1:8080/ -H "Host: tunnel.example.dev" returns HTML/302
  • FRPC logs: “login to server success”, “start proxy success”
  • Local ports: PowerShell → Get-NetTCPConnection -LocalPort 3001 -State Listen shows listening (IPv6 ::1 or IPv4 127.0.0.1)
  • Browser: https://tunnel.example.dev shows your app preview and allows login

Frequently Asked Questions

“Empty reply from server” when curling the server
tls: first record does not look like a TLS handshake
token mismatch at FRPC login
Windows blocks frpc.exe as “virus or PUA”
Cloudflare SSL mode confusion (Flexible vs Full/Strict)
Related Blogs
Cloud Proxy API Service Deployment Guide logo Image
Dev Log
Shopify App Dev
Cloud Proxy API Service Deployment Guide
Complete Guide to Google Merchant API Authorization for Shopify Apps logo Image
Dev Log
Shopify App Dev
Complete Guide to Google Merchant API Authorization for Shopify Apps
How to Deploy a Shopify App to VPS Using Dokploy: Complete Guide logo Image
Dev Log
Shopify App Dev
How to Deploy a Shopify App to VPS Using Dokploy: Complete Guide
How to Deploy FRP on Dokploy Platform logo Image
Dev Log
Shopify App Dev
How to Deploy FRP on Dokploy Platform
Configuring Fixed Port for Shopify App Dev Tunnel (Vite Modification) logo Image
Dev Log
Shopify App Dev
Configuring Fixed Port for Shopify App Dev Tunnel (Vite Modification)
Shopify App Dev Localhost Error: Invalid Webhook URI Fix logo Image
Dev Log
Dev Log
Shopify App Dev Localhost Error: Invalid Webhook URI Fix
How I Built an E-commerce Toolkit with Zero Coding Experience - Dev Log #001 logo Image
Dev Log
Dev Log
How I Built an E-commerce Toolkit with Zero Coding Experience - Dev Log #001