diff --git a/modules/n8n/Dockerfile b/modules/n8n/Dockerfile new file mode 100644 index 0000000..7d15a2b --- /dev/null +++ b/modules/n8n/Dockerfile @@ -0,0 +1,20 @@ +# n8n with what its workflows reach for beyond the upstream image. +# +# The base is named, not pinned here (novox/hq issue 044): module.json's `build.on` declares N8N_BASE +# as the upstream image by digest, and the mesh hands the build its own copy (ADR 0097). +ARG N8N_BASE +FROM ${N8N_BASE} + +USER root +# - `media` (GID 2000), with `node` in it: the shared media library is group-writable by the +# operator's media group, and a workflow files downloads into it. A container resource cannot add +# a supplementary group, so the image's own /etc/group carries it. 2000 is the operator's media +# group today; novox/hq 153 proposes reading it from the accessed data (${access::gid}). +# - uuid, pinned to the version the workflows were written against: Code nodes require() it +# (NODE_FUNCTION_ALLOW_EXTERNAL=*), and a Code node can only require what is installed. +RUN apk add --no-cache shadow \ + && groupadd -g 2000 media \ + && usermod -aG media node \ + && npm install -g uuid@14.0.1 + +USER node diff --git a/modules/n8n/module.json b/modules/n8n/module.json index a1810e8..dfecb44 100644 --- a/modules/n8n/module.json +++ b/modules/n8n/module.json @@ -18,44 +18,60 @@ } }, "binds": { - "postgres-database": "/var/lib/n8n/database.json", - "route": "/var/lib/n8n/route.json" + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" }, "secrets": { - "postgres-database": "/var/lib/n8n/database.secret" - }, - "own-secrets": { - "basic-auth": "/var/lib/n8n/basic-auth.secret" + "postgres-database": "${dir:state}/database.secret" }, + "accesses": [ + { + "path": "/services/media", + "mode": "read-write" + } + ], "listens": [ { "name": "web", - "port": 5682, + "port": 5678, "protocol": "tcp", "from": "mesh", - "why": "the n8n editor and webhook endpoints over http; the public name n8n.novox.be is a route grant, and route-proxy reaches it on this published port" + "why": "the n8n editor, its REST API and the webhook endpoints workflows are triggered through; a public name is the route's" } ], "resources": [ { "id": "state", "type": "directory", - "path": "/var/lib/n8n", - "mode": "0700" + "mode": "0700", + "place": "." }, { "id": "data", "type": "directory", - "path": "/services/n8n/n8n-data", "mode": "0700", "owner": "1000:1000" }, + { + "id": "cache", + "type": "directory", + "mode": "0700", + "owner": "999:999" + }, + { + "id": "database-secret", + "type": "file", + "path": "${dir:state}/n8n-database.secret", + "mode": "0400", + "owner": "1000:1000", + "content": "${secret:postgres-database}" + }, { "id": "server-env", "type": "file", - "path": "/var/lib/n8n/server.env", + "path": "${dir:state}/server.env", "mode": "0600", - "content": "N8N_HOST=n8n.novox.be\nN8N_PORT=5678\nN8N_PROTOCOL=https\nWEBHOOK_URL=https://n8n.novox.be/\nN8N_BASIC_AUTH_ACTIVE=true\nN8N_BASIC_AUTH_USER=admin\nN8N_BASIC_AUTH_PASSWORD=${secret:basic-auth}\nNODE_FUNCTION_ALLOW_BUILTIN=*\nNODE_FUNCTION_ALLOW_EXTERNAL=*\nDB_TYPE=postgresdb\nDB_POSTGRESDB_HOST=${bound:postgres-database:at}\nDB_POSTGRESDB_PORT=${bound:postgres-database:port}\nDB_POSTGRESDB_DATABASE=${bound:postgres-database:as}\nDB_POSTGRESDB_USER=${bound:postgres-database:as}\nDB_POSTGRESDB_PASSWORD=${secret:postgres-database}\n" + "content": "N8N_HOST=${bound:route:name}\nN8N_PORT=5678\nN8N_PROTOCOL=https\nWEBHOOK_URL=https://${bound:route:name}/\nNODE_FUNCTION_ALLOW_BUILTIN=*\nNODE_FUNCTION_ALLOW_EXTERNAL=*\nDB_TYPE=postgresdb\nDB_POSTGRESDB_HOST=${bound:postgres-database:at}\nDB_POSTGRESDB_PORT=${bound:postgres-database:port}\nDB_POSTGRESDB_DATABASE=${bound:postgres-database:as}\nDB_POSTGRESDB_USER=${bound:postgres-database:as}\nDB_POSTGRESDB_PASSWORD_FILE=/run/secrets/database\n" }, { "id": "net", @@ -66,18 +82,61 @@ "id": "server", "type": "container", "name": "n8n", - "image": "n8nio/n8n@sha256:4846eb2f4b874ab04cde7fc1e249d2ddaec66e9aea64439beb2972cfea88e3c0", + "artifact": "server", "network": "n8n", "env-file": [ - "/var/lib/n8n/server.env" + "${dir:state}/server.env" ], "ports": [ "5678" ], "volumes": [ - "/services/n8n/n8n-data:/home/node/.n8n" + "${dir:data}:/home/node/.n8n", + "${dir:state}/n8n-database.secret:/run/secrets/database:ro", + "/services/media:/media-library" + ] + }, + { + "id": "cache-server", + "type": "container", + "name": "n8n-redis", + "image": "redis@sha256:8a1efc5f479551822b47424ccae982026b633f28818eab0387348120a61e10e2", + "network": "n8n", + "args": [ + "redis-server", + "--appendonly", + "yes" ], - "secrets-in-environment": "n8n's loader honours _FILE for every setting; convertible, awaiting a bed that proves it (N8N_BASIC_AUTH_* was removed in n8n 1.0 and is likely dead)" + "volumes": [ + "${dir:cache}:/data" + ] + }, + { + "id": "browser", + "type": "container", + "name": "n8n-selenium", + "image": "selenium/standalone-chrome@sha256:9ae1c78e9b2ca9fe4b22e57873b5ee34aeb8e814e3122293eef4c3abe4c5f448", + "network": "n8n", + "env": { + "SE_ENABLE_TRACING": "false", + "SE_NODE_MAX_SESSIONS": "5", + "SE_NODE_OVERRIDE_MAX_SESSIONS": "true" + } } - ] + ], + "build": { + "on": [ + { + "arg": "N8N_BASE", + "image": "n8nio/n8n@sha256:4846eb2f4b874ab04cde7fc1e249d2ddaec66e9aea64439beb2972cfea88e3c0" + } + ], + "artifacts": [ + { + "name": "server", + "kind": "image", + "from": "Dockerfile" + } + ] + } }