Servire un'applicazione Vue.js con nginx

Vue.js è un framework JavaScript progressivo pensato per costruire interfacce utente moderne. Una volta sviluppata e testata localmente, l'applicazione deve essere distribuita su un server web in grado di gestire le richieste HTTP in modo efficiente. nginx è oggi uno dei server web e reverse proxy più diffusi al mondo, apprezzato per le sue prestazioni elevate e la flessibilità di configurazione. In questo articolo vedremo come compilare un'applicazione Vue.js per la produzione e come configurare nginx per servirla correttamente, affrontando anche i problemi tipici legati al routing lato client.

Prerequisiti

Prima di procedere, assicurarsi di avere installati i seguenti strumenti:

  • Node.js (versione 18 o superiore) e npm
  • Vue CLI oppure un progetto creato con Vite
  • Un server Linux con nginx installato (ad esempio Ubuntu 22.04)
  • Accesso root o sudo al server

Creare e compilare l'applicazione Vue.js

Se non si dispone ancora di un progetto Vue.js, è possibile crearne uno rapidamente con Vite, il build tool consigliato per i nuovi progetti Vue 3:

# Crea un nuovo progetto con Vite
npm create vite@latest my-app -- --template vue
cd my-app
npm install

Una volta sviluppata l'applicazione, il passo fondamentale è la compilazione per la produzione. Questo processo genera file ottimizzati (HTML, CSS e JavaScript minificati) che possono essere serviti direttamente da nginx.

# Compila l'applicazione per la produzione
npm run build

Al termine del processo, nella radice del progetto comparirà una cartella dist/ contenente tutti i file statici dell'applicazione. La struttura tipica è la seguente:

dist/
├── index.html
├── assets/
│   ├── index-Bx1234ab.js
│   ├── index-Cx9876ef.css
│   └── logo-Dk4567gh.png
└── favicon.ico

I nomi dei file contengono un hash calcolato dal contenuto, il che garantisce che il browser non utilizzi versioni precedenti della cache quando il codice viene aggiornato (cache busting automatico).

Installare e verificare nginx

Su sistemi basati su Debian/Ubuntu, nginx si installa con il gestore di pacchetti di sistema:

# Aggiorna i repository e installa nginx
sudo apt update
sudo apt install nginx -y

# Verifica che il servizio sia attivo
sudo systemctl status nginx

Per verificare che nginx risponda correttamente, aprire un browser e navigare verso l'indirizzo IP del server. Comparirà la pagina di benvenuto predefinita di nginx.

Caricare i file sul server

I file presenti nella cartella dist/ devono essere copiati in una directory accessibile da nginx. La convenzione su sistemi Linux è di usare una sottocartella di /var/www/:

# Crea la directory di destinazione
sudo mkdir -p /var/www/my-app

# Copia i file compilati nella directory del server
sudo cp -r dist/* /var/www/my-app/

# Assegna i permessi corretti all'utente www-data (usato da nginx)
sudo chown -R www-data:www-data /var/www/my-app
sudo chmod -R 755 /var/www/my-app

In alternativa, utilizzando rsync da un ambiente di sviluppo remoto si può automatizzare il trasferimento:

# Trasferisce i file via rsync su un server remoto
rsync -avz --delete dist/ user@your-server-ip:/var/www/my-app/

Configurare nginx per servire l'applicazione

La configurazione di nginx avviene tramite file detti virtual host (o server block), situati in /etc/nginx/sites-available/. Ogni file descrive come nginx deve rispondere alle richieste per un determinato dominio o indirizzo IP.

Creiamo un nuovo file di configurazione per la nostra applicazione:

# Crea il file di configurazione del virtual host
sudo nano /etc/nginx/sites-available/my-app

Inserire la seguente configurazione di base:

server {
    # Ascolta sulla porta 80 (HTTP)
    listen 80;

    # Sostituire con il proprio dominio o indirizzo IP
    server_name example.com www.example.com;

    # Percorso radice dove risiedono i file statici
    root /var/www/my-app;

    # File da servire per impostazione predefinita
    index index.html;

    location / {
        # Prova prima il file richiesto, poi la directory,
        # infine ricade sempre su index.html (necessario per il routing SPA)
        try_files $uri $uri/ /index.html;
    }
}

La direttiva try_files $uri $uri/ /index.html è la chiave per il corretto funzionamento delle Single Page Application. Poiché Vue Router gestisce le URL lato client, rotte come /about o /products/42 non corrispondono a file fisici sul server. Senza questa direttiva, nginx restituirebbe un errore 404 per qualsiasi URL diverso dalla radice.

Abilitare il virtual host

In nginx, un virtual host viene abilitato creando un collegamento simbolico dalla cartella sites-available alla cartella sites-enabled:

# Crea il collegamento simbolico per abilitare il virtual host
sudo ln -s /etc/nginx/sites-available/my-app /etc/nginx/sites-enabled/

# Verifica la correttezza della sintassi della configurazione
sudo nginx -t

# Ricarica nginx per applicare le modifiche
sudo systemctl reload nginx

Il comando nginx -t è un controllo fondamentale da eseguire sempre prima di ricaricare il servizio, in quanto individua eventuali errori di sintassi che potrebbero impedire l'avvio di nginx.

Configurazione avanzata: compressione e cache

Per migliorare le prestazioni dell'applicazione in produzione, è opportuno abilitare la compressione gzip e impostare correttamente le intestazioni di cache HTTP. Questo riduce la quantità di dati trasferiti e migliora i tempi di caricamento per gli utenti.

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/my-app;
    index index.html;

    # Abilita la compressione gzip per ridurre la dimensione delle risposte
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        image/svg+xml;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Configurazione cache per gli asset con hash nel nome file
    location /assets/ {
        # I file con hash possono essere memorizzati in cache per un anno
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # index.html non deve mai essere messo in cache
    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        expires 0;
    }
}

La strategia di cache differenziata è importante: i file con hash (generati da Vite) possono avere una cache molto lunga perché cambiano nome a ogni aggiornamento del codice, mentre index.html deve essere sempre aggiornato affinché il browser scarichi le versioni più recenti degli asset.

Configurare HTTPS con Certbot e Let's Encrypt

Oggi un sito web in produzione deve essere servito tramite HTTPS. Let's Encrypt offre certificati SSL gratuiti e automatizzati, gestibili tramite lo strumento Certbot.

# Installa Certbot e il plugin per nginx
sudo apt install certbot python3-certbot-nginx -y

# Richiede e installa automaticamente il certificato SSL
sudo certbot --nginx -d example.com -d www.example.com

Certbot modificherà automaticamente il file di configurazione nginx aggiungendo le direttive necessarie per HTTPS e impostando un redirect dal traffico HTTP a HTTPS. Il rinnovo automatico del certificato viene gestito tramite un timer systemd o un'attività cron installata da Certbot stesso.

Per verificare che il rinnovo automatico funzioni correttamente, è possibile simularlo con:

# Simula il rinnovo del certificato senza modifiche effettive
sudo certbot renew --dry-run

Configurare nginx come reverse proxy (opzionale)

Spesso un'applicazione Vue.js comunica con un backend API. Se il backend gira sullo stesso server (ad esempio un'applicazione Node.js sulla porta 3000), è comodo configurare nginx come reverse proxy per indirizzare le richieste alle API evitando problemi di CORS.

server {
    listen 80;
    server_name example.com;
    root /var/www/my-app;
    index index.html;

    # Serve i file statici dell'applicazione Vue.js
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Inoltra le richieste alle API al backend locale
    location /api/ {
        # Passa le richieste al server backend in ascolto sulla porta 3000
        proxy_pass http://127.0.0.1:3000/;

        # Intestazioni necessarie per il corretto funzionamento del proxy
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Con questa configurazione, le chiamate a /api/users dall'applicazione Vue.js verranno inoltrate a http://127.0.0.1:3000/users, mentre tutte le altre richieste verranno gestite come file statici o indirizzate a index.html per il routing SPA.

Intestazioni di sicurezza

Una buona configurazione di produzione include anche intestazioni HTTP che migliorano la sicurezza dell'applicazione, proteggendola da attacchi comuni come il clickjacking, il cross-site scripting e il MIME sniffing:

server {
    listen 443 ssl;
    server_name example.com;
    root /var/www/my-app;
    index index.html;

    # Impedisce l'inclusione della pagina in iframe su altri domini
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Attiva il filtro XSS nei browser meno recenti
    add_header X-XSS-Protection "1; mode=block" always;

    # Impedisce al browser di cambiare il Content-Type dichiarato
    add_header X-Content-Type-Options "nosniff" always;

    # Forza HTTPS per un anno, inclusi i sottodomini
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Controlla le informazioni inviate nell'intestazione Referer
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Gestione dei log

nginx genera due tipi di log: il log degli accessi (access.log) e il log degli errori (error.log). Questi file sono essenziali per monitorare il traffico e diagnosticare problemi.

server {
    listen 80;
    server_name example.com;
    root /var/www/my-app;

    # Percorso personalizzato per i log di questa applicazione
    access_log /var/log/nginx/my-app.access.log;
    error_log  /var/log/nginx/my-app.error.log warn;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Per consultare i log in tempo reale durante il debug:

# Visualizza gli ultimi accessi in tempo reale
sudo tail -f /var/log/nginx/my-app.access.log

# Visualizza gli errori più recenti
sudo tail -f /var/log/nginx/my-app.error.log

Automatizzare il deployment

In un flusso di lavoro professionale, il deployment viene automatizzato tramite script o pipeline CI/CD. Di seguito un semplice script Bash che compila l'applicazione e aggiorna i file sul server:

#!/bin/bash

# Variabili di configurazione
REMOTE_USER="deploy"
REMOTE_HOST="your-server-ip"
REMOTE_DIR="/var/www/my-app"

# Installa le dipendenze e compila l'applicazione
echo "Compilazione in corso..."
npm ci
npm run build

# Trasferisce i file sul server e ricarica nginx
echo "Deployment in corso..."
rsync -avz --delete dist/ "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
ssh "$REMOTE_USER@$REMOTE_HOST" "sudo systemctl reload nginx"

echo "Deployment completato."

Per eseguire lo script è sufficiente renderlo eseguibile e avviarlo dalla radice del progetto:

# Rende il file eseguibile
chmod +x deploy.sh

# Esegue il deployment
./deploy.sh

Risoluzione dei problemi comuni

Errore 404 navigando direttamente a una rotta: il problema più frequente con le SPA. La causa è l'assenza della direttiva try_files $uri $uri/ /index.html nella configurazione. Verificare che sia presente nel blocco location /.

Pagina bianca dopo il deployment: spesso causato da percorsi degli asset errati. Verificare che la variabile base nel file vite.config.js corrisponda al percorso in cui l'applicazione è servita. Se l'app è sulla radice del dominio, il valore predefinito / è corretto.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  // Modificare se l'app è servita in una sottodirectory, es. '/my-app/'
  base: '/',
})

nginx restituisce 403 Forbidden: il processo nginx non ha i permessi per leggere i file. Rieseguire il comando chown e verificare che i permessi siano impostati correttamente (755 per le directory, 644 per i file).

# Corregge i permessi in modo ricorsivo
sudo chown -R www-data:www-data /var/www/my-app
sudo find /var/www/my-app -type d -exec chmod 755 {} \;
sudo find /var/www/my-app -type f -exec chmod 644 {} \;

Modifiche non visibili dopo il deployment: il browser potrebbe stare utilizzando la cache. Verificare che le intestazioni Cache-Control siano configurate correttamente per index.html e forzare un hard refresh nel browser con Ctrl+Shift+R.

Conclusioni

Configurare nginx per servire un'applicazione Vue.js richiede pochi passaggi ma è fondamentale comprenderli nel dettaglio, soprattutto per quanto riguarda il routing SPA e la gestione della cache. Una configurazione ben strutturata garantisce prestazioni elevate, sicurezza adeguata e un flusso di aggiornamento affidabile. Con l'aggiunta di HTTPS tramite Let's Encrypt, della compressione gzip e delle intestazioni di sicurezza, si ottiene una configurazione robusta e pronta per un ambiente di produzione reale.