diff --git a/n8n/.env.example b/n8n/.env.example new file mode 100644 index 0000000..75e2994 --- /dev/null +++ b/n8n/.env.example @@ -0,0 +1,27 @@ +# Base path for persistent n8n data (mounted at /data) +N8N_DATA=/path/to/n8n/data + +# UID:GID the container runs as. Must own ${N8N_DATA}/data and be able +# to write the backups folder (match it to the share owner where needed). +N8N_USER=1000:1000 + +# Host port mapped to the container's 5678 +N8N_PORT=5678 + +# Timezone (e.g., Europe/Madrid) +TZ=Etc/UTC + +# Public hostname and protocol used by n8n for URLs and cookies +N8N_HOST=n8n.example.com +N8N_PROTOCOL=https +N8N_SECURE_COOKIE=true + +# Encryption key for stored credentials (generate a strong random value) +N8N_ENCRYPTION_KEY=replace_me_with_a_long_random_string + +# Host path bind-mounted at /backups. +# - Host with network storage: point to an OS-level NFS automount (fstab +# with x-systemd.automount,nofail) so n8n boots without waiting for it, +# e.g. /mnt/backups-n8n +# - Host without it: any local directory, e.g. /opt/n8n/backups +N8N_BACKUPS_PATH=/opt/n8n/backups diff --git a/n8n/README.md b/n8n/README.md new file mode 100644 index 0000000..02e433b --- /dev/null +++ b/n8n/README.md @@ -0,0 +1,73 @@ +# n8n + +Deploys [n8n](https://n8n.io/), a workflow automation tool. +Uses SQLite for storage and bind-mounts a host folder at +`/backups`. + +The same `docker-compose.yml` is meant to run as two +independent Dokploy applications on different hosts, +differing only in their environment variables: + +- **Host with network storage access:** `N8N_BACKUPS_PATH` + points to an OS-level NFS automount. +- **Host without it** (e.g. a remote VPS): `N8N_BACKUPS_PATH` + points to a plain local directory. + +## Environment Variables + +| Variable | Description | +|----------------------|---------------------------------------------------------| +| `N8N_DATA` | Local path for persistent n8n data (mounted at `/data`) | +| `N8N_USER` | `UID:GID` the container runs as (see Notes for perms) | +| `N8N_PORT` | Host port mapped to the container's `5678` | +| `TZ` | Timezone (also used as `GENERIC_TIMEZONE`) | +| `N8N_HOST` | Public hostname n8n serves on | +| `N8N_PROTOCOL` | `http` or `https` (used for URLs and cookies) | +| `N8N_SECURE_COOKIE` | `true` when serving over HTTPS, `false` otherwise | +| `N8N_ENCRYPTION_KEY` | Secret used to encrypt stored credentials | +| `N8N_BACKUPS_PATH` | Host path bind-mounted at `/backups` (see Notes) | + +## Networks + +No network is declared in the compose file — Dokploy +attaches its own network automatically on deploy. + +## Volumes + +- `${N8N_DATA}/data` → `/data` (bind mount, holds n8n's + SQLite DB and configuration; `HOME` is set to `/data`). +- `${N8N_BACKUPS_PATH}` → `/backups` (bind mount with + `rslave` propagation, so an on-demand NFS automount that + appears on the host *after* the container starts is still + visible inside it). + +## Notes + +- `${N8N_DATA}/data` on the host must be owned by the same + `UID:GID` set in `N8N_USER`, otherwise n8n will fail to + write its database. The backups folder must also be + writable by that UID (match `N8N_USER` to the owner of + the network share, or make the share group-writable). +- **Network-storage resilience:** mount the NFS share via + the OS, not via a Docker volume, using an fstab line with + `x-systemd.automount,nofail`, e.g.: + + ```text + :/ /mnt/backups-n8n nfs \ + defaults,nofail,x-systemd.automount,_netdev,vers=4.1 0 0 + ``` + + This way n8n boots immediately after a power outage even + if the storage server is slower to come up; autofs mounts + the share on first access and `rslave` propagation makes + it visible inside the running container. While the share + is down, backup operations fail cleanly instead of writing + to the host's local disk and being silently lost. +- Set `N8N_PROTOCOL=https` and `N8N_SECURE_COOKIE=true` + only when a reverse proxy terminates TLS in front of + n8n; for plain HTTP use `http` / `false`. +- Generate `N8N_ENCRYPTION_KEY` once and keep it stable — + rotating it makes existing stored credentials unreadable. +- `NODE_FUNCTION_ALLOW_BUILTIN=fs` enables the `fs` module + inside Function nodes (needed if workflows read/write + files under `/data` or `/backups`). diff --git a/n8n/docker-compose.yml b/n8n/docker-compose.yml new file mode 100644 index 0000000..9463c9e --- /dev/null +++ b/n8n/docker-compose.yml @@ -0,0 +1,36 @@ +services: + n8n: + image: docker.n8n.io/n8nio/n8n:latest + container_name: n8n + restart: unless-stopped + user: ${N8N_USER} + security_opt: + - no-new-privileges:true + ports: + - "${N8N_PORT}:5678" + volumes: + - ${N8N_DATA}/data:/data + - type: bind + source: ${N8N_BACKUPS_PATH} + target: /backups + bind: + propagation: rslave + environment: + HOME: /data + TZ: ${TZ} + GENERIC_TIMEZONE: ${TZ} + N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} + N8N_HOST: ${N8N_HOST} + N8N_PORT: 5678 + N8N_PROTOCOL: ${N8N_PROTOCOL} + N8N_SECURE_COOKIE: ${N8N_SECURE_COOKIE} + DB_SQLITE_POOL_SIZE: 5 + N8N_BLOCK_ENV_ACCESS_IN_NODE: "true" + NODE_FUNCTION_ALLOW_BUILTIN: fs + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + labels: + - com.centurylinklabs.watchtower.enable=true