1. Home
  2. Academy
  3. Vibe Coding Professionisti
  4. n8n: automazioni con l'AI senza scrivere codice
Vibe Coding Professionisti Vibe Coding Professionisti · Capitolo 6

n8n: automazioni con l'AI senza scrivere codice

Postazione di lavoro con laptop: automazione e intelligenza artificiale
Immagine: konkapo · Pixabay · Licenza dei contenuti Pixabay

Quando copiare i dati diventa un lavoro

Un contatto compila il modulo del sito. Copi i dati nel CRM, prepari l’email di benvenuto, aggiungi un’attività in Asana e avvisi il team su Slack. Se ogni contatto richiede 15 minuti, con 20 contatti a settimana dedichi 5 ore a queste operazioni. Nei passaggi manuali puoi perdere una richiesta, inviare due email o dimenticare un’attività.

Se hai provato Zapier o Make, conosci le voci da valutare: costi mensili, dove passano i dati, numero di operazioni e dipendenza dal fornitore. Qui il confronto considera una spesa di €50-200 al mese per volumi di lavoro reali.

In questo capitolo installi n8n sul tuo server e costruisci flussi visuali senza limiti imposti al loro numero. La configurazione presentata usa una VPS da €5-10 al mese, lascia a te il controllo dei dati e offre oltre 400 nodi nativi, con la possibilità di aggiungere codice. Vediamo come costruire il flusso e mantenerlo in funzione senza dipendere da un servizio ospitato dal fornitore.


I concetti di n8n da cui partire

ConcettoCos'èAnalogo
NodeBlocco funzionale (trigger, action, logic)Funzione/lego
WorkflowGrafo orientato di nodi collegatiProgramma/flowchart
CredentialsAuth salvate criptate (OAuth, API key, Basic)Password manager
Expression{{ $json.field }} per passare dati tra nodiVariabili/piping
ExecutionSingola run del workflow (manual, trigger, webhook)Process run
WebhookEndpoint HTTP pubblico per trigger esterniAPI endpoint

I nodi usati negli esempi

CategoriaNodi chiaveUso tipico
TriggerWebhook, Cron, n8n Form, Email TriggerInizio workflow
LogicIF, Switch, Merge, SplitInBatches, Set, CodeControllo flusso
DataHTTP Request, GraphQL, Webhook RespondAPI integration
AppGmail, Outlook, Slack, HubSpot, Pipedrive, Asana, Notion, Airtable, Google SheetsSaaS connectors
AIOpenAI, Anthropic, LangChain, Custom LLMAI steps
StorageRead/Write Binary File, Redis, Postgres, MongoDBPersistence

Flusso 1: dal modulo del sito al CRM e alle notifiche

Il JSON da importare in n8n (Workflows → Import)

Il webhook è pubblico, quindi prima di creare contatti e inviare email il flusso verifica il token Cloudflare Turnstile, controlla che il campo nascosto anti-bot (honeypot) sia vuoto e che l'informativa privacy sia stata accettata. Le richieste che non passano ricevono un errore 400 e non toccano CRM né email. I nomi dei parametri dei nodi cambiano tra le versioni di n8n: dopo l'import apri ogni nodo e controlla i campi.

{
  "name": "Lead to Cash - Complete",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "lead-capture",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "1",
      "name": "Webhook - Lead Form",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [200, 300],
      "webhookId": "lead-capture"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://challenges.cloudflare.com/turnstile/v0/siteverify",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {"name": "secret", "value": "={{ $env.TURNSTILE_SECRET }}"},
            {"name": "response", "value": "={{ $json.body['cf-turnstile-response'] }}"}
          ]
        },
        "options": {}
      },
      "id": "2",
      "name": "HTTP - Verify Turnstile",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [400, 300]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {"value1": "={{ $json.success === true && !$('Webhook - Lead Form').item.json.body.website && $('Webhook - Lead Form').item.json.body.privacy === 'on' }}", "value2": true}
          ]
        }
      },
      "id": "3",
      "name": "IF - Human, No Honeypot, Privacy OK",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [600, 300]
    },
    {
      "parameters": {
        "fields": {
          "values": [
            {"name": "email", "value": "={{ $('Webhook - Lead Form').item.json.body.email }}"},
            {"name": "first_name", "value": "={{ $('Webhook - Lead Form').item.json.body.first_name }}"},
            {"name": "last_name", "value": "={{ $('Webhook - Lead Form').item.json.body.last_name }}"},
            {"name": "company", "value": "={{ $('Webhook - Lead Form').item.json.body.company }}"},
            {"name": "phone", "value": "={{ $('Webhook - Lead Form').item.json.body.phone }}"},
            {"name": "source", "value": "website_form"},
            {"name": "utm_source", "value": "={{ $('Webhook - Lead Form').item.json.body.utm_source }}"},
            {"name": "utm_medium", "value": "={{ $('Webhook - Lead Form').item.json.body.utm_medium }}"},
            {"name": "utm_campaign", "value": "={{ $('Webhook - Lead Form').item.json.body.utm_campaign }}"},
            {"name": "message", "value": "={{ $('Webhook - Lead Form').item.json.body.message }}"},
            {"name": "marketing_consent", "value": "={{ $('Webhook - Lead Form').item.json.body.marketing_consent === 'on' }}"}
          ]
        },
        "options": {}
      },
      "id": "4",
      "name": "Set - Normalize Lead Data",
      "type": "n8n-nodes-base.set",
      "typeVersion": 2,
      "position": [800, 200]
    },
    {
      "parameters": {
        "jsCode": "return $input.all().map(i => ({json: {...i.json, lead_score: 50 + (i.json.utm_source ? 10 : 0) + (i.json.company ? 15 : 0) + (i.json.phone ? 10 : 0)}}));"
      },
      "id": "5",
      "name": "Code - Calculate Lead Score",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1000, 200]
    },
    {
      "parameters": {
        "operation": "create",
        "resource": "contact",
        "properties": {
          "email": "={{ $json.email }}",
          "firstname": "={{ $json.first_name }}",
          "lastname": "={{ $json.last_name }}",
          "company": "={{ $json.company }}",
          "phone": "={{ $json.phone }}",
          "lifecyclestage": "lead",
          "hs_lead_status": "NEW",
          "website_form_source": "={{ $json.source }}",
          "utm_source": "={{ $json.utm_source }}",
          "utm_medium": "={{ $json.utm_medium }}",
          "utm_campaign": "={{ $json.utm_campaign }}",
          "marketing_consent": "={{ $json.marketing_consent }}"
        },
        "options": {}
      },
      "id": "6",
      "name": "HubSpot - Create/Update Contact",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 3,
      "position": [1200, 200],
      "credentials": {"hubspotOAuth2Api": "HubSpot Account"},
      "retryOnFail": true,
      "maxTries": 3
    },
    {
      "parameters": {
        "operation": "create",
        "resource": "deal",
        "properties": {
          "dealname": "={{ $('Set - Normalize Lead Data').item.json.first_name }} {{ $('Set - Normalize Lead Data').item.json.last_name }} - {{ $('Set - Normalize Lead Data').item.json.company }}",
          "amount": 0,
          "dealstage": "appointmentscheduled",
          "pipeline": "default",
          "hubspot_owner_id": "={{ $env.HUBSPOT_OWNER_ID }}"
        },
        "options": {}
      },
      "id": "7",
      "name": "HubSpot - Create Deal",
      "type": "n8n-nodes-base.hubspot",
      "typeVersion": 3,
      "position": [1400, 0],
      "credentials": {"hubspotOAuth2Api": "HubSpot Account"}
    },
    {
      "parameters": {
        "operation": "create",
        "resource": "contact",
        "fields": {
          "email": "={{ $json.email }}",
          "first_name": "={{ $json.first_name }}",
          "last_name": "={{ $json.last_name }}",
          "organization_name": "={{ $json.company }}",
          "phone": "={{ $json.phone }}",
          "custom_fields": {
            "source": "website_form",
            "utm_source": "={{ $json.utm_source }}",
            "utm_campaign": "={{ $json.utm_campaign }}"
          }
        },
        "options": {}
      },
      "id": "8",
      "name": "Pipedrive - Create Person + Org",
      "type": "n8n-nodes-base.pipedrive",
      "typeVersion": 1,
      "position": [1200, 400],
      "credentials": {"pipedriveApi": "Pipedrive Account"},
      "disabled": true
    },
    {
      "parameters": {
        "fromEmail": "noreply@tuodominio.com",
        "toEmail": "={{ $('Set - Normalize Lead Data').item.json.email }}",
        "subject": "=Grazie {{ $('Set - Normalize Lead Data').item.json.first_name }}! Ricevuta la tua richiesta",
        "text": "=Ciao {{ $('Set - Normalize Lead Data').item.json.first_name }},\n\nGrazie per averci contattato. Il nostro team ti risponderà entro 24h.\n\nA presto,\nIl team di TuaAzienda",
        "options": {}
      },
      "id": "9",
      "name": "Gmail - Send Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 1,
      "position": [1400, 100],
      "credentials": {"gmailOAuth2": "Gmail Account"}
    },
    {
      "parameters": {
        "workspace": "={{ $env.ASANA_WORKSPACE_ID }}",
        "project": "={{ $env.ASANA_LEADS_PROJECT_ID }}",
        "name": "=Follow-up: {{ $('Set - Normalize Lead Data').item.json.first_name }} {{ $('Set - Normalize Lead Data').item.json.last_name }} ({{ $('Set - Normalize Lead Data').item.json.company }})",
        "notes": "=Lead da form sito\nEmail: {{ $('Set - Normalize Lead Data').item.json.email }}\nTelefono: {{ $('Set - Normalize Lead Data').item.json.phone }}\nMessaggio: {{ $('Set - Normalize Lead Data').item.json.message }}\nUTM: {{ $('Set - Normalize Lead Data').item.json.utm_source }}/{{ $('Set - Normalize Lead Data').item.json.utm_medium }}/{{ $('Set - Normalize Lead Data').item.json.utm_campaign }}\nLead Score: {{ $('Code - Calculate Lead Score').item.json.lead_score }}\nHubSpot Contact: {{ $('HubSpot - Create/Update Contact').item.json.id }}",
        "assignee": "={{ $env.ASANA_SALES_OWNER_ID }}",
        "dueDate": "={{ new Date(Date.now() + 24*60*60*1000).toISOString().split('T')[0] }}",
        "options": {}
      },
      "id": "10",
      "name": "Asana - Create Follow-up Task",
      "type": "n8n-nodes-base.asana",
      "typeVersion": 1,
      "position": [1400, 200],
      "credentials": {"asanaOAuth2Api": "Asana Account"}
    },
    {
      "parameters": {
        "channel": "#leads",
        "text": "=🎯 *Nuovo Lead!*\n*Nome:* {{ $('Set - Normalize Lead Data').item.json.first_name }} {{ $('Set - Normalize Lead Data').item.json.last_name }}\n*Azienda:* {{ $('Set - Normalize Lead Data').item.json.company }}\n*Email:* {{ $('Set - Normalize Lead Data').item.json.email }}\n*Telefono:* {{ $('Set - Normalize Lead Data').item.json.phone || 'N/A' }}\n*Fonte:* {{ $('Set - Normalize Lead Data').item.json.utm_source || 'direct' }}/{{ $('Set - Normalize Lead Data').item.json.utm_medium || '' }}\n*Score:* {{ $('Code - Calculate Lead Score').item.json.lead_score }}\n*Messaggio:* {{ $('Set - Normalize Lead Data').item.json.message?.substring(0, 200) || 'N/A' }}\n\n<@{{ $env.SLACK_SALES_USER_ID }}>, assegna e contatta entro 24h.",
        "options": {"username": "Lead Bot", "iconEmoji": "🎯"}
      },
      "id": "11",
      "name": "Slack - Notify Sales Team",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 1,
      "position": [1400, 300],
      "credentials": {"slackApi": "Slack Bot"}
    },
    {
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseCode": 200,
        "responseData": "={{ {success: true, message: 'Lead ricevuto, ti contatteremo presto', lead_id: $('HubSpot - Create/Update Contact').item.json.id} }}"
      },
      "id": "12",
      "name": "Webhook Respond - Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [1400, 400]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseCode": 400,
        "responseData": "={{ {success: false, message: 'Richiesta non valida: verifica anti-bot o consenso mancante'} }}"
      },
      "id": "13",
      "name": "Webhook Respond - Rejected",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [800, 450]
    }
  ],
  "connections": {
    "Webhook - Lead Form": {"main": [[{"node": "HTTP - Verify Turnstile", "type": "main", "index": 0}]]},
    "HTTP - Verify Turnstile": {"main": [[{"node": "IF - Human, No Honeypot, Privacy OK", "type": "main", "index": 0}]]},
    "IF - Human, No Honeypot, Privacy OK": {"main": [
      [{"node": "Set - Normalize Lead Data", "type": "main", "index": 0}],
      [{"node": "Webhook Respond - Rejected", "type": "main", "index": 0}]
    ]},
    "Set - Normalize Lead Data": {"main": [[{"node": "Code - Calculate Lead Score", "type": "main", "index": 0}]]},
    "Code - Calculate Lead Score": {"main": [[
      {"node": "HubSpot - Create/Update Contact", "type": "main", "index": 0},
      {"node": "Pipedrive - Create Person + Org", "type": "main", "index": 0}
    ]]},
    "HubSpot - Create/Update Contact": {"main": [[
      {"node": "HubSpot - Create Deal", "type": "main", "index": 0},
      {"node": "Gmail - Send Welcome Email", "type": "main", "index": 0},
      {"node": "Asana - Create Follow-up Task", "type": "main", "index": 0},
      {"node": "Slack - Notify Sales Team", "type": "main", "index": 0},
      {"node": "Webhook Respond - Success", "type": "main", "index": 0}
    ]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"},
  "meta": {"templateCredsSetupCompleted": true}
}

Come usarlo

  1. Importa in n8n: Workflows → Import → incolla JSON
  2. Configura le credenziali (una volta):
    • HubSpot OAuth2: collega l’account
    • Gmail OAuth2: collega l’account
    • Asana OAuth2: collega l’account
    • Slack Bot: inserisci il token del bot
    • Pipedrive API: inserisci il token API (opzionale, disabilitato inizialmente)
    • In HubSpot crea la proprietà personalizzata marketing_consent (sì/no)
  3. Definisci le variabili nel blocco environment del docker-compose (o usa le Variables $vars sui piani che le includono); per i dati sensibili preferisci le Credentials. Da n8n 2.0 N8N_BLOCK_ENV_ACCESS_IN_NODE vale true di default: per usare $env devi impostarlo a false, ma così i nodi Code leggono tutte le variabili del container, chiave di cifratura compresa. Per gli ID non sensibili è più sicuro scriverli direttamente nei campi del nodo.
    HUBSPOT_OWNER_ID=123456
    ASANA_WORKSPACE_ID=123456
    ASANA_LEADS_PROJECT_ID=789012
    ASANA_SALES_OWNER_ID=me
    SLACK_SALES_USER_ID=U123456
    TURNSTILE_SECRET=la-secret-key-di-turnstile
  4. Attiva workflow → copia Webhook URL: https://tuon8n.com/webhook/lead-capture
  5. Integra nel form sito (con il widget Cloudflare Turnstile, un campo honeypot nascosto e i consensi):
    <form id="lead-form" action="https://tuon8n.com/webhook/lead-capture" method="POST">
      <input name="email" type="email" required>
      <input name="first_name" required>
      <input name="last_name" required>
      <input name="company">
      <input name="phone" type="tel">
      <input name="utm_source" type="hidden" value="">
      <input name="utm_medium" type="hidden" value="">
      <input name="utm_campaign" type="hidden" value="">
      <textarea name="message"></textarea>
      <!-- Honeypot: invisibile alle persone, i bot lo compilano -->
      <input name="website" type="text" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">
      <label><input type="checkbox" name="privacy" required> Ho letto l'<a href="/privacy">informativa privacy</a></label>
      <label><input type="checkbox" name="marketing_consent"> Acconsento a ricevere email informative e commerciali (facoltativo)</label>
      <div class="cf-turnstile" data-sitekey="LA_TUA_SITE_KEY"></div>
      <button type="submit">Invia</button>
    </form>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
    <script>
      // Auto-popola UTM da URL
      const params = new URLSearchParams(window.location.search);
      ['utm_source','utm_medium','utm_campaign'].forEach(p => {
        if(params.get(p)) document.querySelector(`[name="${p}"]`).value = params.get(p);
      });
    </script>

Flusso 2: una sequenza di email dopo il primo contatto

La sequenza invia email commerciali, quindi parte solo per i contatti con marketing_consent = true (la casella facoltativa, non preselezionata, del form). Ogni email contiene le istruzioni per disiscriversi: quando qualcuno lo chiede, imposta subito marketing_consent a false in HubSpot. I nodi Wait distanziano gli invii di 2 e 4 giorni; il primo aggiornamento di stato evita che lo stesso contatto venga ripreso dal Cron del giorno dopo.

{
  "name": "Lead Nurture - 3 Email Sequence",
  "nodes": [
    {"name": "Cron Daily Check", "type": "n8n-nodes-base.cron", "typeVersion": 1, "position": [200, 200], "parameters": {"triggerTimes": {"item": [{"mode": "everyDay", "hour": 10, "minute": 0}]}}},
    {"name": "HubSpot - Get New Leads", "type": "n8n-nodes-base.hubspot", "typeVersion": 3, "position": [400, 200], "parameters": {"resource": "contact", "operation": "getAll", "filters": {"properties": {"lifecyclestage": "lead", "hs_lead_status": "NEW", "marketing_consent": "true"}, "limit": 50}}},
    {"name": "Mark In Nurture", "type": "n8n-nodes-base.hubspot", "typeVersion": 3, "position": [600, 200], "parameters": {"resource": "contact", "operation": "update", "contactId": "={{ $json.id }}", "updateFields": {"hs_lead_status": "IN_PROGRESS"}}},
    {"name": "Day 1 - Value Email", "type": "n8n-nodes-base.gmail", "typeVersion": 1, "position": [800, 200], "parameters": {"toEmail": "={{ $('HubSpot - Get New Leads').item.json.properties.email }}", "subject": "={{ $('HubSpot - Get New Leads').item.json.properties.firstname }}, ecco la guida che promettevo", "text": "=Ciao {{ $('HubSpot - Get New Leads').item.json.properties.firstname }},\n\nCome promesso, ecco la nostra guida \"{{ $env.LEAD_MAGNET_TITLE }}\":\n{{ $env.LEAD_MAGNET_URL }}\n\nPunti chiave:\n1. {{ $env.KEY_POINT_1 }}\n2. {{ $env.KEY_POINT_2 }}\n3. {{ $env.KEY_POINT_3 }}\n\nDomande? Rispondi a questa email.\n\n{{ $env.SENDER_NAME }}\n\nNon vuoi più ricevere queste email? Rispondi \"Cancellami\" e ti rimuoviamo subito."}},
    {"name": "Wait 2 Days", "type": "n8n-nodes-base.wait", "typeVersion": 1, "position": [1000, 200], "parameters": {"resume": "timeInterval", "amount": 2, "unit": "days"}},
    {"name": "Day 3 - Case Study", "type": "n8n-nodes-base.gmail", "typeVersion": 1, "position": [1200, 200], "parameters": {"toEmail": "={{ $('HubSpot - Get New Leads').item.json.properties.email }}", "subject": "=Come {{ $env.CASE_STUDY_CLIENT }} ha ottenuto {{ $env.CASE_STUDY_RESULT }}", "text": "=Ciao {{ $('HubSpot - Get New Leads').item.json.properties.firstname }},\n\nti racconto in breve un progetto reale: {{ $env.CASE_STUDY_CLIENT }} aveva un problema simile al tuo e ha ottenuto {{ $env.CASE_STUDY_RESULT }}.\nTrovi il caso completo qui: {{ $env.CASE_STUDY_URL }}\n\nSe vuoi capire se un approccio simile funziona anche per te, rispondi a questa email.\n\n{{ $env.SENDER_NAME }}\n\nNon vuoi più ricevere queste email? Rispondi \"Cancellami\" e ti rimuoviamo subito."}},
    {"name": "Wait 4 Days", "type": "n8n-nodes-base.wait", "typeVersion": 1, "position": [1400, 200], "parameters": {"resume": "timeInterval", "amount": 4, "unit": "days"}},
    {"name": "Day 7 - Soft Pitch", "type": "n8n-nodes-base.gmail", "typeVersion": 1, "position": [1600, 200], "parameters": {"toEmail": "={{ $('HubSpot - Get New Leads').item.json.properties.email }}", "subject": "=Pronto per il prossimo passo, {{ $('HubSpot - Get New Leads').item.json.properties.firstname }}?", "text": "=Ciao {{ $('HubSpot - Get New Leads').item.json.properties.firstname }},\n\nnegli ultimi giorni ti ho mandato la guida e un caso concreto. Se vuoi applicare queste idee alla tua azienda, prenota una call gratuita di 20 minuti: {{ $env.BOOKING_URL }}\n\nNessun impegno: se non è il momento giusto, va benissimo così.\n\n{{ $env.SENDER_NAME }}\n\nNon vuoi più ricevere queste email? Rispondi \"Cancellami\" e ti rimuoviamo subito."}},
    {"name": "Update Lead Status", "type": "n8n-nodes-base.hubspot", "typeVersion": 3, "position": [1800, 200], "parameters": {"resource": "contact", "operation": "update", "contactId": "={{ $('HubSpot - Get New Leads').item.json.id }}", "updateFields": {"hs_lead_status": "CONNECTED"}}}
  ],
  "connections": {
    "Cron Daily Check": {"main": [[{"node": "HubSpot - Get New Leads", "type": "main", "index": 0}]]},
    "HubSpot - Get New Leads": {"main": [[{"node": "Mark In Nurture", "type": "main", "index": 0}]]},
    "Mark In Nurture": {"main": [[{"node": "Day 1 - Value Email", "type": "main", "index": 0}]]},
    "Day 1 - Value Email": {"main": [[{"node": "Wait 2 Days", "type": "main", "index": 0}]]},
    "Wait 2 Days": {"main": [[{"node": "Day 3 - Case Study", "type": "main", "index": 0}]]},
    "Day 3 - Case Study": {"main": [[{"node": "Wait 4 Days", "type": "main", "index": 0}]]},
    "Wait 4 Days": {"main": [[{"node": "Day 7 - Soft Pitch", "type": "main", "index": 0}]]},
    "Day 7 - Soft Pitch": {"main": [[{"node": "Update Lead Status", "type": "main", "index": 0}]]}
  }
}

⚠️ Prima di Day 3 e Day 7 conviene rileggere il contatto da HubSpot e fermarsi se marketing_consent è diventato false nel frattempo: chi si disiscrive non deve ricevere le email successive.


Installa n8n sul tuo server con Docker Compose

docker-compose.yml (VPS €5-10/mese, es. Hetzner CX22, DigitalOcean Droplet)

services:
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    # Nessuna porta pubblicata: n8n è raggiungibile solo tramite Traefik in HTTPS
    environment:
      # Al primo avvio crea subito l'account owner: da n8n 1.0 la gestione utenti è obbligatoria

      # Host & Protocol (dietro reverse proxy)
      - N8N_HOST=${N8N_HOST:-n8n.tuodominio.com}
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${N8N_HOST:-n8n.tuodominio.com}/
      
      # Database (PostgreSQL per produzione)
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
      - DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      
      # Security
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}  # GENERA: openssl rand -hex 32
      - N8N_PERSONALIZATION_ENABLED=false
      - N8N_TEMPLATES_ENABLED=true
      - N8N_ONBOARDING_FLOW_DISABLED=true
      
      # Performance
      - EXECUTIONS_TIMEOUT=3600
      - EXECUTIONS_TIMEOUT_MAX=7200
      
      # Logging
      - N8N_LOG_LEVEL=info
      - N8N_LOG_OUTPUT=console
      
      # Timezone
      - GENERIC_TIMEZONE=Europe/Rome
      - TZ=Europe/Rome

    volumes:
      - n8n_data:/home/node/.n8n
      - ./local-files:/files  # Per file locali (Read/Write Binary File)
      - ./backups:/backups    # Export creati dallo script di backup
    depends_on:
      - postgres
    networks:
      - n8n_network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.n8n.rule=Host(`${N8N_HOST}`)"
      - "traefik.http.routers.n8n.entrypoints=websecure"
      - "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
      - "traefik.http.services.n8n.loadbalancer.server.port=5678"

  postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    environment:
      - POSTGRES_DB=${POSTGRES_DB:-n8n}
      - POSTGRES_USER=${POSTGRES_USER:-n8n}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - n8n_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-n8n}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Reverse Proxy (Traefik) - opzionale ma raccomandato
  traefik:
    image: traefik:v3.0
    container_name: n8n-traefik
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"
    networks:
      - n8n_network

volumes:
  n8n_data:
  postgres_data:

networks:
  n8n_network:
    driver: bridge

.env (mai committare!)

# Genera con: openssl rand -hex 32
N8N_ENCRYPTION_KEY=your-64-char-hex-key-here
N8N_HOST=n8n.tuodominio.com

POSTGRES_DB=n8n
POSTGRES_USER=n8n
POSTGRES_PASSWORD=your-db-password-here

LETSENCRYPT_EMAIL=admin@tuodominio.com

Conserva N8N_ENCRYPTION_KEY nel password manager: senza la chiave le credenziali non si ripristinano.

I comandi per avviare il servizio

# Su VPS
git clone https://github.com/tuo-repo/n8n-selfhosted.git
cd n8n-selfhosted
cp .env.example .env
# Modifica .env con valori reali
docker compose up -d

# Verifica
docker compose logs -f n8n
# Apri https://n8n.tuodominio.com e crea subito l'account owner

Come salvare e ripristinare i dati

Backup automatico dall'host

Il backup gira sull'host con cron, non dentro un workflow: da n8n 2.0 Execute Command è disabilitato di default e il container n8n non contiene gli strumenti per copiare i file altrove.

#!/usr/bin/env bash
# backup_n8n.sh: da cron sull'host, es. "0 3 * * * /opt/n8n-selfhosted/backup_n8n.sh"
set -euo pipefail
cd /opt/n8n-selfhosted
mkdir -p backups
chown 1000:1000 backups   # n8n nel container gira come utente 1000: deve poter scrivere qui
docker compose exec -T postgres pg_dump -U n8n --clean --if-exists n8n > "backups/backup_$(date +%F).sql"
docker compose exec -T n8n n8n export:workflow --all --output="/backups/workflows_$(date +%F).json"
docker compose exec -T n8n n8n export:credentials --all --output="/backups/credentials_$(date +%F).json"
find backups -name '*.sql' -mtime +30 -delete
find backups -name '*.json' -mtime +30 -delete

Copia poi la cartella backups su un altro server o storage (per esempio con rclone installato sull'host). Le credenziali esportate restano cifrate con N8N_ENCRYPTION_KEY.

Procedura di ripristino

# 1. Stop n8n
docker compose stop n8n

# 2. Restore DB
docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U n8n -d n8n < backups/backup_2026-11-03.sql

# 3. Restore workflows/credentials
docker compose run --rm n8n import:workflow --input=/backups/workflows_2026-11-03.json
docker compose run --rm n8n import:credentials --input=/backups/credentials_2026-11-03.json

# 4. Start
docker compose start n8n

Controlla il servizio e ricevi gli avvisi

L’endpoint di controllo da aggiungere a Traefik/n8n

# n8n health
curl -f https://n8n.tuodominio.com/healthz || exit 1

# Esecuzioni in errore (via n8n API): filtra startedAt lato client, es. con jq
curl -H "X-N8N-API-KEY: $N8N_API_KEY" \
  "https://n8n.tuodominio.com/api/v1/executions?status=error&limit=100"

Invia a Slack un avviso in caso di errore

Schema indicativo: nomi dei nodi e parametri cambiano con la versione di n8n. Ricrea i nodi dall'editor e usa questo JSON come traccia.

{
  "name": "Error Alert - Failed Executions",
  "nodes": [
    {"name": "Cron Every 15min", "type": "n8n-nodes-base.cron", "typeVersion": 1, "position": [200, 200], "parameters": {"triggerTimes": {"item": [{"mode": "every15Minutes"}]}}},
    {"name": "Get Failed Executions", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4, "position": [400, 200], "parameters": {"url": "https://n8n.tuodominio.com/api/v1/executions", "method": "GET", "queryParameters": {"status": "error", "limit": "20"}, "authentication": "headerAuth", "options": {}}},
    {"name": "Filter New Errors", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [600, 200], "parameters": {"jsCode": "const sd = $getWorkflowStaticData('global');\nconst seen = new Set(sd.seen || []);\nconst since = Date.now() - 15 * 60 * 1000;\nconst newErrors = $input.first().json.data.filter(e => !seen.has(e.id) && new Date(e.startedAt).getTime() >= since);\nnewErrors.forEach(e => seen.add(e.id));\nsd.seen = [...seen].slice(-500);\nreturn newErrors.map(e => ({json: e}));"}},
    {"name": "Slack Alert", "type": "n8n-nodes-base.slack", "typeVersion": 1, "position": [800, 200], "parameters": {"channel": "#n8n-alerts", "text": "=🚨 *n8n Execution Failed*\n*Workflow:* {{ $json.workflowData.name }}\n*Error:* {{ $json.error.message }}\n*Time:* {{ $json.startedAt }}\n*Execution ID:* {{ $json.id }}\n<https://n8n.tuodominio.com/workflow/{{ $json.workflowData.id }}|Open in n8n>", "options": {}}}
  ],
  "connections": {
    "Cron Every 15min": {"main": [[{"node": "Get Failed Executions", "type": "main", "index": 0}]]},
    "Get Failed Executions": {"main": [[{"node": "Filter New Errors", "type": "main", "index": 0}]]},
    "Filter New Errors": {"main": [[{"node": "Slack Alert", "type": "main", "index": 0}]]}
  }
}

Nella credenziale Header Auth del nodo HTTP usa il nome X-N8N-API-KEY e come valore la chiave API di n8n.


Tabella decisionale: n8n, Make o Zapier

Fattoren8n (self-hosted)MakeZapier
Costo mensile (high volume)€5-10 (VPS)€50-200€100-500+
Data sovereignty✅ Totale (tuo VPS)⚠️ Cloud UE o USA a scelta❌ Cloud US
Operazioni/meseIllimitate10k-1M (a pagamento)2k-2M (a pagamento)
Nodi/Integrations400+ + custom code1500+5000+
Custom code (JS/Python)✅ Code node (JavaScript e Python)✅ Functions⚠️ JavaScript e Python (Code by Zapier), con limiti di tempo e memoria
Self-host option✅ Native❌ No❌ No
Learning curveMediaBassaBassa
Debug/Replay✅ Ottimo✅ Buono⚠️ Limitato
Team/Collaboration✅ (Enterprise)✅ Nativo✅ Nativo
AI/LLM nodes✅ Native (OpenAI, Anthropic, LangChain)✅✅

Prezzi e limiti di operazioni indicativi a novembre 2026: verifica i listini ufficiali di n8n, Make e Zapier.

La scelta proposta qui è n8n sul proprio server per PMI e freelance che cercano controllo dei dati, costi fissi bassi e possibilità di estensione. Make si rivolge ai team che preferiscono evitare la configurazione del server; Zapier alle aziende con budget maggiori e necessità di integrazioni specifiche.


Prossimo capitolo

Capitolo 7: costruisci un’applicazione web. Usa Cursor con FastAPI e Streamlit per un preventivatore, un download, un’area clienti o un portfolio. Vai al capitolo 7


Serie Vibe Coding per Professionisti — Capitolo 6 di 8. Il codice è in questo articolo; gli esempi JSON sono schemi da adattare alla tua versione di n8n. Nel prossimo costruiamo applicazioni web con Cursor, FastAPI e Streamlit.

Per approfondire

Fonti e riferimenti

Condividi LinkedIn Facebook X WhatsApp Telegram Email
Coding & debugging

Codice scritto con l’AI? Funziona oggi. Reggerà domani?

Audit del debito tecnico e della sicurezza di app e siti, debugging e code review.

Cybersecurity per PMI

Quanto è esposta la tua azienda?

Un hacker etico certificato trova le falle prima di chi attacca e ti dice come chiuderle, partendo dalla più pericolosa.

Marketing & Google Ads

Le tue campagne stanno bruciando budget?

Raccontami obiettivi e numeri attuali: ti dico dove intervenire per primo.

iLife · crescita personale

Riprogramma la tua vita con il metodo di un hacker etico

Percorsi one-to-one per capire dove sei, scegliere la direzione e arrivarci con metodo.

Digital Agency

Dove sta frenando il tuo business?

Una consulenza per capire cosa sistemare prima: sicurezza, tecnologia, marketing o persone.

Continua a leggere

Tutti gli articoli del blog

Rocco Balzamà
Parli direttamente con me

Il prossimo passo?
Parliamone.

Raccontami cosa vuoi proteggere, costruire o far crescere. Ti aiuto a capire da dove partire.

Di cosa mi occupo

Da oltre trent'anni faccio tre cose. Cerco le falle della tua azienda prima che le trovi chi vuole entrare. Imposto il marketing perché i risultati si possano misurare. E aiuto persone e aziende a capire chi sono, prima ancora di decidere cosa vendere.

Da remoto e in presenzaLavoro da remoto tramite call e videocall. Quando il progetto lo richiede, intervengo anche in presenza.
Seguimi sui social
Certified Ethical HackerSicurezza informatica dal 1993
Gli strumenti con cui lavoro
  • Google Ads
  • Meta
  • WordPress
  • WooCommerce
  • Shopify
  • TikTok
Strumenti gratuiti