Look, I've been working with n8n deployments for over a year now, and when I saw the v2.0 announcement, my first thought wasn't "what cool features," it was "how many production workflows am I going to have to fix?"
And you know what? This release is genuinely good, but it's going to break your stuff if you don't prepare.
The Timeline (Mark Your Calendar)
- →Beta: December 8, 2025
- →Stable: December 15, 2025
- →Your deadline to test: right now
Version 1.x will get 3 months of security patches after v2 releases, and then that's it.
The Good Stuff
Autosave is coming. Yes, finally. After how many years of people losing work? Better late than never.
Also: updated canvas interface, new sidebar, and "surprises."
But let's be real, you're not here for UI updates.
The Stuff That Will Actually Break Your Workflows
Security: They're Locking Everything Down
1. Code Node Can No Longer Access Environment Variables
bash
N8N_BLOCK_ENV_ACCESS_IN_NODE=true # This is now the default
If you've been lazy and were accessing process.env.SOME_SECRET in your Code nodes... yeah, that's over.
The fix?:
- →Set it back to
false (which defeats the purpose)
I've seen too many people storing API keys in environment variables and accessing them in Code nodes. This forces better practices.
2. Task Runners Are Mandatory
bash
N8N_RUNNERS_ENABLED=true
All Code node executions now run in task runners for isolation. This is good for security, but your infrastructure needs to handle it.
Now they have to be in external mode which is the most secure.
3. Python Code Node: Complete Rewrite
They removed Pyodide (the browser-based Python). Now it's native Python only, task runners required, external mode mandatory.
What breaks:
- →
_input variable (removed)
- →Dot notation access (removed)
- →Any Python Code node without proper task runner setup (broken)
This is better long-term, but the migration pain is real. Review every Python Code node you have.
4. ExecuteCommand and LocalFileTrigger: Disabled by Default
Finally. These nodes are security nightmares. They allow users to execute arbitrary system commands and access the file system.
They're disabled now. If you absolutely need them (and you probably don't), you have to explicitly enable them:
bash
NODES_EXCLUDE="[]" # Or just remove these specific nodes
5. OAuth Callbacks Need Authentication
bash
N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=false # New default
Before upgrading, test every OAuth integration. Slack, Google, whatever you've got connected—test it all.
6. File Operations Are Sandboxed
bash
N8N_RESTRICT_FILE_ACCESS_TO=~/.n8n-files # New default
ReadWriteFile and ReadBinaryFiles nodes can only touch files in this directory now. Good for security, annoying if you've been reading files from random locations.
7. Config File Permissions: SSH-Style Security
Your config files need 0600 permissions (owner read/write only).
bash
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
Windows users: This doesn't work on Windows. Set it to false.
Linux/Mac users: Run chmod 600 on your config files or n8n won't start.
Database Changes (The Painful Ones)
MySQL/MariaDB: Removed
Gone. PostgreSQL or SQLite only. This was deprecated in v1.0—if you're still on MySQL, you've had a year to migrate.
Use the database migration tool or you're screwed.
SQLite: Legacy Driver Removed
Only the pooling driver remains. It's faster (10x in benchmarks), uses WAL mode, and is the new default.
bash
DB_SQLITE_POOL_SIZE=2 # Auto-configured
Most people won't notice this change. If you do, it's probably because something was broken before.
Binary Data: No More In-Memory Mode
The default mode that kept binary data in memory during execution? Gone.
Your options:
- →
filesystem (default for single instance)
- →
database (default for queue mode)
- →
s3 (if you're fancy)
Make sure you have disk space. If you're processing large files and suddenly run out of space, this is why.
Behavior Changes (The Subtle Ones)
Subworkflow + Wait Node = Fixed
Before: Parent workflow received input from Wait nodes in child workflows (which made no sense)
Now: Parent workflow receives output from the end of the child workflow (which is correct)
If you have workflows calling subworkflows with Wait nodes, review them. The behavior change is correct, but your logic might depend on the previous broken behavior.
Configuration Hell
dotenv Update
Your .env file parsing is different now.
Changes:
- →Backticks need quotes now
- →
# always starts a comment (no more values containing #)
- →Multi-line values work now
Review your .env files. Especially if you have weird characters in passwords or tokens.
Removed stuff:
- →
QUEUE_WORKER_MAX_STALLED_COUNT
- →CLI option
n8n --tunnel (use ngrok or Cloudflare Tunnel)
- →
update:workflow --all --active=true (good riddance, this was dangerous)
Removed nodes:
- →Spontit
- →crowd.dev
- →Kitemaker
Docker Setup: Old vs New (The Part You Actually Need)
Alright, enough theory. Here's how your docker-compose actually needs to look.
The Old Way (v1.x) - Simple but Insecure
yaml
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: always
ports:
- "5678:5678"
environment:
- N8N_HOST=your-domain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://your-domain.com/
- GENERIC_TIMEZONE=Europe/Berlin
# Database
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=n8n_password
# These were the defaults (and security issues)
- N8N_BLOCK_ENV_ACCESS_IN_NODE=false # Code nodes could access env vars
- N8N_RUNNERS_ENABLED=false # No task runners
- N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=true # No auth on OAuth callbacks
volumes:
- n8n_data:/home/node/.n8n
- ./n8n-files:/files # Files wherever you wanted
depends_on:
- postgres
postgres:
image: postgres:15
container_name: n8n_postgres
restart: always
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=n8n_password
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
postgres_data:
This worked fine but had security holes you could drive a truck through.
The New Way (v2.0) - Option 1: Internal Mode (Simpler)
For most people, this is enough. Task runners run as child processes inside the same container.
yaml
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: always
ports:
- "5678:5678"
environment:
- N8N_HOST=your-domain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://your-domain.com/
- GENERIC_TIMEZONE=Europe/Berlin
# Database
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=n8n_password
# New v2.0 - Internal mode (simple)
- N8N_RUNNERS_ENABLED=true # Just this!
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
- N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=false
# File Access Restrictions
- N8N_RESTRICT_FILE_ACCESS_TO=/home/node/.n8n-files
# File Permissions (Linux/Mac only)
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
# - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false # For Windows
# Binary Data Storage
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
# Git Node Security
- N8N_GIT_NODE_DISABLE_BARE_REPOS=true
# If you want to allow ExecuteCommand and LocalFileTrigger (disabled by default)
- NODES_EXCLUDE=[]
volumes:
- n8n_data:/home/node/.n8n
- n8n_files:/home/node/.n8n-files
depends_on:
- postgres
postgres:
image: postgres:15
container_name: n8n_postgres
restart: always
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=n8n_password
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
n8n_files:
postgres_data:
When to use internal mode:
- →Simpler setup
- →Don't need Python Code nodes
- →Don't have extreme security requirements
- →Most use cases
The New Way (v2.0) - Option 2: External Mode (Maximum Security)
You only need this if:
- →You use Python Code nodes (external mode is mandatory for Python)
- →You want maximum security isolation
- →You're running in queue mode with workers
yaml
services:
n8n:
image: n8nio/n8n:latest
container_name: n8n
restart: always
ports:
- "5678:5678"
environment:
- N8N_HOST=your-domain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://your-domain.com/
- GENERIC_TIMEZONE=Europe/Berlin
# Database
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=n8n_password
# New v2.0 - External Mode
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=external
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0 # Allow external connections
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN}
- N8N_NATIVE_PYTHON_RUNNER=true # For Python Code nodes
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
- N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=false
# File Access Restrictions
- N8N_RESTRICT_FILE_ACCESS_TO=/home/node/.n8n-files
# File Permissions (Linux/Mac)
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
# - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false # Windows
# Binary Data Storage
- N8N_DEFAULT_BINARY_DATA_MODE=filesystem
# Git Node Security
- N8N_GIT_NODE_DISABLE_BARE_REPOS=true
# If you want to allow ExecuteCommand and LocalFileTrigger (disabled by default)
- NODES_EXCLUDE="[]"
volumes:
- n8n_data:/home/node/.n8n
- n8n_files:/home/node/.n8n-files
depends_on:
- postgres
# External Task Runner (separate container)
n8n-runner:
image: n8nio/runners:latest
container_name: n8n_runner
restart: always
environment:
# Connect to broker in n8n via HTTP/WebSocket on port 5679
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
- N8N_RUNNERS_AUTH_TOKEN=${N8N_RUNNERS_AUTH_TOKEN} # Same token as n8n
depends_on:
- n8n
postgres:
image: postgres:15
container_name: n8n_postgres
restart: always
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=n8n_password
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
n8n_files:
postgres_data:
Generate your auth token:
Add it to your .env file:
bash
N8N_RUNNERS_AUTH_TOKEN=your-generated-token-here
What Changed? Visual Comparison
Before (v1.x):
- →n8n container
- →postgres container
- →No runners, no isolation
New (v2.0) - Internal:
- →n8n container (with internal runners)
- →postgres container
- →No extra containers
New (v2.0) - External:
- →n8n container (acts as task broker on port 5679)
- →n8n-runner container (connects to n8n via WebSocket)
- →postgres container
- →Auth token for secure communication
- →No Redis (unless you're in queue mode)
Migration Checklist
For internal mode (most people):
For external mode (Python users or high security):
Common Issues
"Task runner won't connect"
Check:
- →Auth token identical in n8n and runner
- →Broker URI correct:
http://n8n:5679 (or your container name)
- →
N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0 in n8n (to accept external connections)
"OAuth stopped working"
You didn't test with N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=false before upgrading.
"File operations failing"
Files aren't in /home/node/.n8n-files. Move them or adjust the path.
"n8n won't start - permission error"
Config files need chmod 600. Windows users: N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=false.
"Unauthorized error from runner"
Tokens don't match. Check your .env.
How Not to Screw This Up
Step 1: Check the migration report
Settings → Migration Report (you need admin access)
Available since v1.121.0. Run it now.
Step 2: Decide your runner mode
Using Python Code nodes? → External mode mandatory
Need maximum security? → External mode
Everything else? → Internal mode is enough
Step 3: If going external, generate token
bash
openssl rand -hex 32 >> .env
Edit .env:
bash
N8N_RUNNERS_AUTH_TOKEN=your-token-here
Set permissions:
Step 4: Test in staging
bash
N8N_RUNNERS_ENABLED=true
N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=false
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
Step 5: Review every Code node
Especially:
- →Anything using
process.env
- →All Python Code nodes
- →File operations
Step 6: Test OAuth
All integrations. Manually. No exceptions.
Step 7: Back up everything
Database, workflows, credentials, configs, complete .n8n directory.
Step 8: Help find bugs
Report bugs either in an issue on the repository or in the n8n community forum.
My Take
This is the most significant n8n release since 1.0. The security improvements are necessary.
Most people will be fine with internal mode—it's simple, you just set N8N_RUNNERS_ENABLED=true and you're done. You don't need extra containers, you don't need Redis, you don't need complicated auth tokens.
External mode is only necessary if:
- →You use Python Code nodes (mandatory)
- →You have extreme security requirements
- →You're running queue mode with multiple workers
If you're running n8n in production (like I do every day), take this seriously. Budget real time for testing and migration. Don't just do docker pull and hope for the best.
The good news? After migrating, your instance will be more secure, more stable, and better positioned for future updates.
Resources