El problema
Dejé n8n corriendo en mi Windows con Docker, con los workflows y las credenciales a salvo en un volumen. Un primer webhook responde a un POST lanzado desde PowerShell.
Quería una base local para automatizar sin pagar un servicio ni montar un servidor. La primera prueba la hice sin volumen y perdí la cuenta al borrar el contenedor.
Este post es la base de la serie. El caso real que se apoya en ella está en Auto-publicar el blog en LinkedIn con GitHub Actions y n8n.
El contenedor se puede tirar cuando quieras. Lo que importa vive en el volumen.
Qué necesito
- Docker Desktop arrancado en Windows.
- PowerShell.
- El puerto
5678libre, que es el que usa n8n por defecto.
n8n guarda su estado en /home/node/.n8n dentro del contenedor: la base de datos SQLite, los workflows, las credenciales y la clave con que las cifra. Ese directorio es el que monto en un volumen.
Arrancar con docker run
Para comprobar que n8n arranca en mi equipo, un solo comando basta:
# PowerShell
docker run -d `
--name n8n-local `
-p 5678:5678 `
-v n8n_data:/home/node/.n8n `
-e TZ=Europe/Madrid `
docker.n8n.io/n8nio/n8n:latest
-v n8n_data:... crea un volumen con nombre gestionado por Docker. TZ fija la zona horaria, que importa cuando un workflow se programa por hora.
# PowerShell: estado y logs (Ctrl + C sale sin parar el contenedor)
docker ps
docker logs -f n8n-local
Con el contenedor en marcha, abro http://localhost:5678 y creo la cuenta de owner. Esa cuenta ya queda en el volumen.
Pasar a Compose sin perder nada
docker run sirve para probar. Para el día a día prefiero Compose: la configuración queda en un fichero, y arrancar o recrear es un comando corto.
Creo una carpeta fuera de cualquier repo, por ejemplo C:\n8n-local, y dentro este fichero:
# docker-compose.yml
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n-local
restart: unless-stopped
ports:
- "5678:5678"
environment:
TZ: Europe/Madrid
N8N_HOST: localhost
N8N_PORT: 5678
N8N_PROTOCOL: http
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:
external: true
name: n8n_data
El bloque external: true es lo que me dejó migrar sin perder nada. Le dice a Compose que use el volumen que ya creó docker run, en vez de crear otro.
Sin external: true, Compose crea un volumen con el nombre del proyecto delante, como n8n-local_n8n_data. n8n arranca vacío y parece que se ha borrado todo.
Antes de levantarlo, quito el contenedor de la prueba. El volumen no se toca:
# PowerShell, en C:\n8n-local
docker rm -f n8n-local
docker compose up -d
docker compose ps
docker compose logs -f n8n
Si empiezo directamente con Compose, sin pasar por docker run, quito external: true y name. Compose crea el volumen la primera vez.
Para comprobar la persistencia, guardo un workflow vacío, ejecuto docker compose down y docker compose up -d. Si el workflow sigue ahí, el volumen funciona.
Un primer workflow con webhook
Para entender cómo pasan los datos entre nodos, monto el flujo más corto que recibe algo y contesta:
Webhook → Edit Fields → Respond to Webhook
Webhook: métodoPOST, pathdemo-n8n-local. EnRespond, elijoUsing 'Respond to Webhook' Node.Edit Fields(opcional): añadoreceivedysourcepara ver cómo se transforma la entrada.Respond to Webhook: respondo con JSON.
Si Respond se queda en Immediately, el webhook contesta antes de llegar al último nodo. n8n lo avisa con un warning en Respond to Webhook.
El cuerpo de la respuesta tiene que ser JSON válido, con claves entre comillas. Este es el que uso:
{
"ok": true,
"message": "Webhook recibido en local"
}
Probarlo desde PowerShell
Guardo el workflow y pulso Listen for test event en el nodo Webhook. Mientras escucha, lanzo el POST:
# PowerShell: POST a la URL de prueba
$body = @{
event = "test_local"
source = "powershell"
message = "Hola desde Windows"
} | ConvertTo-Json
$response = Invoke-WebRequest `
-UseBasicParsing `
-Method Post `
-Uri "http://localhost:5678/webhook-test/demo-n8n-local" `
-ContentType "application/json" `
-Body $body
$response.StatusCode
$response.Content
# salida esperada
200
{"ok":true,"message":"Webhook recibido en local"}
Uso Invoke-WebRequest y no Invoke-RestMethod porque quiero ver el código HTTP, no solo el cuerpo. -UseBasicParsing evita el aviso de Windows PowerShell 5.1.
En la UI aparece la ejecución, con el payload de entrada y la salida de cada nodo. Ahí es donde depuro.
webhook-test o webhook
Cada nodo Webhook tiene dos URL, y confundirlas fue mi primer «no funciona»:
| URL | Cuándo responde |
|---|---|
/webhook-test/demo-n8n-local | Solo mientras el editor está en Listen for test event |
/webhook/demo-n8n-local | Solo con el workflow publicado (activo) |
En las versiones recientes, activar es el botón Publish; en las anteriores, el interruptor Active. Con el workflow publicado, repito el POST cambiando webhook-test por webhook y debe volver el mismo 200.
La URL de producción es la que importa para automatizar. Ningún sistema externo va a pulsar un botón en mi editor.
Si algo falla
localhost:5678no abre: Docker Desktop parado, contenedor caído o puerto ocupado. Miradocker ps.- Workflows perdidos tras reiniciar: falta el volumen, o Compose creó uno nuevo.
404en el webhook: URL de test sin escucha, URL de producción sin publicar o path distinto.- No responde lo esperado:
Respondsigue enImmediately, o el método no esPOST. - Cambié el YAML y no se nota:
docker compose up -d --force-recreate.
Lo que aprendí
El volumen, antes que nada: la cuenta y las credenciales de n8n viven ahí, no en el contenedor. Y la mitad de mis «el webhook no funciona» eran la URL de test sin el editor escuchando.
Si tienes dudas o lo estás montando tú, cuéntamelo.