localhost:5173 / config-vs-cli
vite.config.js or a CLI flag — which one wins on port 5173
A CLI flag such as --port overrides the same option in vite.config.js, but a plugin that sets the option in its config hook overrides both, because Vite merges plugin results after the command line.
What is the order, from weakest to strongest?
Vite 8.3.1 resolves the dev-server options in three merge steps, each one laid over the previous result. The source is resolveConfig() in packages/vite/src/node/config.ts: the config file is loaded first, then mergeConfig(fileConfig, inlineConfig) puts the command-line flags on top, then every plugin config hook is merged over that.
- 1. Vite default — port 5173, host "localhost".
- 2. vite.config.js (or .mjs, .ts, .cjs, .mts, .cts).
- 3. CLI flags — --host, --port, --open, --cors, --strictPort.
- 4. The config hook of each plugin, in plugin order. This is where a framework wrapper sits.
A CLI flag beats the config file
Reproduced on 30 September 2026 with Vite 8.3.1 and Node 24.19: a config with server.port 4000 starts on 4000; the same project started with --port 4100 starts on 4100. Only the key you pass is replaced — the rest of the server block stays as the file wrote it.
# vite.config.js: server: { port: 4000 } $ npx vite ➜ Local: http://localhost:4000/ $ npx vite --port 4100 ➜ Local: http://localhost:4100/
A plugin beats the CLI flag
A plugin config hook runs after the flags are merged and its return value is merged on top. A plugin returning server: { port: 4600 } wins against both the file and --port 4100 — reproduced on the same setup. The hook receives the already merged config as its first argument, so a plugin can respect a flag; whether a particular framework does is up to that framework, not to Vite. If a flag seems to be ignored in a framework project, this is the first place to look.
# vite.config.js: server.port 4000, plus a plugin whose # config hook returns { server: { port: 4600 } } $ npx vite --port 4100 ➜ Local: http://localhost:4600/
npm run dev -- --port adds a second flag, and the last one wins
When the dev script already contains a port, npm appends yours after it, so the CLI sees the flag twice. Vite keeps the last value: filterDuplicateOptions() in cli.ts replaces any array of values with value.at(-1). With "dev": "vite --port 4200", npm run dev starts on 4200 and npm run dev -- --port 4300 starts on 4300.
# package.json: "dev": "vite --port 4200" $ npm run dev -- --port 4300 # runs: vite --port 4200 --port 4300 ➜ Local: http://localhost:4300/
When is vite.config.js not read at all?
Without --config, Vite looks for a config file in the root it was given, and falls back to the current directory only when no root is passed. It stops at the first file it finds, in this order: vite.config.js, .mjs, .ts, .cjs, .mts, .cts. Each case below was reproduced with a config setting port 4000.
- vite.config.js and vite.config.ts side by side: the .js file loads, the .ts file is ignored without a warning. Typical after a migration to TypeScript that left the old file behind.
- vite started from a subdirectory: the config in the project root is not found, the server starts on 5173.
- vite sub from the project root (sub as root argument): Vite looks for the config inside sub/, not next to package.json — again 5173.
- vite sub --config vite.config.js: an explicit path is resolved from the current directory, not from the root, and the server starts on 4000.
- An explicit --config pointing to a file that does not exist stops the server with an error; there is no fallback to the default file.
Does an environment variable change the port?
No. PORT=4700 npx vite still starts on 5173 — the dev server does not read PORT, and there is no VITE_PORT either. The config function cannot see the flags: it receives only mode, command, isSsrBuild and isPreview. If you want an environment variable to set the port, read it yourself in vite.config.js; a --port flag will still override it.
export default defineConfig({
server: {
// Vite ignores PORT on its own; this line makes it count
port: Number(process.env.PORT) || 5173,
},
})How to see which value won
The Local line Vite prints at startup shows the port it actually bound, after all merges and after any fallback to the next free port. If that line and your config disagree, check in this order: a duplicate flag in the npm script, a plugin config hook, a second config file, the directory the command runs in.
# faq
Questions
Does --port override server.port in vite.config.js?
Yes. Vite merges the CLI flags over the config file, so --port replaces server.port for that run. The other server options from the file stay in effect.
Why does my framework ignore --port?
A plugin config hook is merged after the CLI flags. If the framework plugin returns its own server.port there, it overrides the flag.
What happens with npm run dev -- --port when the script already sets a port?
The flag appears twice and Vite uses the last one, which is the one you appended.
Which config file wins if both vite.config.js and vite.config.ts exist?
vite.config.js. Vite checks .js, .mjs, .ts, .cjs, .mts and .cts in that order and loads the first file it finds.
Does Vite read the PORT environment variable?
No. Tested with Vite 8.3.1: PORT is ignored and the dev server starts on 5173 unless the config reads process.env.PORT itself.
# next
Related
# sources
- Vite — Configuring Vite (config file resolution, --config)
- Vite — CLI: vite dev
- Vite — Plugin API: config hook
- Vite source v8.3.1 — cli.ts (filterDuplicateOptions, dev command)
- Vite source v8.3.1 — config.ts (resolveConfig, loadConfigFromFile, runConfigHook)
- Vite source v8.3.1 — constants.ts (DEFAULT_CONFIG_FILES)
Checked against the Vite documentation on 2026-09-30.