- Home
- Academy
- Vibe Coding Professionisti
- n8n: automazioni con l'AI senza scrivere codice
n8n: automazioni con l'AI senza scrivere codice
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
| Concetto | Cos'è | Analogo |
|---|---|---|
| Node | Blocco funzionale (trigger, action, logic) | Funzione/lego |
| Workflow | Grafo orientato di nodi collegati | Programma/flowchart |
| Credentials | Auth salvate criptate (OAuth, API key, Basic) | Password manager |
| Expression | {{ $json.field }} per passare dati tra nodi | Variabili/piping |
| Execution | Singola run del workflow (manual, trigger, webhook) | Process run |
| Webhook | Endpoint HTTP pubblico per trigger esterni | API endpoint |
I nodi usati negli esempi
| Categoria | Nodi chiave | Uso tipico |
|---|---|---|
| Trigger | Webhook, Cron, n8n Form, Email Trigger | Inizio workflow |
| Logic | IF, Switch, Merge, SplitInBatches, Set, Code | Controllo flusso |
| Data | HTTP Request, GraphQL, Webhook Respond | API integration |
| App | Gmail, Outlook, Slack, HubSpot, Pipedrive, Asana, Notion, Airtable, Google Sheets | SaaS connectors |
| AI | OpenAI, Anthropic, LangChain, Custom LLM | AI steps |
| Storage | Read/Write Binary File, Redis, Postgres, MongoDB | Persistence |
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
- Importa in n8n: Workflows → Import → incolla JSON
- 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)
- Definisci le variabili nel blocco
environmentdel docker-compose (o usa le Variables$varssui piani che le includono); per i dati sensibili preferisci le Credentials. Da n8n 2.0N8N_BLOCK_ENV_ACCESS_IN_NODEvaletruedi default: per usare$envdevi impostarlo afalse, 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 - Attiva workflow → copia Webhook URL:
https://tuon8n.com/webhook/lead-capture - 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
| Fattore | n8n (self-hosted) | Make | Zapier |
|---|---|---|---|
| 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/mese | Illimitate | 10k-1M (a pagamento) | 2k-2M (a pagamento) |
| Nodi/Integrations | 400+ + custom code | 1500+ | 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 curve | Media | Bassa | Bassa |
| 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
- Agente digitale, capitolo 1: cos'è un agente AI
- Agenti AI: cosa sono e come usarli in azienda
- Casi d'uso dell'AI in azienda: 3 aree dove conviene iniziare
- Automazioni con l'AI






