# Remix: changing the dev server port (and why PORT is not enough)

Vite ignores `PORT` by default. Change the port with `server.port` or `--port`; in production `remix-serve` reads `PORT`, but that is another process.

> Page: https://ui.eneawork.it/blog/remix-porta-dev-server
> Markdown: https://ui.eneawork.it/blog/remix-porta-dev-server.md

*2026-11-01*

Risposta breve: in sviluppo la porta la decide Vite, non PORT. Imposta server.port in vite.config.ts oppure passa --port. In produzione remix-serve legge PORT, ma è un altro processo.

Il tag remix.run conta 558 domande sulla Stack Exchange API al 31 ottobre 2026. È un ecosistema piccolo: la documentazione della versione installata vale più di una risposta generica. Il catalogo EneUI misura 111.985 componenti, 1.556 blocchi, 12.808 icone da 1.472 librerie, valori misurati e letti da /api/stats il 1 novembre 2026. Tutti i comandi qui sotto girano in locale, nella cartella del progetto: nessun account.

## Due processi, due configurazioni

In sviluppo npm run dev avvia il plugin Remix dentro Vite. Vite crea il server HTTP, risolve i moduli e serve il client HMR; Remix fornisce il routing e il rendering. La porta appartiene quindi a Vite, e PORT non entra nel calcolo, perché nessun file del plugin la legge. In produzione il comando è remix-serve, un server HTTP autonomo che legge PORT e non ha alcun rapporto con Vite. Due processi diversi, due lettori diversi della stessa variabile.

C'è un secondo strato da tenere presente: Vite carica i file .env in import.meta.env, non in process.env. Se scrivi PORT=4000 nel file .env e poi lo leggi con process.env.PORT dentro la configurazione, il valore risulta assente e ricadi sul valore di riserva. Per leggere davvero quel file serve loadEnv, oppure una variabile esportata dalla shell prima del comando.

## La configurazione

// vite.config.ts import { vitePlugin as remix } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [remix()], server: { port: 4000, }, });

Con questa configurazione npm run dev parte su 4000. Il flag --port 4000 sulla riga di comando ha la precedenza sul file, utile quando due progetti devono convivere. L'ordine è fisso: flag, poi configurazione, poi il default del framework.

## Perché PORT non basta

Vite non legge PORT per impostazione predefinita. Molti script di deploy assumono che la variabile sia rispettata, perché altri strumenti lo fanno. Se vuoi che funzioni comunque, leggila nella configurazione:

server: { port: Number(process.env.PORT) || 3000, },

In produzione il comando è diverso: remix-serve ./build/server/index.js usa un server HTTP che legge PORT. La stessa variabile, due comportamenti: è qui che nasce la confusione.

## Il passo diagnostico

# terminal 1: start the dev server on the expected port npx vite --port 4000 --strictPort # terminal 2: see which process answers on that port curl -sI http://localhost:4000 | head -n 1

Se la risposta è 200 e il log di Vite mostra la stessa porta, il client parla con il processo giusto. Se curl risponde ma la pagina non si aggiorna, la porta HTTP è corretta e il problema è il WebSocket: apri la scheda Network del browser, filtra WS e confronta l'indirizzo con quello stampato da Vite all'avvio. Se invece curl fallisce, il processo in ascolto non è quello che credi: cerca chi occupa la porta con netstat -ano | findstr :4000 su Windows o lsof -i :4000 su macOS.

## Il caso del proxy

server: { port: 4000, hmr: { clientPort: 443, protocol: "wss", }, },

Dietro nginx o un tunnel la pagina si carica ma non si aggiorna. Il client HMR non usa l'URL della pagina: costruisce il proprio indirizzo da server.hmr. Se la pagina arriva su 443 e il socket parte su 4000, la connessione muore in silenzio. La configurazione qui sopra dice al browser dove trovare il WebSocket.

## Quando la correzione comune peggiora le cose

La correzione più diffusa è copiare PORT nella configurazione. In un container quella variabile è impostata dal runtime e vale 3000: il dev server occupa così la porta che altri servizi si aspettano libera, e il conflitto compare solo sulla macchina di chi ha esportato la variabile. Lo stesso vale per hmr.clientPort: 443, giusto dietro un proxy e sbagliato in locale, dove il browser prova a parlare in TLS con una porta in chiaro e la pagina resta ferma. Una correzione condizionale, attiva solo fuori dallo sviluppo, evita entrambi i casi.

## Un limite con un nome

Limite dichiarato di server.hmr.clientPort: cambia la porta annunciata al browser, non il percorso del socket. Se il proxy espone il WebSocket su /hmr e non sulla radice, il client continua a chiedere / e riceve 404 anche con la porta giusta. In quel caso serve server.hmr.path con lo stesso valore nel proxy: la sola porta non basta, e il sintomo resta identico.

## Come prevenirlo

## Dove continuare

Nel catalogo: componenti, dashboard e blocchi. Le guide: struttura App Router e alternative a MUI; il confronto: copy-paste o headless.

## FAQ

### Perché `PORT=4000 npm run dev` non cambia nulla?

Perché Vite non legge quella variabile da solo. Serve `server.port` nella configurazione o il flag `--port`.

### Come cambio la porta in produzione?

Con `remix-serve`, `PORT=4000 npm start` funziona: il processo legge la variabile. È un server diverso da quello di sviluppo.

### La pagina si carica ma non si aggiorna: perché?

Il client HMR prova a connettersi alla porta sbagliata. Imposta `server.hmr.clientPort` o `server.hmr.host`.

### Posso usare una porta diversa per ogni progetto?

Sì, ma documentala: uno script che assume 3000 fallisce in silenzio su un altro progetto.

### Serve configurare anche il proxy?

Se metti Remix dietro nginx o un tunnel, sì: il proxy deve inoltrare sia HTTP sia il WebSocket di Vite.
