Errori e fix

Remix: cambiare porta al dev server (e perché PORT non basta)

Vite non legge `PORT` per impostazione predefinita. La porta si cambia con `server.port` o con il flag `--port`; in produzione `remix-serve` usa `PORT`, ma è un altro processo.

In sintesi

  • In sviluppo la porta è di Vite: `server.port` in `vite.config.ts` o `--port` sulla riga di comando.
  • La variabile `PORT` funziona con `remix-serve` in produzione, non con il dev server Vite.
  • Dietro un proxy, anche la porta del WebSocket HMR va dichiarata, altrimenti la pagina non si aggiorna.

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.

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

  • Documenta la porta nel README e nello script di avvio.
  • Usa strictPort in sviluppo: un conflitto deve essere visibile, non silenzioso.
  • Non assumere che PORT valga in sviluppo e in produzione allo stesso modo.
  • Metti la porta del WebSocket accanto a quella HTTP, nello stesso blocco di configurazione.

Dove continuare

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

Domande frequenti

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.