Configuring Fixed Port for Shopify App Dev Tunnel (Vite Modification)
Watch Video Tutorial
Why fix the port?
The Shopify App dev server (React Router + Vite) may choose a random port when the default is occupied, which complicates tunneling and reverse proxies. Fixing the port makes FRP/Cloudflare Tunnel configuration straightforward and reduces daily friction.
Before vs After (Summary)
- Before: react-router dev may bind to an unpredictable port; HMR might attempt to bind to your public tunnel hostname, causing EADDRNOTAVAIL on Windows.
- After: Vite dev server uses a fixed port with strictPort: true; HMR listens locally without binding to the public hostname, while clients connect via the tunnel domain.
Step-by-step Changes
1) Determine Your Tunnel Port Configuration
First, check your tunnel configuration to find the correct port:
For FRP users:
Check your frpc.toml file:
[[proxies]]
name = "dev-https"
type = "tcp"
localIP = "::1"
localPort = 3001 # ← This is your target port
remotePort = 443For Cloudflare Tunnel users:
Check your tunnel configuration for the local port mapping.
The key principle: Vite's port must match your tunnel's localPort exactly.
2) Update Vite server port and strict mode
File: vite.config.ts
Before:
server: {
port: Number(process.env.PORT || 3000),
hmr: /* dynamic host based on SHOPIFY_APP_URL */
}After:
server: {
port: 3001, // Use the port from your tunnel config
strictPort: true,
hmr: hmrConfig, // see next section
}3) Fix HMR to avoid binding to public tunnel IP
In vite.config.ts, the HMR config is derived from SHOPIFY_APP_URL. When using a public tunnel URL, binding HMR host to that domain may resolve to Cloudflare IPs and fail to listen on Windows.
Before:
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",
host, // ❌ This causes EADDRNOTAVAIL
port: parseInt(process.env.FRONTEND_PORT!) || 8002, // ❌ Wrong port
clientPort: 443
};
}After:
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",
// ✅ Do not set host - avoids binding to public IP
// ✅ Use same port as main server
port: 3001, // Must match your tunnel's localPort
clientPort: 443,
};
}4) Complete Configuration Example
Here's a complete vite.config.ts example:
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig, type UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
// Handle HOST env var replacement
if (
process.env.HOST &&
(!process.env.SHOPIFY_APP_URL ||
process.env.SHOPIFY_APP_URL === process.env.HOST)
) {
process.env.SHOPIFY_APP_URL = process.env.HOST;
delete process.env.HOST;
}
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",
// Do not set host to avoid binding to public tunnel IP
// Use same port as main server for consistency
port: 3001, // Must match your tunnel's localPort
clientPort: 443,
};
}
export default defineConfig({
server: {
allowedHosts: [host],
cors: {
preflightContinue: true,
},
// Fixed port matching your tunnel configuration
port: 3001, // Change this to match your tunnel's localPort
strictPort: true,
hmr: hmrConfig,
fs: {
allow: ["app", "node_modules"],
},
},
plugins: [
reactRouter(),
tsconfigPaths(),
],
build: {
assetsInlineLimit: 0,
},
optimizeDeps: {
include: ["@shopify/app-bridge-react"],
},
}) satisfies UserConfig;
5) Start the dev server with your tunnel
Use your tunnel URL:
shopify app dev --tunnel-url https://your-tunnel.example.com:443
Port Configuration Workflow
Step 1: Check Your Tunnel Config
# For FRP users
cat frpc.toml | grep localPort
# Output: localPort = 3001Step 2: Update Vite Config
Set both the main server port and HMR port to match your tunnel's localPort:
server: {
port: 3001, // From step 1
strictPort: true,
},
hmr: {
port: 3001, // Same as server port
clientPort: 443, // Your tunnel's remote port
}
Step 3: Verify Configuration
shopify app dev --tunnel-url=https://your-tunnel.example.com:443
Common Pitfalls and Lessons Learned
- Port Mismatch: Vite port must exactly match your tunnel's localPort
- HMR Host Binding: Don't set host in HMR config when using tunnels
- Port Consistency: HMR port should match main server port
- Environment Variables: Avoid using
--portflags; prefer vite.config.ts - Proxy Issues: Clear proxy env vars or set
NO_PROXY=localhost,127.0.0.1,::1
Troubleshooting
Port Already in Use
# Check what's using your port
netstat -ano | findstr :3001
# Kill the process if needed
taskkill /PID <process_id> /F
HMR Connection Issues
- Ensure HMR port matches main server port
- Don't set host in HMR config for tunnel setups
- Check that clientPort matches your tunnel's remote port
Tunnel Not Working
- Verify tunnel service is running
- Check that localPort in tunnel config matches Vite port
- Ensure tunnel is forwarding to the correct local IP (127.0.0.1 or ::1)
.jpg)
