A step-by-step tutorial for deploying N8N with Docker, Nginx, Let’s Encrypt SSL, and RDS PostgreSQL
Introduction: Why Self-Host N8N?
N8N is a powerful workflow automation tool – think Zapier, but open-source and self-hostable. While n8n.cloud offers a managed solution, self-hosting on AWS gives you:
- Full control over your data and workflows
- No execution limits (n8n.cloud charges per execution)
- Custom integrations without restrictions
- Cost predictability (fixed EC2/RDS costs vs. usage-based pricing)
But here’s the catch: installing N8N properly requires more than just npm install n8n. You need:
- A reverse proxy (Nginx) for SSL termination
- PostgreSQL for data persistence (SQLite doesn’t cut it in production)
- Redis for distributed queue management
- SSL certificates (Let’s Encrypt)
- Proper container orchestration (Docker Compose)
This guide walks you through every single step – from launching an EC2 instance to accessing your fully secured N8N installation at https://yourdomain.com.
What we’ll build:
Internet (HTTPS:443)
β
Nginx (SSL termination + WebSocket support)
β
N8N Main Container (UI + API on localhost:5678)
β
N8N Worker Container (Background job execution)
β
Redis (Queue management)
β
RDS PostgreSQL (Data persistence)
Time required: 1-2 hours (depending on your AWS familiarity)
Architecture Overview
The Stack
Here’s what we’re installing and why:
1. Docker + Docker Compose
- Why: Containerization isolates N8N from the host system
- Benefit: Zero pollution of your EC2 instance, easy rollbacks, horizontal scaling
- Alternative: Direct npm install (messy, hard to maintain)
2. Nginx (Reverse Proxy)
- Why: Acts as the gateway between the internet and N8N
- Benefit: SSL termination, WebSocket support, request buffering control
- Alternative: Exposing N8N directly on port 443 (requires root, no SSL layer)
3. Let’s Encrypt (via Certbot)
- Why: Free, automated SSL certificates
- Benefit: HTTPS out of the box, auto-renewal via cron
- Alternative: Paid SSL certificates (unnecessary cost)
4. RDS PostgreSQL
- Why: Production-grade database outside of EC2
- Benefit: Automated backups, point-in-time recovery, independent scaling
- Alternative: SQLite (not suitable for production), local PostgreSQL (backup complexity)
5. Redis
- Why: Message queue for distributed execution
- Benefit: N8N Main offloads heavy workflows to Worker containers
- Alternative: In-memory queue (doesn’t survive restarts)
Architecture Diagram
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Internet β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β HTTPS (443)
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AWS EC2 Instance β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Nginx (Reverse Proxy) β β
β β - SSL Termination (Let's Encrypt) β β
β β - WebSocket Support β β
β β - Port 80 β 443 Redirect β β
β βββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ β
β β HTTP (localhost:5678) β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Docker Compose Environment β β
β β β β
β β βββββββββββββββββββ βββββββββββββββββββ β β
β β β Redis βββββββΊβ N8N Main β β β
β β β (Queue) β β (UI + API) β β β
β β βββββββββββββββββββ ββββββββββ¬βββββββββ β β
β β β β β
β β ββββββββββΌβββββββββ β β
β β β N8N Worker β β β
β β β (Background) β β β
β β βββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββ
β SSL Connection
β
ββββββββββββββββββββββββββββββββββ
β AWS RDS PostgreSQL β
β (Data Persistence) β
ββββββββββββββββββββββββββββββββββ
Traffic Flow
1. Initial HTTP Request (Port 80)
User β agent.fasilytics.fr:80 β Nginx β 301 Redirect β HTTPS
2. HTTPS Request (Port 443)
User β agent.fasilytics.fr:443 β Nginx (SSL decrypt) β localhost:5678 (N8N)
3. WebSocket Connection (Real-time UI)
User β HTTPS WebSocket β Nginx (upgrade headers) β N8N (WebSocket)
4. Workflow Execution
N8N Main β Workflow triggered β Redis Queue β N8N Worker executes β Results stored in RDS
Why This Approach?
Docker vs. Bare Metal:
- Bare metal install pollutes the system with Node.js, PM2, PostgreSQL client libraries
- Docker encapsulates everything, making the host clean and portable
- Rollback is instant:
docker-compose down && git checkout previous-version && docker-compose up
Nginx vs. Direct Exposure:
- N8N on port 443 requires running as root (security risk)
- Nginx handles SSL termination, freeing N8N to focus on workflows
- WebSocket timeouts and buffering can be fine-tuned in Nginx without touching N8N
Let’s Encrypt vs. Paid SSL:
- Free, automated, and trusted by all browsers
- 90-day expiration forces good renewal practices (automated via cron)
- No vendor lock-in
RDS vs. Local PostgreSQL:
- Automated backups without custom scripts
- Point-in-time recovery out of the box
- Scale storage/compute independently
- Survives EC2 termination
Prerequisites
Before we start, you’ll need:
1. AWS Account with:
- EC2 Instance
- Type: t3.medium or larger (2 vCPU, 4GB RAM minimum)
- OS: Amazon Linux 2023
- Storage: 20GB GP3 (for Docker images and logs)
- Public IP address assigned
- RDS PostgreSQL Instance
- Engine: PostgreSQL 15.x
- Instance class: db.t3.micro (can scale later)
- Storage: 20GB GP3
- Public accessibility: No (accessed via EC2 security group)
- Security Groups Configured: EC2 Security Group (Inbound Rules):
Port 22 (SSH) β Your IP or 0.0.0.0/0 Port 80 (HTTP) β 0.0.0.0/0 Port 443 (HTTPS) β 0.0.0.0/0RDS Security Group (Inbound Rules):Port 5432 (PostgreSQL) β EC2 Security Group ID
2. Domain Name
- A domain or subdomain pointing to your EC2 public IP
- Example:
agent.fasilytics.frβ35.x.x.x - DNS propagation completed (verify with
dig yourdomain.com)
3. RDS Credentials
- Endpoint (e.g.,
n8n-db.cluster-xxx.eu-west-3.rds.amazonaws.com) - Master username (usually
postgres) - Master password
- Database name (we’ll create
n8ndatabase during setup)
4. Local Machine
- SSH access to your EC2 instance
- Basic terminal knowledge
Pre-Installation Checklist
Before running any commands, verify:
# Test EC2 connectivity
ssh ec2-user@your-ec2-ip
# Test RDS connectivity from EC2
# (Install PostgreSQL client first: sudo yum install -y postgresql15)
PGPASSWORD='your-rds-password' psql \
--host="your-rds-endpoint.amazonaws.com" \
--username="postgres" \
--dbname="postgres" \
-c 'SELECT version();'
# Verify DNS resolution
dig yourdomain.com +short
# Should return your EC2 public IP
If all three tests pass, you’re ready to proceed.
Installation Guide – 22 Detailed Steps
BLOCK 0: Installing Oh-My-Zsh (Optional but Recommended)
Before diving into the installation, let’s improve your terminal experience with Oh-My-Zsh.
Why Oh-My-Zsh?
- Auto-completion for Docker, AWS CLI, Git commands
- Colored prompts for better readability
- Plugin ecosystem (we’ll enable docker and docker-compose plugins)
Commands:
# Install Zsh
sudo yum install -y zsh
# Install Oh-My-Zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
# When prompted, type 'y' to change your default shell
# Configure plugins
nano ~/.zshrc
# Find the line: plugins=(git)
# Replace with: plugins=(git docker docker-compose aws)
# Save: Ctrl+O, Enter, Ctrl+X
# Reload configuration
source ~/.zshrc
Verification:
echo $SHELL
# Should return: /bin/zsh (after reconnecting)
Anecdote: During our installation, we initially skipped Oh-My-Zsh and ran into issues with Zsh’s history expansion interpreting ! characters in passwords. Oh-My-Zsh handles this gracefully, plus the auto-completion saved us dozens of keystrokes throughout the 22 blocks.
BLOCK 1: System Update
Why this matters: Amazon Linux updates include security patches and dependency fixes. Starting with an updated system prevents conflicts.
Commands:
sudo yum update -y
What happens:
- Updates all installed packages to their latest versions
- Applies kernel security patches
- Updates system libraries
Duration: ~2-3 minutes
Verification:
# Check for remaining updates
sudo yum check-update
# Should return: "No packages marked for update"
BLOCK 2: Nginx (Reverse Proxy)
Why Nginx?
- Industry-standard reverse proxy
- Handles SSL termination efficiently
- Built-in WebSocket support (critical for N8N’s real-time UI)
- Request buffering control (prevents UI freezes during long workflows)
Commands:
sudo yum install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
What each command does:
yum install– Installs Nginx from Amazon’s repositoriessystemctl start– Starts Nginx immediatelysystemctl enable– Configures Nginx to start on boot
Verification:
# Check Nginx status
sudo systemctl status nginx
# Should show: "Active: active (running)"
# Verify Nginx is listening on port 80
sudo netstat -tlnp | grep :80
# Should show: "nginx" listening on 0.0.0.0:80
What you should see: At this point, visiting http://your-ec2-ip in a browser should show the default Nginx welcome page.
BLOCK 3: Certbot (SSL Certificate Manager)
Why Certbot?
- Official Let’s Encrypt client
- Automated certificate issuance and renewal
- Nginx integration (automatically updates Nginx config)
Commands:
sudo yum install -y epel-release 2>/dev/null || true
sudo yum install -y certbot python3-certbot-nginx
Explanation:
epel-release– Enables Extra Packages for Enterprise Linux repository2>/dev/null || true– Suppresses errors if EPEL is already installedpython3-certbot-nginx– Certbot with Nginx plugin
Verification:
certbot --version
# Should return: certbot 2.x.x
Note: We’re not requesting a certificate yet – that comes after N8N is running (BLOCK 21). Let’s Encrypt needs to verify domain ownership via HTTP, so we need Nginx serving traffic first.
BLOCK 4: Cron (Task Scheduler)
Why Cron?
- Automates SSL certificate renewal (Let’s Encrypt certificates expire every 90 days)
- Can be used for automated N8N backups later
Commands:
sudo yum install -y cronie
sudo systemctl start crond
sudo systemctl enable crond
Verification:
sudo systemctl status crond
# Should show: "Active: active (running)"
Future use: After SSL setup, we’ll add a cron job:
0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'
This runs daily at 3 AM, renewing certificates if they’re within 30 days of expiration.
BLOCK 5: Docker (Containerization)
Why Docker?
- Isolates N8N from the host system
- Guarantees consistent environment across dev/staging/prod
- Simplifies version rollbacks (just change image tag)
- Enables horizontal scaling (add more worker containers)
Commands:
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
Explanation:
yum install docker– Installs Docker Enginesystemctl start/enable– Starts Docker now and on bootusermod -aG docker– Adds your user to thedockergroup
Important: The usermod command takes effect after logout/login. Until then, use sudo docker for all Docker commands.
Verification:
sudo docker --version
# Should return: Docker version 20.x.x
sudo docker ps
# Should return empty list (no containers running yet)
BLOCK 6: Docker Compose (Container Orchestration)
Why Docker Compose?
- Manages multi-container applications (N8N + Redis + Worker)
- Single YAML file defines entire stack
- One command to start/stop everything
Commands:
sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Troubleshooting Note: On some Amazon Linux versions, you might encounter:
Error loading Python lib: libcrypt.so.1: cannot open shared object file
Fix:
sudo yum install -y libxcrypt-compat
This installs the legacy cryptography library that the Docker Compose binary requires.
Verification:
sudo docker-compose --version
# Should return: docker-compose version 2.24.0
Anecdote: We hit this exact libcrypt.so.1 error during our installation. Amazon Linux 2023 ships with libxcrypt.so.2, but the Docker Compose standalone binary still expects the older version. Five minutes of head-scratching solved by one package install.
BLOCK 7: PostgreSQL Client
Why install the client?
- Test RDS connectivity before configuring N8N
- Create the N8N database
- Debug connection issues
- Run manual SQL queries if needed
Commands:
sudo yum install -y postgresql15
Verification:
# Check client version
psql --version
# Should return: psql (PostgreSQL) 15.x
# Test RDS connection
PGPASSWORD='your-rds-password' psql \
--host="your-rds-endpoint.amazonaws.com" \
--username="postgres" \
--dbname="postgres" \
-c 'SELECT version();'
Expected output:
version
--------------------------------------------------------------------------------------------------------
PostgreSQL 15.x on x86_64-pc-linux-gnu, compiled by gcc (GCC) 7.3.1 20180712 (Red Hat 7.3.1-12), 64-bit
(1 row)
If connection fails:
- Verify RDS Security Group allows port 5432 from EC2 Security Group
- Check RDS endpoint is correct
- Verify password has no typos
BLOCK 8: Python, Git
Why these packages?
- Python 3: Required by Certbot and some N8N nodes
- pip: Python package manager (useful for custom scripts later)
- Git: Version control (useful if you want to track N8N configuration changes)
Commands:
sudo yum install -y python3 python3-pip git
Verification:
python3 --version # Should return: Python 3.x.x
pip3 --version # Should return: pip 21.x.x
git --version # Should return: git version 2.x.x
BLOCK 9: Node.js + PM2
Disclaimer: In our Docker-based architecture, Node.js and PM2 are not strictly necessary. N8N runs inside containers, not directly on the host. However, the original script includes them, and they can be useful for:
- Running custom Node.js scripts alongside N8N
- Using PM2 to manage other services if needed
Commands:
# Install Node.js 22 (latest LTS)
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
sudo yum install -y nodejs
# Install PM2 (process manager)
sudo npm install -g pm2
Verification:
node --version # Should return: v22.x.x
npm --version # Should return: v10.x.x
pm2 --version # Should return: v5.x.x
What is PM2? PM2 is a production process manager for Node.js. It provides:
- Automatic restarts on crashes
- Log management
- Monitoring (CPU/RAM usage)
- Cluster mode (run multiple instances)
Example usage (if you were NOT using Docker):
pm2 start n8n --name "n8n-instance"
pm2 startup # Configure PM2 to start on boot
pm2 save # Save current process list
In our case: Docker’s restart: always policy replaces PM2’s auto-restart functionality.
Anecdote: During setup, we got a deprecation warning about Node.js 18. We upgraded to Node.js 22 (latest LTS) to avoid future compatibility issues. Always install the latest LTS version unless you have a specific reason not to.
BLOCK 10: Create Working Directory
Why a dedicated directory?
- Keeps all N8N configuration files organized
- Easy to backup (just tar the entire directory)
- Clear separation from system files
Commands:
mkdir -p ~/n8n-docker
cd ~/n8n-docker
Verification:
pwd
# Should return: /home/ec2-user/n8n-docker
What we’ll store here:
~/n8n-docker/
βββ .env # Environment variables
βββ docker-compose.yml # Container orchestration
βββ rds-ca.pem # RDS SSL certificate
βββ init-data.sh # Database initialization script
BLOCK 11: Download RDS SSL Certificate
Why this certificate?
- RDS requires SSL for production workloads
- Certificate verifies the RDS endpoint is genuine (not a man-in-the-middle)
- N8N will mount this certificate to connect securely to RDS
Commands:
wget https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem -O rds-ca.pem
Verification:
ls -lh rds-ca.pem
# Should show a file ~240 KB in size
Technical detail: This certificate bundle contains all AWS RDS Certificate Authority certificates. When N8N connects to RDS, it uses this bundle to verify the RDS server’s SSL certificate is signed by a trusted AWS CA.
Connection flow:
N8N β SSL Handshake β RDS
β
Verify RDS cert using rds-ca.pem
β
Encrypted connection established
BLOCK 12: Generate N8N Encryption Key
Why an encryption key? N8N encrypts sensitive data in the database:
- API keys
- OAuth tokens
- Webhook secrets
- Password credentials
Without this key, your credentials are stored in plain text (huge security risk).
Commands:
N8N_ENCRYPTION_KEY=$(openssl rand -base64 32)
echo $N8N_ENCRYPTION_KEY
What this does:
openssl rand -base64 32– Generates 32 random bytes, base64-encoded (44 characters)- Stores in environment variable
$N8N_ENCRYPTION_KEY
Example output:
a3B9xK2mP8qR5tV7wY1zC4dF6gH8jL0nM3pQ5sT7vX9=
CRITICAL WARNING: If you lose this key, you cannot decrypt your stored credentials. N8N will start, but all encrypted fields will be unreadable.
Best practice: After installation, save this key to a password manager or encrypted vault. Later, we’ll save it to a file as a backup.
BLOCK 13: Create .env File
Why .env?
- Centralizes all configuration in one file
- Docker Compose automatically loads variables from
.env - Keeps secrets out of
docker-compose.yml(which might be committed to git)
Commands:
cat > .env << EOF
# AWS RDS Config
RDS_HOST=your-rds-endpoint.amazonaws.com
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-rds-password
POSTGRES_DB=n8n
# Auth n8n (disabled by default - N8N has its own auth system)
N8N_BASIC_AUTH_ACTIVE=false
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=YourAdminPassword
# Domain & SSL Config
DOMAIN_NAME=yourdomain.com
N8N_PATH=/
# Redis Config (internal Docker network)
REDIS_HOST=redis
# N8N Encryption Key (CRITICAL - BACKUP THIS!)
N8N_ENCRYPTION_KEY=$N8N_ENCRYPTION_KEY
# N8N Optimizations
N8N_RUNNERS_ENABLED=false
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
# WebSocket and Performance
N8N_DISABLE_UI=false
N8N_METRICS=false
# Community Packages (AI Agent tools, custom nodes)
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
# Email for SSL Certificate
SSL_EMAIL=your-email@example.com
EOF
Replace these values:
your-rds-endpoint.amazonaws.comβ Your actual RDS endpointyour-rds-passwordβ Your actual RDS master passwordyourdomain.comβ Your domain (e.g.,agent.fasilytics.fr)your-email@example.comβ Your email for Let’s Encrypt notifications
Key configuration explained:
Database settings:
RDS_HOST=... # Where to connect
POSTGRES_USER=postgres # Database user
POSTGRES_PASSWORD=... # Database password
POSTGRES_DB=n8n # Database name (we'll create this next)
N8N Authentication:
N8N_BASIC_AUTH_ACTIVE=false
We disable HTTP Basic Auth because N8N has its own user management system (more flexible). On first login, N8N will prompt you to create the admin account.
Execution mode:
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
This sends heavy workflows to the worker container, keeping the UI responsive.
Community packages:
N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
Allows AI Agent nodes to use community packages as tools (useful for LangChain integrations).
Verification:
cat .env
# Review the file - make sure all values are correct
BLOCK 14: Create Database Initialization Script
Why a separate script?
- Ensures the N8N database exists before containers start
- Grants necessary permissions to the PostgreSQL user
- Reusable for future database resets
Commands:
cat > init-data.sh << 'SCRIPT_EOF'
#!/bin/bash
set -e
RDS_HOST=$1
POSTGRES_USER=$2
POSTGRES_PASSWORD=$3
POSTGRES_DB=$4
export PGPASSWORD=$POSTGRES_PASSWORD
# Check if database exists
DB_EXISTS=$(psql -v ON_ERROR_STOP=1 --host="$RDS_HOST" --username="$POSTGRES_USER" --dbname="postgres" --tuples-only --command="SELECT 1 FROM pg_database WHERE datname='$POSTGRES_DB';")
if [[ -z $DB_EXISTS ]]; then
echo "Database $POSTGRES_DB does not exist. Creating..."
psql -v ON_ERROR_STOP=1 --host="$RDS_HOST" --username="$POSTGRES_USER" --dbname="postgres" --command="CREATE DATABASE \"$POSTGRES_DB\";"
else
echo "Database $POSTGRES_DB already exists."
fi
# Grant permissions
psql -v ON_ERROR_STOP=1 --host="$RDS_HOST" --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" <<EOSQL
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO $POSTGRES_USER;
GRANT CREATE ON SCHEMA public TO $POSTGRES_USER;
EOSQL
unset PGPASSWORD
SCRIPT_EOF
chmod +x init-data.sh
What this script does:
1. Set error handling:
set -e
Exit immediately if any command fails (prevents partial database setup).
2. Check database existence:
SELECT 1 FROM pg_database WHERE datname='n8n';
Returns 1 if database exists, empty if not.
3. Create database if missing:
CREATE DATABASE "n8n";
4. Grant permissions:
GRANT ALL PRIVILEGES ON DATABASE "n8n" TO postgres;
GRANT CREATE ON SCHEMA public TO postgres;
This allows N8N to create tables during its first startup (database migrations).
5. Clean up:
unset PGPASSWORD
Removes password from environment (security best practice).
Verification:
ls -lh init-data.sh
# Should show: -rwxr-xr-x (executable permissions)
BLOCK 15: Test RDS Connection and Initialize Database
Why test first?
- Catches connectivity issues before Docker containers start
- Validates Security Group configuration
- Creates the database N8N will use
Commands:
# Test connection with retry logic
until PGPASSWORD='your-rds-password' psql \
--host="your-rds-endpoint.amazonaws.com" \
--username="postgres" \
--dbname="postgres" \
-c '\q' 2>/dev/null; do
echo "Waiting for PostgreSQL..."
sleep 5
done
echo "β
PostgreSQL accessible - Initializing database..."
# Run initialization script
./init-data.sh "your-rds-endpoint.amazonaws.com" "postgres" 'your-rds-password' "n8n"
What the loop does:
- Attempts to connect to RDS every 5 seconds
\q– Quit command (doesn’t actually query, just tests connection)2>/dev/null– Suppresses error messages during retry attempts- Exits loop when connection succeeds
Expected output:
Waiting for PostgreSQL...
Waiting for PostgreSQL...
β
PostgreSQL accessible - Initializing database...
Database n8n does not exist. Creating...
CREATE DATABASE
GRANT
GRANT
Troubleshooting Zsh Password Issue:
If you see:
zsh: event not found: WbXK48TFe!
This is because Zsh interprets ! as history expansion. Solution: Use single quotes around the password:
PGPASSWORD='your-password-with-!-chars'
Verification:
# List databases
PGPASSWORD='your-rds-password' psql \
--host="your-rds-endpoint.amazonaws.com" \
--username="postgres" \
--dbname="postgres" \
-c '\l'
You should see n8n in the database list.
Anecdote: We ran into the Zsh ! issue during our installation. The script worked fine in Bash but broke in Zsh. Switching to single quotes fixed it instantly. This is why we recommend Oh-My-Zsh earlier – it handles these edge cases better.
BLOCK 16: Create docker-compose.yml
Why Docker Compose?
- Defines all three services (Redis, N8N Main, N8N Worker) in one file
- Manages dependencies (Worker won’t start until N8N Main is healthy)
- Automatically creates Docker networks and volumes
Commands:
cat > docker-compose.yml << 'EOF'
version: '3.8'
volumes:
n8n_storage:
redis_storage:
services:
redis:
image: redis:6-alpine
restart: always
volumes:
- redis_storage:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=${RDS_HOST}
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_BASIC_AUTH_ACTIVE=false
- DB_POSTGRESDB_SSL_CA=/rds-ca.pem
- DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=false
- N8N_HOST=${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://${DOMAIN_NAME}/
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_RUNNERS_ENABLED=false
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true
ports:
- 5678:5678
volumes:
- n8n_storage:/home/node/.n8n
- ./rds-ca.pem:/rds-ca.pem
depends_on:
- redis
n8n-worker:
image: docker.n8n.io/n8nio/n8n
restart: always
command: worker
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=${RDS_HOST}
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- DB_POSTGRESDB_SSL_CA=/rds-ca.pem
- DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=false
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_RUNNERS_ENABLED=false
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- ./rds-ca.pem:/rds-ca.pem
depends_on:
- redis
- n8n
EOF
Architecture breakdown:
Service 1: Redis
redis:
image: redis:6-alpine
restart: always
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
- Image: Redis 6 (Alpine = lightweight Linux)
- Restart policy: Always restart on failure
- Healthcheck: Pings Redis every 5 seconds to verify it’s responsive
- Use case: Message queue for workflow execution
Service 2: N8N Main
n8n:
ports:
- 5678:5678
depends_on:
- redis
- Ports: Exposes container port 5678 to host port 5678
- Dependency: Won’t start until Redis is healthy
- Role: Serves the UI and API
Key environment variables:
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
This configures N8N to use Redis for job queuing. Heavy workflows are sent to the worker.
- DB_POSTGRESDB_SSL_CA=/rds-ca.pem
- DB_POSTGRESDB_SSL_REJECT_UNAUTHORIZED=false
Enables SSL connection to RDS. SSL_REJECT_UNAUTHORIZED=false allows self-signed certificates (acceptable for AWS RDS).
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://${DOMAIN_NAME}/
Tells N8N it’s behind an HTTPS proxy. Webhooks will generate https:// URLs.
Service 3: N8N Worker
n8n-worker:
command: worker
depends_on:
- redis
- n8n
- Command: Runs in worker mode (no UI, just workflow execution)
- Dependencies: Waits for both Redis and N8N Main
- No ports exposed: Workers don’t serve HTTP traffic
Volumes:
volumes:
n8n_storage: # Stores N8N data (/home/node/.n8n)
redis_storage: # Stores Redis data (/data)
Docker manages these volumes. Data persists even if containers are removed.
Variables from .env:
- DB_POSTGRESDB_HOST=${RDS_HOST}
Docker Compose automatically reads .env and substitutes variables.
Verification:
cat docker-compose.yml
# Review the file - should match the structure above
BLOCK 17: Start Docker Containers
The moment of truth. This command launches all three containers.
Commands:
sudo docker-compose up -d
Flags:
up– Create and start containers-d– Detached mode (runs in background)
What happens:
1. Image pulling (first run only):
Pulling redis (redis:6-alpine)...
Pulling n8n (docker.n8n.io/n8nio/n8n:latest)...
Docker downloads images from Docker Hub and N8N’s registry. This can take 2-5 minutes depending on connection speed.
2. Network creation:
Creating network "n8n-docker_default" with the default driver
Docker creates an internal network for the containers to communicate.
3. Volume creation:
Creating volume "n8n-docker_n8n_storage"
Creating volume "n8n-docker_redis_storage"
4. Container startup:
Creating n8n-docker_redis_1 ... done
Creating n8n-docker_n8n_1 ... done
Creating n8n-docker_n8n-worker_1 ... done
Verification:
sudo docker-compose ps
Expected output:
NAME STATE
n8n-docker_redis_1 Up (healthy)
n8n-docker_n8n_1 Up
n8n-docker_n8n-worker_1 Up
All three containers should show Up.
Troubleshooting:
If you see a warning:
WARNING: The DB_POSTGRESDB_PASSWORD variable is not set. Defaulting to a blank string.
This means Docker Compose didn’t load the .env file. However, since we put credentials directly in docker-compose.yml using ${POSTGRES_PASSWORD}, this warning is misleading – the containers still have the correct password.
To eliminate the warning:
# Stop containers
sudo docker-compose down
# Recreate docker-compose.yml with variables from .env
# (Already done if you followed BLOCK 16)
# Restart
sudo docker-compose up -d
BLOCK 18: Wait for N8N to Start
Why wait? N8N needs 1-2 minutes to:
- Connect to RDS
- Run database migrations (create tables, indexes)
- Initialize Redis connection
- Start the HTTP server
If we configure Nginx before N8N is ready, requests will fail.
Commands:
echo "β³ Waiting for N8N to start..."
RETRY_COUNT=0
MAX_RETRIES=20
WAIT_TIME=30
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
sleep $WAIT_TIME
if curl -f http://localhost:5678 > /dev/null 2>&1; then
echo "β
N8N is accessible locally"
break
else
RETRY_COUNT=$((RETRY_COUNT+1))
echo "β³ N8N starting... ($RETRY_COUNT/$MAX_RETRIES) - Waiting ${WAIT_TIME}s"
if [ $RETRY_COUNT -eq 5 ]; then
echo "π Checking N8N logs..."
sudo docker-compose logs --tail=10 n8n
fi
fi
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "β N8N did not start after 10 minutes"
sudo docker-compose logs n8n
exit 1
fi
What this does:
1. Retry loop:
- Attempts to connect to
http://localhost:5678every 30 seconds - Maximum 20 attempts = 10 minutes timeout
2. Progress indicator:
β³ N8N starting... (1/20) - Waiting 30s
β³ N8N starting... (2/20) - Waiting 30s
3. Log output after 5 attempts: After 2.5 minutes (5 Γ 30s), shows the last 10 log lines to diagnose issues.
4. Success:
β
N8N is accessible locally
Expected logs (healthy startup):
n8n-docker_n8n_1 | Initializing n8n process
n8n-docker_n8n_1 | Version: 1.x.x
n8n-docker_n8n_1 | Database: Migrations running...
n8n-docker_n8n_1 | Database: Migrations completed
n8n-docker_n8n_1 | Editor is now accessible via:
n8n-docker_n8n_1 | http://localhost:5678/
Manual verification:
# Check logs in real-time
sudo docker-compose logs -f n8n
# Test HTTP endpoint
curl -I http://localhost:5678
# Should return: HTTP/1.1 200 OK
Common issues:
Issue 1: N8N crashes immediately
sudo docker-compose logs n8n
# Look for error messages
Possible causes:
- RDS connection failed (check credentials in
.env) - Database permissions issue (verify BLOCK 15 ran successfully)
Issue 2: N8N hangs during migrations
Database: Running migration CreateWorkflowsTable
Solution: Wait longer. First-time migrations can take 2-3 minutes on slow RDS instances.
Anecdote: During our installation, N8N took 90 seconds to start (within the timeout). After 5 attempts, we checked the logs and saw it was still running migrations. Patience paid off – it came online after attempt 7.
BLOCK 19: Redis Optimization
Why this matters: Redis logs a warning without this setting:
WARNING overcommit_memory is set to 0! Background save may fail
What is overcommit_memory?
- Linux memory allocation policy
0= Kernel estimates available memory before allocating1= Kernel always allows memory allocation (Redis recommendation)
Commands:
sudo sysctl vm.overcommit_memory=1
echo 'vm.overcommit_memory = 1' | sudo tee -a /etc/sysctl.conf > /dev/null
Explanation:
sysctl– Sets the value immediately (runtime)echo ... >> /etc/sysctl.conf– Persists across reboots
Verification:
sysctl vm.overcommit_memory
# Should return: vm.overcommit_memory = 1
Effect: Redis warning disappears from logs. No functional impact on N8N, just cleaner logs.
BLOCK 20: Configure Nginx (HTTP Temporary)
Why start with HTTP? Let’s Encrypt needs to access your server via HTTP on port 80 to verify domain ownership. We’ll upgrade to HTTPS after obtaining the certificate.
Commands:
sudo tee /etc/nginx/conf.d/n8n.conf > /dev/null <<'EOF'
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffering off;
proxy_request_buffering off;
}
}
EOF
sudo nginx -t && sudo systemctl reload nginx
Replace: yourdomain.com with your actual domain (e.g., agent.fasilytics.fr)
Configuration breakdown:
Basic proxy:
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:5678;
- Listen on port 80 (HTTP)
- Forward all requests to N8N on
localhost:5678
Headers:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Host– Original domain nameX-Real-IP– Client’s IP addressX-Forwarded-For– Chain of proxies (for logging)X-Forwarded-Proto– Original protocol (http/https)
WebSocket support (critical for N8N):
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Without these headers, N8N’s real-time UI won’t work. You’d see:
- Workflows not updating in real-time
- Manual execution not showing progress
- Disconnection errors in browser console
Timeouts:
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
Long workflows (e.g., processing 10,000 rows) can take minutes. These timeouts prevent Nginx from killing the connection prematurely.
Buffering:
proxy_buffering off;
proxy_request_buffering off;
Disables Nginx’s buffering for real-time streaming. Without this, you’d see delays in workflow execution output.
Verification:
# Test Nginx configuration syntax
sudo nginx -t
# Should return: syntax is ok, test is successful
# Test HTTP access
curl -I http://yourdomain.com
# Should return: HTTP/1.1 200 OK
Traffic flow now:
Internet β yourdomain.com:80 β Nginx β localhost:5678 β N8N
Troubleshooting:
Issue: nginx: [emerg] invalid number of arguments in "proxy_pass"
Cause: Stray backslashes in the config (e.g., proxy_pass http://127.0.0.1:5678\;)
Fix:
sudo nano /etc/nginx/conf.d/n8n.conf
# Remove any backslashes before semicolons
# Save: Ctrl+O, Enter, Ctrl+X
sudo nginx -t && sudo systemctl reload nginx
Anecdote: We hit this exact issue during our setup. The heredoc syntax with unquoted EOF caused the shell to interpret backslashes. Using <<'EOF' (with quotes) prevents this. Five minutes of head-scratching over a single character.
BLOCK 21: Obtain SSL Certificate
Why Let’s Encrypt?
- Free, automated, and trusted by all browsers
- 90-day expiration encourages automation (via cron)
- No vendor lock-in
Commands:
sudo certbot certonly --nginx \
-d yourdomain.com \
--non-interactive \
--agree-tos \
--email your-email@example.com
Replace:
yourdomain.comβ Your domainyour-email@example.comβ Your email (for renewal notifications)
Flags:
certonly– Only obtain certificate (don’t modify Nginx config)--nginx– Use Nginx plugin for domain verification--non-interactive– Don’t prompt for input--agree-tos– Accept Let’s Encrypt Terms of Service
What happens:
1. Domain verification: Certbot creates a temporary file at:
http://yourdomain.com/.well-known/acme-challenge/random-token
Let’s Encrypt requests this URL. If successful, domain ownership is verified.
2. Certificate generation:
Requesting a certificate for yourdomain.com
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/yourdomain.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/yourdomain.com/privkey.pem
3. Files created:
/etc/letsencrypt/live/yourdomain.com/
βββ cert.pem # Your certificate
βββ chain.pem # Intermediate certificates
βββ fullchain.pem # cert.pem + chain.pem (use this in Nginx)
βββ privkey.pem # Private key (keep secret!)
βββ README # Instructions
Verification:
sudo ls -lh /etc/letsencrypt/live/yourdomain.com/
# Should show 4 .pem files + README
Certificate validity:
sudo openssl x509 -in /etc/letsencrypt/live/yourdomain.com/cert.pem -noout -dates
# Should show:
# notBefore=Dec 22 18:00:00 2024 GMT
# notAfter=Mar 22 18:00:00 2025 GMT (90 days later)
Common issues:
Issue 1: Domain not pointing to EC2
Failed authorization procedure. yourdomain.com (http-01):
Connection refused
Fix: Verify DNS with dig yourdomain.com +short – should return your EC2 IP.
Issue 2: Port 80 blocked
Error: Fetching http://yourdomain.com/.well-known/acme-challenge/...:
Connection timed out
Fix: Check EC2 Security Group allows inbound port 80 from 0.0.0.0/0.
Issue 3: Nginx not running
nginx: [error] open() "/run/nginx.pid" failed
Fix: sudo systemctl start nginx
BLOCK 22: Configure Nginx (HTTPS Final)
Now that we have an SSL certificate, let’s upgrade to HTTPS and redirect all HTTP traffic.
Commands:
sudo tee /etc/nginx/conf.d/n8n.conf > /dev/null <<'EOF'
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffering off;
proxy_request_buffering off;
}
}
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
EOF
sudo nginx -t && sudo systemctl reload nginx
Replace: yourdomain.com with your actual domain
Configuration breakdown:
HTTPS Server Block:
server {
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
- Listen on port 443 (HTTPS)
- Use Let’s Encrypt certificates
- Same proxy configuration as before (WebSocket support, timeouts, etc.)
HTTP Redirect Block:
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
- Any HTTP request β Permanent redirect (301) to HTTPS
- Preserves original path (e.g.,
http://domain/workflow/123βhttps://domain/workflow/123)
Verification:
# Test Nginx syntax
sudo nginx -t
# Should return: syntax is ok, test is successful
# Test HTTPS access
curl -I https://yourdomain.com
# Should return: HTTP/1.1 200 OK
# Test HTTP redirect
curl -I http://yourdomain.com
# Should return: HTTP/1.1 301 Moved Permanently
# Location: https://yourdomain.com/
Final traffic flow:
Internet β yourdomain.com:80 β Nginx β 301 Redirect β HTTPS
Internet β yourdomain.com:443 β Nginx (SSL decrypt) β localhost:5678 β N8N
Certificate auto-renewal:
Let’s Encrypt certificates expire every 90 days. Set up automatic renewal:
# Add cron job (runs daily at 3 AM)
(crontab -l 2>/dev/null; echo "0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
# Verify cron job
crontab -l
# Should show: 0 3 * * * certbot renew --quiet --post-hook 'systemctl reload nginx'
What this does:
- Runs
certbot renewdaily at 3 AM - Certbot checks if certificates are within 30 days of expiration
- If yes, renews automatically
--post-hookreloads Nginx after renewal (applies new certificates)
Test renewal (dry run):
sudo certbot renew --dry-run
# Should succeed without errors
Troubleshooting Real Issues
Issue 1: Docker Compose “libcrypt.so.1” Error
Symptom:
Error loading Python lib '/tmp/_MEIslZi1a/libpython3.7m.so.1.0':
dlopen: libcrypt.so.1: cannot open shared object file
Cause: Amazon Linux 2023 ships with libxcrypt.so.2, but Docker Compose binary expects the legacy libxcrypt.so.1.
Fix:
sudo yum install -y libxcrypt-compat
Why this works: libxcrypt-compat provides the older library version as a compatibility layer.
Issue 2: Zsh Password with Special Characters
Symptom:
zsh: event not found: WbXK48TFe!
Cause: Zsh interprets ! as history expansion.
Fix: Use single quotes (not double quotes):
# Wrong (double quotes)
PGPASSWORD="myPassword!123"
# Right (single quotes)
PGPASSWORD='myPassword!123'
Why this works: Single quotes prevent all shell expansions (variables, history, etc.).
Issue 3: Nginx “invalid number of arguments”
Symptom:
nginx: [emerg] invalid number of arguments in "proxy_pass" directive
Cause: Stray backslashes before semicolons (e.g., proxy_pass http://127.0.0.1:5678\;)
Fix:
sudo nano /etc/nginx/conf.d/n8n.conf
# Remove backslashes before semicolons
# Save and exit
sudo nginx -t && sudo systemctl reload nginx
Prevention: Use heredoc with single quotes:
cat > file.conf << 'EOF'
# Content here (no shell expansion)
EOF
Issue 4: N8N Not Starting After 10 Minutes
Symptom:
β N8N did not start after 10 minutes
Diagnosis:
sudo docker-compose logs n8n
Common causes:
A) RDS connection failure
Error: connect ECONNREFUSED your-rds-endpoint:5432
Fix: Verify RDS Security Group allows port 5432 from EC2 Security Group.
B) Database permission error
ERROR: permission denied for schema public
Fix: Re-run BLOCK 15 (database initialization script).
C) Encryption key mismatch
Error: Stored data could not be decrypted
Fix: This happens if N8N_ENCRYPTION_KEY changed. Either:
- Restore the original key from backup
- Reset N8N (deletes all encrypted data):
sudo docker-compose down -v# Re-run BLOCK 12-17
Issue 5: Let’s Encrypt Certificate Request Failed
Symptom:
Failed authorization procedure. yourdomain.com (http-01):
Connection timed out
Checklist:
- DNS pointing to EC2?
dig yourdomain.com +short # Should return EC2 public IP - Port 80 open?
- Check EC2 Security Group Inbound Rules
- Should allow
0.0.0.0/0on port 80
- Nginx serving HTTP?
curl -I http://yourdomain.com # Should return 200 OK (not connection refused) - Certbot accessing correct domain?
# Check Nginx config grep server_name /etc/nginx/conf.d/n8n.conf # Should match your domain exactly
Issue 6: WebSocket Disconnections in UI
Symptom: N8N UI shows “Connection lost” or workflows freeze mid-execution.
Cause: Missing WebSocket headers or timeout too short.
Fix: Verify Nginx configuration includes:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
Test WebSocket:
# Install wscat
npm install -g wscat
# Test connection
wscat -c wss://yourdomain.com/
# Should connect without errors
Post-Installation Management
Useful Commands
View running containers:
cd ~/n8n-docker
sudo docker-compose ps
View N8N logs:
sudo docker-compose logs -f n8n
# -f follows logs in real-time (Ctrl+C to exit)
View Worker logs:
sudo docker-compose logs -f n8n-worker
Restart N8N:
sudo docker-compose restart n8n
Restart all containers:
sudo docker-compose restart
Stop N8N:
sudo docker-compose down
# Stops and removes containers (data persists in volumes)
Start N8N:
sudo docker-compose up -d
Update N8N to latest version:
cd ~/n8n-docker
sudo docker-compose pull # Download latest images
sudo docker-compose up -d # Recreate containers
View Docker volumes (data storage):
docker volume ls
# Shows: n8n-docker_n8n_storage, n8n-docker_redis_storage
Backup N8N data:
# Backup encryption key (CRITICAL)
echo $N8N_ENCRYPTION_KEY > ~/n8n_encryption_key_backup.txt
# Backup entire N8N directory
cd ~
tar -czf n8n-backup-$(date +%Y%m%d).tar.gz n8n-docker/
# Backup RDS (via AWS)
aws rds create-db-snapshot \
--db-instance-identifier your-rds-instance \
--db-snapshot-identifier n8n-backup-$(date +%Y%m%d)
Check disk usage:
# Docker images and containers
docker system df
# EC2 disk space
df -h
Clean up unused Docker resources:
# Remove stopped containers, unused images, unused volumes
docker system prune -a --volumes
# Warning: This deletes ALL unused Docker data
Accessing N8N
Open your browser and navigate to:
https://yourdomain.com
First-time setup:
- N8N prompts you to create an admin account
- Fill in:
- First name
- Last name
- Password
- Click “Get started”
You’re in! Start creating workflows.
Security Best Practices
1. Backup encryption key immediately:
# Save to a secure location (password manager, encrypted USB)
cat ~/n8n-docker/.env | grep N8N_ENCRYPTION_KEY
2. Restrict SSH access:
# Edit EC2 Security Group
# Change SSH source from 0.0.0.0/0 to your IP only
3. Enable RDS encryption at rest:
- AWS Console β RDS β Modify instance β Enable encryption
- (Requires snapshot β restore to encrypted instance)
4. Regular backups:
# Add to crontab (daily at 2 AM)
0 2 * * * cd ~/n8n-docker && tar -czf ~/n8n-backup-$(date +\%Y\%m\%d).tar.gz .
5. Monitor logs for errors:
# Check N8N logs daily
sudo docker-compose logs --tail=50 n8n
Conclusion
What we built:
- Production-ready N8N installation on AWS EC2
- SSL-secured with auto-renewing Let’s Encrypt certificates
- Persistent data storage on RDS PostgreSQL
- Distributed execution with Redis queue and worker containers
- Reverse proxy with WebSocket support via Nginx
Architecture benefits:
- Isolation: Docker containers prevent system pollution
- Scalability: Add more workers by editing
docker-compose.yml - Reliability: RDS backups, container auto-restart policies
- Security: SSL encryption, separate database instance
- Maintainability: Single-command updates and restarts
Total setup time: 1-2 hours (mostly waiting for downloads and certificates)
Next steps:
- Create your first workflow
- Connect integrations (APIs, databases, webhooks)
- Set up automated backups
- Explore N8N’s AI Agent nodes for LangChain workflows
Resources:
- N8N Documentation: https://docs.n8n.io/
- Docker Compose Reference: https://docs.docker.com/compose/
- Let’s Encrypt: https://letsencrypt.org/
- Nginx Configuration Guide: https://nginx.org/en/docs/