Workflow Automation with n8n: Self-Hosting, Architecture, and First Workflows
A practical look at workflow engines using n8n: how it compares to Make, Zapier, Temporal, and Airflow, plus Docker self-hosting and a first workflow.
Automation tools are indispensable in modern IT architectures. Whether you need to synchronize data between SaaS systems, process webhooks, or orchestrate recurring batch jobs, without a dedicated automation layer the logic quickly ends up as spaghetti code in microservices or as fragile shell scripts in cron jobs. This article examines the class of workflow engines using n8n as an example, positions it against related tools, and shows how to run n8n yourself in just a few minutes.
What Is a Workflow Engine?
A workflow engine models automation logic as a directed graph of nodes. Each node encapsulates an action or transformation; connections (edges) define the data flow. The core principle is:
Trigger --> Verarbeitung --> Aktion(en)
n8n follows this model visually: you drag nodes onto a canvas, connect them, and configure them via forms. Under the hood, n8n is a Node.js application (TypeScript) that persists workflows in a SQLite or PostgreSQL database and triggers them via webhooks, cron, or manually.
Typical use cases:
- Processing incoming webhooks and forwarding them to internal APIs
- Synchronizing data between CRM, ERP, and communication platforms
- Enriching and escalating alerts from monitoring systems
- Transforming file uploads and writing them to object storage
Positioning: n8n, Make/Zapier, Temporal, Airflow
Before choosing a tool, an honest comparison is worthwhile. The following table shows the key differences:
+------------------+------------+------------+------------+------------+
| Kriterium | n8n | Make/Zapier| Temporal | Airflow |
+------------------+------------+------------+------------+------------+
| Hosting | Self/SaaS | SaaS only | Self/Cloud | Self/Cloud |
| Code-Freiheit | hoch | niedrig | vollstaend.| hoch |
| Visuelle Canvas | ja | ja | nein | DAG-View |
| Durability/Retry | einfach | einfach | nativ/tief | mittel |
| Zielgruppe | Dev/Ops | No-Code | Backend-Dev| Data-Eng. |
| Lizenz | fair-code | proprietaer| MIT | Apache 2.0 |
| DSGVO Self-Host | ja | nein | ja | ja |
+------------------+------------+------------+------------+------------+
Make and Zapier are well-suited for quick, no-code integrations between cloud services. As soon as you need custom transformation logic, self-hosting for data protection reasons, or more complex branching, you hit their limits.
Temporal and Apache Airflow are genuine code orchestrators. Temporal natively offers durable workflows with automatic retries, the saga pattern, and deterministic replays, making it suitable for distributed transactions that take minutes to hours (or longer). Airflow is specifically geared toward data-driven DAG pipelines (ETL, ML pipelines). Both require considerably more development effort and infrastructure.
n8n positions itself in between: powerful enough for production integration flows, developer-friendly thanks to code nodes, but without the guarantees of a durable-execution framework. For operational webhooks, notification pipelines, and medium complexity, n8n is the pragmatic choice.
Self-Hosting n8n with Docker
n8n provides an official Docker image at docker.n8n.io/n8nio/n8n. The following docker-compose.yml is production-oriented and covers persistence, time zone, and webhook URL:
# docker-compose.yml
services:
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
ports:
- "5678:5678"
environment:
- N8N_HOST=n8n.beispiel.de
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.beispiel.de/
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
# Verschluesselungs-Key fuer Credentials fest setzen (sonst pro Volume generiert)
- N8N_ENCRYPTION_KEY=bitte-ersetzen-langer-zufallswert
# Externe PostgreSQL-Datenbank statt SQLite
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=db-passwort
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=db-passwort
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
n8n_data:
pg_data:
Start the stack with:
docker compose up -d
# Logs beobachten
docker compose logs -f n8n
n8n is then reachable at http://localhost:5678.
Note on authentication: The basic auth previously available via
N8N_BASIC_AUTH_ACTIVE/N8N_BASIC_AUTH_USER/N8N_BASIC_AUTH_PASSWORDwas removed with n8n 1.0 and has no effect in current versions. Instead, the built-in user management takes over: on first launch you create an owner account (email + password); you then invite additional users afterward. A real protection layer for the exposed instance belongs in the upstream reverse proxy.
In production, you place a reverse proxy (nginx, Caddy, Traefik) in front of it that terminates TLS and optionally implements IP allowlisting. The WEBHOOK_URL variable must point to the externally reachable HTTPS URL so that webhook nodes generate correct callback URLs.
Example Workflow: From Webhook to HTTP Request
The following flow illustrates a typical integration case: an incoming webhook delivers raw data, a code node transforms it, an HTTP request node sends the result to an external API, and a respond node replies to the caller.
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| Webhook Trigger |--->| Code / Transform|--->| HTTP Request |--->| Respond to |
| POST /hook/xyz | | (JS, $input) | | POST /api/ingest| | Webhook |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
Code Node: Transforming Items
n8n passes data along as an array of items. In the code node, you access them via $input.all() and return a new array:
// Code-Node (JavaScript) - Modus "Run Once for All Items"
const items = $input.all();
return items
.filter(item => item.json.status === "active")
.map(item => {
const { id, name, email, createdAt } = item.json;
return {
json: {
externalId: `EXT-${id}`,
displayName: name.trim(),
contactEmail: email.toLowerCase(),
// ISO-8601-Timestamp sicherstellen
registeredAt: new Date(createdAt).toISOString(),
source: "n8n-webhook",
},
};
});
Important conventions:
- Every returned object must have a
jsonproperty. - Binary data goes into an optional
binaryproperty. $input,$workflow, and$executionare global helper objects of the n8n runtime; you idiomatically access data from other nodes via$('Node-Name').
Workflow Skeleton as JSON
n8n workflows can be exported as JSON and version-controlled. A minimal skeleton looks like this:
{
"name": "Webhook-Transform-Request",
"nodes": [
{
"id": "1",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [240, 300],
"parameters": {
"httpMethod": "POST",
"path": "ingest",
"responseMode": "responseNode"
}
},
{
"id": "2",
"name": "Transform",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [460, 300],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "return $input.all().map(i => ({ json: { ...i.json, processed: true } }));"
}
},
{
"id": "3",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [680, 300],
"parameters": {
"method": "POST",
"url": "https://api.beispiel.de/ingest",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json) }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "Bearer {{ $env.API_TOKEN }}" }
]
}
}
},
{
"id": "4",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [900, 300],
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"ok\": true } }}"
}
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Transform", "type": "main", "index": 0 }]] },
"Transform": { "main": [[{ "node": "HTTP Request", "type": "main", "index": 0 }]] },
"HTTP Request": { "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]] }
}
}
Since the webhook is configured with responseMode: "responseNode", the Respond to Webhook node (n8n-nodes-base.respondToWebhook) finalizes the HTTP response to the caller. Secrets such as API_TOKEN should be stored in n8n as credentials or as an environment variable, never hardcoded in the workflow JSON.
License and GDPR
n8n is released under the Sustainable Use License (fair-code). This means you may use, modify, and distribute the software free of charge, but only for your own internal business purposes or for non-commercial or personal use. In particular, offering n8n as a hosted service to third parties or selling a product whose value rests substantially on n8n is restricted; a separate commercial license is required for that. Simply offering consulting or implementation services around n8n, on the other hand, is permitted. You can find the details in the Sustainable Use License as well as in the n8n repository.
For European companies, the GDPR aspect of self-hosting is relevant: all processed payload data remains within your own infrastructure. With SaaS providers such as Make or Zapier, payload data is processed on their servers, often outside the EU, which necessitates data processing agreements and, where applicable, third-country transfer assessments.
Practical Recommendation
Start with SQLite for local development and prototypes; for production, PostgreSQL with a dedicated volume and regular backups (pg_dump) is recommended. Also set a fixed N8N_ENCRYPTION_KEY so that stored credentials can still be decrypted after a rebuild. Version your workflow JSONs in Git, limit verbose execution logs to debug phases (data minimization), and protect the instance with a reverse proxy using TLS. For workflows with strict reliability requirements, such as distributed transactions spanning multiple external systems, evaluate whether Temporal would be the better foundation.
Do you have questions about integrating this into your specific system landscape, about scaling across multiple workers, or about securing your n8n instance? Write to info@yurtbay.dev.