Why does CLI V3 overwrite my specified server port?

Topic summary

Shopify CLI v3 overwrites a user-specified dev server port (e.g., 3000) with a random 5xxxx port, ignoring PORT in .env, shopify.web.toml, and --tunnel-url flags. Forcing the port led to a second ngrok tunnel and “only one tunnel” errors; Cloudflare Tunnel flags were also ignored, and process.env.PORT was reset.

Working approaches shared:

  • JS/Vite apps: Configure vite.config.js to proxy to the backend host and use explicit FRONTEND_PORT/BACKEND_PORT. Mirror these ports in frontend and backend shopify.app.toml (e.g., 8081) and set host/HMR accordingly. This stabilized port usage and avoided random reassignment. (Code snippets and config files are central to this solution.)
  • Framework-specific dev command: In shopify.web.toml, customize [commands] dev to pass the port flag to the framework (e.g., “next dev --port 4000”). A similar pattern should apply to other languages/frameworks.

Limitations and context:

  • The Vite-based fix doesn’t apply to server-rendered Rails apps; no Rails-specific resolution was provided.
  • Documentation for CLI v3, Vite, and FRONTEND_PORT/BACKEND_PORT interactions was noted as insufficient.

Recent update: The original poster moved to Remix (server-side rendering), reporting some performance challenges with Shopify’s redirect helper but expects improvements.

Status: Partially resolved via workarounds; no official, universal fix confirmed, especially for Rails setups.

Summarized with AI on January 4. AI used: gpt-5.

Migrating my existing app from CLIv2 to CLIv3.

Issue:

CLI always overwrites my desired server Port 3000 with a random port in the 5xxxx range at runtime.

Attempted Solutions:

A) Tried adding PORT=3000 to .env

B) Tried adding PORT=3000 to the shopify.web.toml files

C) Tried forcing the port number to 3000 by changing the backend dev script

from:

"dev": "NODE_ENV=development nodemon --exec babel-node server/index.js --watch server/" 

to:

"dev": "cross-env PORT=3000 nodemon --exec babel-node server/index.js --watch server/",

… but in this case, the tunnel is created at 3000, but the CLI tries to open a second ngrok tunnel, resulting in a 'you are only allowed one tunnel" error.

D) Tried to preset an NGROK Tunnel and use --tunnel-url flag in the CLI dev script:

sudo npm run dev -- --reset --tunnel-url [https://7ea4-100-37-157-53.ngrok.io:3000](https://7ea4-100-37-157-53.ngrok.io:3000)

… resulting in the flag being ignored – the port is overwritten to 5xxxx

E) Repeated (D) with a Cloudflare Tunnel

… resulting in the flag being ignored – the port is overwritten to 5xxxx

In all cases, the process.env.port is rewritten to a 5xxxx port.

Any thoughts or guidance would be appreciated.

Regards,

Tom

Hi there,

Have you managed to solve this?

I’m having the same issue.

Regards,

Bogdan

Bogdan,

I did solve this last spring; with a bit of trial and error:

Note the proxy set in the vite config file, located in your front-end directory.

vite.config.js

import { defineConfig } from "vite";
import { dirname } from "path";
import { fileURLToPath } from "url";
//import https from "https";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import dotenv from 'dotenv';
//import basicSsl from '@vitejs/plugin-basic-ssl'

dotenv.config(); // load env vars from .env

if (
  process.env.npm_lifecycle_event === "build" &&
  !process.env.CI &&
  !process.env.SHOPIFY_API_KEY
) {
  console.warn(
    "\nBuilding the frontend app without an API key. The frontend build will not run without an API key. Set the SHOPIFY_API_KEY environment variable when running the build command.\n"
  );
}

// Note for below: Change *[http://ITS-D25Q310QF8J](http://ITS-D25Q310QF8J) to resolve to your host*
const proxyOptions = {
  target: `*http://ITS-D25Q310QF8J*:${process.env.BACKEND_PORT}`,  
  changeOrigin: true,
  secure: false,  // changed from true 4-3-23
  ws: false,
};

const host = process.env.HOST
  ? process.env.HOST.replace(/https?:\/\//, "")
  : "localhost";

let hmrConfig;
if (host === "localhost") {
  hmrConfig = {
    protocol: "ws",
    host: "localhost",
    port: 64999,
    clientPort: 64999,
  };
} else {
  hmrConfig = {
    protocol: "wss",
    host: host,
    port: process.env.FRONTEND_PORT,
    clientPort: 443,
  };
}
console.log("48 hmrConfig: ", hmrConfig);

export default defineConfig({
  root: dirname(fileURLToPath(import.meta.url)),
  plugins: [
      react(),
    nodePolyfills({
      // Whether to polyfill `node:` protocol imports.
      protocolImports: true,
    }),
    //basicSsl(),
  ],
  define: {
    "process.env.SHOPIFY_API_KEY": JSON.stringify(process.env.SHOPIFY_API_KEY),
    'process.env.NODE_DEBUG' : "'development'",
    'global'                 : {
      "Uint8Array": Uint8Array
    },
    __BACKEND_PORT__: `"${process.env.BACKEND_PORT}"`,
  },
  resolve: {
    preserveSymlinks: true,
    alias: {
      //'node:stream/web': path.resolve(__dirname, 'node_modules/stream-browserify/index.js'),
      //stream: 'stream-browserify',
      'node:stream/web':'stream-browserify/index.js',
      'node:fs':'node/lib/fs.js',
    },
  },
  server: {
    host: "its-d25q310qf8j",  // Resolve to your host server
    port: process.env.FRONTEND_PORT,
    hmr: hmrConfig,
    cors: true,
    proxy: {
      "^/(\\?.*)?$": proxyOptions,
      "^/api(/|(\\?.*)?$)": proxyOptions,
      "^/qrcodes/[0-9]+/image(\\?.*)?$": proxyOptions,
      "^/qrcodes/[0-9]+/scan(\\?.*)?$": proxyOptions,
      "^/smp(/|(\\?.*)?$)": proxyOptions,
    },
  },
});

With the vite proxy set for your host machine, the rest of the Shopify CLIv3 config is straight-forward:

App-level shopify.app.toml

# This file stores configurations for your Shopify app.

scopes = "write_products,read_discounts,write_customers ....

Back-end shopify.app.toml

==========================

type="backend"
[commands]
dev = "npm run dev"
PORT=8081
FRONTEND_PORT=8081
BACKEND_PORT=8081

Front-end shopify.app.toml

type="frontend"
[commands]
dev = "npm run dev"
build = "npm run build"
PORT=8081
FRONTEND_PORT=8081
BACKEND_PORT=8081

===

Note:
Shopify should publish a pattern with best practices for the CLIv3 framework,

including its move from webpack to vite. Deeper documentation of FRONTEND_PORT / BACKEND_PORT and how this all of this works with VITE would also be useful.

I hope this helps you.

Regards,

Tom

Hey Tom,

Thank you for taking the time to provide all these details.

I’m sure this will be useful for someone.

Unfortunately it does not apply to my case, because my app does not have a JS frontend. It’s a plain Rails app with view generated on the server side.

Regarding the Shopify documentation on CLIv3 it is indeed a lot of room for improvement.

Regards,

Bogdan

@BogdanM & @tomadelaney it took me a while to figure it out, but when I did, well the answer was hilariously obvious.

I am using node, but I assume you have a similar option for Ruby (or whatever language).

The shopify.web.toml has the following (for my node app)

[commands]
dev = “next dev”
build = “next build”

All you have to do is customize the command to run here.

[commands]
dev = “next dev --port 4000”
build = “next build”

2 Likes

Will,

Thanks for the update.

I’ve since moved from Vite to the latest Shopify framework, Remix.

Remix is offers a great deal with its server-side rendering.

I am having some challenges getting the remiox platform to perform well

with Shopify’s redirect helper, however I do believe these issues will be sorted

out over time.

Regards,

Tom