Monitoring proattivo per Laravel su VPS unmanaged: come evitare di scoprire un downtime dalla telefonata del cliente

Monitoring proattivo per Laravel su VPS unmanaged: come evitare di scoprire un downtime dalla telefonata del cliente

Il 18 luglio 2025, un venerdì alle 16:40, il certificato Let's Encrypt di un e-commerce B2B Laravel ospitato su un VPS Contabo (6 vCPU, 16 GB RAM, 400 GB SSD) è scaduto. Il cron job di Certbot che avrebbe dovuto rinnovarlo automaticamente aveva smesso di funzionare tre mesi prima - il 14 aprile - quando un aggiornamento del sistema operativo aveva spostato il binario di Certbot da /usr/bin/certbot a /snap/bin/certbot senza aggiornare il crontab. Per tre mesi il rinnovo aveva fallito silenziosamente, scrivendo un errore in /var/log/letsencrypt/letsencrypt.log che nessuno leggeva. Il venerdì pomeriggio, alla scadenza del certificato, il sito è diventato irraggiungibile: gli header HSTS (HTTP Strict Transport Security) che l'applicazione inviava correttamente impedivano ai browser di accettare il fallback a HTTP in chiaro. Per l'utente finale il sito era semplicemente morto - nessuna pagina di errore, nessun messaggio, solo un ERR_CERT_DATE_INVALID che Chrome mostrava senza possibilità di bypass.

Il cliente - una PMI pugliese che vende componentistica idraulica industriale al mercato europeo, con un fatturato e-commerce di circa 2 milioni di euro - ha scoperto il problema il lunedì mattina alle 8:15, quando il primo commerciale ha provato ad accedere al backoffice per controllare gli ordini del weekend. Sessanta ore di downtime. Nessun alert, nessuna notifica, nessun SMS. Il VPS era tecnicamente online - SSH funzionava, MySQL era attivo, PHP-FPM era in esecuzione. Mancava solo il certificato SSL, e senza di quello il sito era un fantasma. Stima delle perdite: circa 80 ordini non completati nel weekend (basata sulla media storica), equivalenti a circa 35.000 euro di fatturato lordo.

Quando sono intervenuto il lunedì mattina, il fix tecnico è stato banale: aggiornare il path di Certbot nel crontab e forzare il rinnovo con certbot renew --force-renewal. Quindici minuti di lavoro. Ma il problema vero non era il certificato - era l'assenza totale di un sistema di monitoring che avrebbe potuto segnalare il problema tre mesi prima, quando il cron job aveva iniziato a fallire, o al massimo il venerdì pomeriggio, quando il certificato era scaduto. Questo articolo descrive lo stack di monitoring che ho installato su quel VPS e che da allora installo su ogni server che prendo in carico per clienti PMI.

Cosa deve monitorare un sistema di monitoring per essere davvero utile su un VPS unmanaged?

Un dashboard di Grafana con grafici colorati che nessuno guarda non è monitoring - è decorazione. Il monitoring che previene i downtime ha tre caratteristiche: misura le cose giuste, genera alert quando una soglia viene superata, e ogni alert ha un'azione associata. Se un alert suona e chi lo riceve non sa cosa fare, quell'alert è rumore, non segnale.

Lo stack che ha dimostrato nel tempo il miglior rapporto costo/efficacia su VPS unmanaged per PMI è composto da quattro componenti: Prometheus per la raccolta e lo storage delle metriche, Grafana per la visualizzazione, Alertmanager per l'instradamento degli alert, e Node Exporter per le metriche del sistema operativo. A livello applicativo, il pacchetto laravel-prometheus di Spatie espone metriche custom di Laravel in un formato che Prometheus sa leggere nativamente.

Le metriche si dividono in tre livelli, e tutti e tre devono essere coperti:

  • Infrastruttura: CPU, RAM, disco, I/O, network. Node Exporter le espone automaticamente. Il 90% dei downtime evitabili su VPS unmanaged nasce da disco pieno o RAM esaurita.
  • Applicazione: tempo di risposta HTTP, tasso di errori 5xx, profondità della coda Laravel, job falliti, connessioni al database. Queste sono le metriche che ti dicono se l'applicazione sta degradando prima che l'utente se ne accorga.
  • Business: transazioni completate per ora, email transazionali inviate con successo, scadenza dei certificati SSL, freschezza dell'ultimo backup. Queste sono le metriche che il proprietario dell'azienda capisce - e sono quelle che trasformano il monitoring da costo tecnico a investimento di business.

Stai cercando un Consulente Informatico esperto per mettere in sicurezza e monitorare la tua infrastruttura Laravel? Nel mio profilo professionale trovi l'esperienza concreta su VPS Hetzner, Contabo, OVH e Digital Ocean, monitoring proattivo e incident response per PMI.

Lo stack in pratica: Prometheus, Node Exporter e metriche Laravel

L'installazione di Prometheus e Node Exporter su un VPS Debian/Ubuntu è diretta. Node Exporter gira come servizio systemd sulla porta 9100 ed espone centinaia di metriche del sistema operativo senza nessuna configurazione applicativa. Prometheus viene configurato per interrogare Node Exporter e l'endpoint /prometheus dell'applicazione Laravel a intervalli regolari:

# /etc/prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

rule_files:
  - "/etc/prometheus/rules/*.yml"

scrape_configs:
  - job_name: "node"
    static_configs:
      - targets: ["localhost:9100"]

  - job_name: "laravel"
    metrics_path: "/prometheus"
    static_configs:
      - targets: ["localhost:80"]
    scheme: "https"
    tls_config:
      insecure_skip_verify: true

Sul lato Laravel, il pacchetto Spatie espone automaticamente metriche di base (richieste per secondo, tempo di risposta). Le metriche custom più utili che aggiungo su ogni installazione sono il contatore di job falliti in coda, la profondità della coda stessa, e un gauge che misura il tempo dell'ultima transazione completata con successo - perché se l'e-commerce è online ma nessuno riesce a completare un ordine, il monitoring infrastrutturale non ti dice nulla:

// app/Providers/PrometheusServiceProvider.php
use Spatie\Prometheus\Facades\Prometheus;

public function register(): void
{
    Prometheus::addGauge('Profondità coda Laravel')
        ->name('laravel_queue_depth')
        ->helpText('Numero di job in attesa nella coda')
        ->value(fn () => DB::table('jobs')->count());

    Prometheus::addGauge('Job falliti')
        ->name('laravel_failed_jobs_total')
        ->helpText('Numero totale di job falliti')
        ->value(fn () => DB::table('failed_jobs')->count());

    Prometheus::addGauge('Ultimo ordine completato (secondi fa)')
        ->name('laravel_last_order_seconds_ago')
        ->helpText('Secondi trascorsi dall ultimo ordine completato')
        ->value(function () {
            $last = DB::table('orders')
                ->where('status', 'completed')
                ->max('created_at');
            return $last ? now()->diffInSeconds($last) : -1;
        });
}

Per una guida più completa sull'integrazione tra Laravel, Prometheus e Grafana con esempi di dashboard e configurazione avanzata, Better Stack ha pubblicato una delle guide più dettagliate disponibili - la consiglio come riferimento per chi vuole approfondire oltre il setup di base che descrivo qui.

Le regole di alerting che prevengono i downtime reali

Prometheus senza Alertmanager è un archivio storico: utile per l'analisi post-mortem, inutile per la prevenzione. Alertmanager prende le regole definite in Prometheus e le trasforma in notifiche - email, Telegram, Slack, webhook. La configurazione che uso come baseline su ogni VPS copre i sette scenari che nella mia esperienza causano il 95% dei downtime evitabili:

# /etc/prometheus/rules/vps-alerts.yml
groups:
  - name: infrastruttura
    rules:
      - alert: DiscoQuasiPieno
        expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Disco root sopra l'85%"
          runbook: "Controllare log rotation, /tmp, /var/log, storage/logs"

      - alert: RAMCritica
        expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.90
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "RAM sopra il 90% da 5 minuti"
          runbook: "Verificare OOM killer, processi zombie, memory leak PHP-FPM"

  - name: applicazione
    rules:
      - alert: ErrorRate5xx
        expr: rate(laravel_http_responses_total{status=~"5.."}[5m]) / rate(laravel_http_responses_total[5m]) > 0.01
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "Tasso errori 5xx sopra l'1%"
          runbook: "Controllare storage/logs/laravel.log, stato MySQL, stato Redis"

      - alert: CodaTroppoLunga
        expr: laravel_queue_depth > 1000
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Più di 1000 job in coda da 10 minuti"
          runbook: "Verificare stato Horizon, worker attivi, job bloccanti"

      - alert: JobFalliti
        expr: laravel_failed_jobs_total > 0
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Presenti job falliti nella tabella failed_jobs"
          runbook: "php artisan queue:failed, analizzare eccezione, retry o delete"

  - name: business
    rules:
      - alert: CertificatoInScadenza
        expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 14
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Certificato SSL scade entro 14 giorni"
          runbook: "certbot renew --dry-run, verificare crontab certbot"

      - alert: BackupAssente
        expr: time() - laravel_last_backup_timestamp > 90000
        for: 30m
        labels:
          severity: critical
        annotations:
          summary: "Nessun backup completato nelle ultime 25 ore"
          runbook: "Verificare spatie/laravel-backup, spazio disco remoto, credenziali S3"

Ogni alert ha un campo runbook - una riga che dice esattamente cosa controllare. Questo è il dettaglio che separa il monitoring che funziona dal monitoring che genera ansia: quando il tuo telefono vibra alle 2 di notte con "DiscoQuasiPieno", non devi pensare. Leggi il runbook, esegui i comandi, il problema è circoscritto. Per l'alert CertificatoInScadenza - quello che avrebbe evitato le 60 ore di downtime del cliente pugliese - serve Blackbox Exporter, un componente aggiuntivo di Prometheus che fa probe HTTPS e verifica la scadenza del certificato dall'esterno. Sul cliente pugliese, questa regola da sola avrebbe generato un alert il 4 luglio - 14 giorni prima della scadenza - dando tutto il tempo necessario per diagnosticare il problema con Certbot e risolverlo senza alcun impatto sull'operatività.

Alertmanager: dove mandare gli alert perché qualcuno li legga

La configurazione di Alertmanager determina chi riceve quale alert e attraverso quale canale. La regola che applico è semplice: i warning vanno su un canale Telegram o Slack di team, i critical vanno anche via email al responsabile IT e via SMS (tramite webhook a un servizio come Twilio o MessageBird):

# /etc/alertmanager/alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'team-telegram'
  routes:
    - match:
        severity: critical
      receiver: 'responsabile-it'

receivers:
  - name: 'team-telegram'
    telegram_configs:
      - bot_token: '{{ .TelegramBotToken }}'
        chat_id: -100XXXXXXXXXX
        send_resolved: true
        message: '{{ template "telegram.default.message" . }}'

  - name: 'responsabile-it'
    telegram_configs:
      - bot_token: '{{ .TelegramBotToken }}'
        chat_id: -100XXXXXXXXXX
        send_resolved: true
    email_configs:
      - to: '[email protected]'
        from: '[email protected]'
        smarthost: 'smtp.azienda.it:587'
        require_tls: true

Il parametro repeat_interval: 4h è critico: senza di esso, un alert che resta attivo per giorni (come un disco che cresce lentamente) genera notifiche ogni 5 minuti - e dopo il primo giorno il team le ignora tutte. Con un repeat ogni 4 ore, l'alert ricorda la situazione senza diventare rumore bianco.

Health check applicativo: il livello che manca alla maggior parte dei setup

Prometheus e Node Exporter ti dicono se il server è vivo. Le metriche Laravel ti dicono se l'applicazione risponde. Ma nessuno dei due ti dice se l'applicazione funziona correttamente dal punto di vista del business. Per questo serve un health check applicativo dedicato - un endpoint che verifica non solo che Laravel risponda, ma che tutti i servizi da cui dipende siano raggiungibili e funzionanti:

// routes/web.php
Route::get('/health', function () {
    $checks = [];

    // Database
    try {
        DB::select('SELECT 1');
        $checks['database'] = 'ok';
    } catch (\Throwable $e) {
        $checks['database'] = 'fail: ' . $e->getMessage();
    }

    // Redis
    try {
        Redis::ping();
        $checks['redis'] = 'ok';
    } catch (\Throwable $e) {
        $checks['redis'] = 'fail: ' . $e->getMessage();
    }

    // Storage scrivibile
    try {
        $path = storage_path('app/.health_check');
        file_put_contents($path, time());
        unlink($path);
        $checks['storage'] = 'ok';
    } catch (\Throwable $e) {
        $checks['storage'] = 'fail: ' . $e->getMessage();
    }

    // Spazio disco
    $freePercent = disk_free_space('/') / disk_total_space('/') * 100;
    $checks['disk_free_percent'] = round($freePercent, 1);

    $allOk = !in_array(false, array_map(
        fn ($v) => is_string($v) ? !str_starts_with($v, 'fail') : true,
        $checks
    ));

    return response()->json($checks, $allOk ? 200 : 503);
});

Questo endpoint restituisce 200 se tutto funziona, 503 se qualcosa è guasto. Blackbox Exporter di Prometheus lo interroga ogni 15 secondi, e l'alert scatta dopo 2 check consecutivi con stato non-200. In Laravel 12 esiste anche il facade Health nativo che offre un meccanismo simile out of the box, ma nella mia esperienza l'endpoint custom resta preferibile perché ti dà il controllo totale su cosa verificare e come esporre i risultati - e soprattutto perché puoi aggiungere check specifici del tuo business (ultimo ordine, ultima fattura emessa, ultimo backup riuscito) che nessun pacchetto generico può conoscere in anticipo. Per chi gestisce applicazioni PHP legacy senza Laravel, ho descritto un approccio analogo - logging strutturato, metriche essenziali e alert senza riscrivere il codice - nel mio articolo sull'osservabilità minima per applicazioni PHP legacy.

Sul cliente pugliese, dopo l'installazione completa dello stack, il primo alert utile è arrivato 11 giorni dopo: DiscoQuasiPieno al 86%. La causa: log di Laravel che non ruotavano perché il daily channel in config/logging.php era configurato con days => 365 - un anno di log accumulati. Il fix è stato cambiare il valore a 14 giorni e aggiungere logrotate come safety net. Senza l'alert, quel disco si sarebbe riempito entro tre settimane, causando un altro downtime - diverso dal certificato, ma con lo stesso risultato per il business.

Per chi vuole approfondire cosa fare quando il monitoring rileva un incidente reale e bisogna intervenire in emergenza, ho descritto il protocollo completo di incident response in 72 ore per Laravel e Symfony, allineato a NIS2 - che copre contenimento, forensics, ripristino e notifiche obbligatorie.

Se gestisci un VPS unmanaged con applicazioni Laravel in produzione e il tuo sistema di monitoring è "il cliente mi chiama quando il sito non funziona", stai operando alla cieca su un'infrastruttura che genera fatturato. Il costo di un setup Prometheus + Grafana + Alertmanager su un VPS è di mezza giornata di lavoro iniziale e zero costi ricorrenti - il software è tutto open source. Il costo di non averlo è il prossimo downtime che scoprirai dalla telefonata del lunedì mattina. Contattami se vuoi uno stack di monitoring operativo sulla tua infrastruttura: in una giornata installo, configuro e cablaggio gli alert su tutti e tre i livelli - infrastruttura, applicazione e business - con runbook per ogni scenario.

Ultima modifica: