Dev Log
Shopify App Dev
October 24, 2025

Configuring Fixed Port for Shopify App Dev Tunnel (Vite Modification)

Shawn Shen avatar
Shawn Shen
Founder of Selofy
min read
Learn how to pin Shopify App dev server to a fixed port (Vite + React Router), avoid random ports, and fix HMR WebSocket binding errors on Windows.

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 = 443

For 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 = 3001

Step 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

  1. Port Mismatch: Vite port must exactly match your tunnel's localPort
  2. HMR Host Binding: Don't set host in HMR config when using tunnels
  3. Port Consistency: HMR port should match main server port
  4. Environment Variables: Avoid using --port flags; prefer vite.config.ts
  5. 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)

References

Frequently Asked Questions

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 Self‑Host a Stable Shopify App Dev Tunnel with FRP + Cloudflared (Fixed Port Mode) logo Image
Dev Log
Shopify App Dev
How to Self‑Host a Stable Shopify App Dev Tunnel with FRP + Cloudflared (Fixed Port Mode)
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
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