Self-hosted n8n automation stack with secure AWS deployment, PostgreSQL support, and full CloudFormation infrastructure.
- Why This Solution?
- Features
- Quick Start
- AWS Deployment
- Project Structure
- Configuration
- Backup and Restore
- Security
- Troubleshooting
- FAQ
- Extend This Stack
- Contributing
- 💰 Cost-Optimized: $5-10/month vs $25+ for managed solutions
- 🔒 Secure by Default: Private subnet deployment, SSM access only, no public IPs
- 🚀 Production-Ready: Auto-healing via ASG, automated backups, CloudWatch monitoring
- 🛠️ Zero Lock-in: Your data, your infrastructure, export and migrate anytime
- 📦 Simple Architecture: Just EC2 + PostgreSQL, no Kubernetes complexity
- ⚡ Infrastructure as Code: Fully automated CloudFormation deployment
- 🎯 Single-Command Deploy: From zero to running in ~5 minutes
- Docker Compose based deployment
- PostgreSQL backend
- Automatic encryption key generation
- Persistent data storage
- AWS CloudFormation templates for production deployment
- Secure AWS deployment with SSM port forwarding
- Clone this repository:
git clone https://github.com/cunicopia-dev/n8n-aws-self-hosting.git
cd n8n-aws-self-hosting- Run the installation script:
# With SQLite (default)
./install-n8n.sh
# With PostgreSQL
./install-n8n.sh --postgres- Access n8n at http://localhost:5678
To switch from SQLite to PostgreSQL after initial setup:
-
Edit
.envfile and change:DB_TYPE=sqlitetoDB_TYPE=postgresdb- Uncomment and configure the PostgreSQL settings
-
Restart with PostgreSQL profile:
docker compose down
docker compose --profile postgres up -dThe infrastructure now supports namespaced deployments, allowing you to run separate n8n instances for different clients or environments. Each namespace creates isolated resources with no shared dependencies.
graph TB
subgraph "Your Computer"
Browser[Web Browser<br/>localhost:5678]
Terminal[Terminal/CLI]
end
subgraph "AWS Infrastructure"
subgraph "CloudFormation Stacks"
CF1[S3 Stack]
CF2[IAM Stack]
CF3[EC2 Stack]
end
subgraph "Resources"
S3[(S3 Bucket<br/>File Storage)]
IAM[IAM Role +<br/>Instance Profile]
subgraph "VPC"
ASG[Auto Scaling Group<br/>min=1, max=1]
LT[Launch Template]
EC2[EC2 Instance<br/>t4g.small/medium<br/>+ n8n + PostgreSQL 17]
end
end
SSM[Systems Manager]
end
Browser -.->|Port Forward| Terminal
Terminal -->|SSM Session| SSM
SSM -->|Private Connection| EC2
CF1 -->|Creates| S3
CF2 -->|Creates| IAM
CF3 -->|Creates| ASG
CF3 -->|Creates| LT
ASG -->|Uses| LT
LT -->|Launches| EC2
IAM -->|Attached to| EC2
EC2 -->|Store Files| S3
EC2 -->|Internet Access| Internet((Internet))
style EC2 fill:#e1f5fe,color:#01579b
style S3 fill:#fff3e0,color:#e65100
style SSM fill:#f3e5f5,color:#4a148c
style CF1 fill:#c8e6c9,color:#1b5e20
style CF2 fill:#c8e6c9,color:#1b5e20
style CF3 fill:#c8e6c9,color:#1b5e20
Note: AWS deployments now default to PostgreSQL 17 for better performance and reliability. The EC2 instance includes PostgreSQL client tools for database management and backups.
- AWS CLI: Installation guide
- AWS SAM CLI: Installation guide
- AWS Session Manager Plugin: Installation guide (required for SSM connections)
- Docker and Docker Compose v2.20.2+ (for local development)
- AWS credentials configured (
aws configure) - VPC with at least one subnet
- Appropriate AWS permissions for CloudFormation, EC2, S3, IAM, and SSM
💰 Estimated Cost: With AWS EC2 RIs, you can get a t4g.small for as low as $5 a month. This setup is ideal for lean, production-grade automation infrastructure without the managed service tax.
You have two options for deployment:
- Deploy the S3 bucket:
cd infra
aws cloudformation create-stack \
--stack-name n8n-s3 \
--template-body file://s3.yaml- Deploy IAM roles:
aws cloudformation create-stack \
--stack-name n8n-iam \
--template-body file://iam.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=S3BucketArn,ParameterValue=$(aws cloudformation describe-stacks --stack-name n8n-s3 --query 'Stacks[0].Outputs[?OutputKey==`BucketArn`].OutputValue' --output text)- Deploy EC2 instance:
- Make sure to replace the
VpcIdand theSubnetIdwith real values.
aws cloudformation create-stack \
--stack-name n8n-ec2 \
--template-body file://ec2.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxxxxx \
ParameterKey=SubnetId,ParameterValue=subnet-xxxxxx \
ParameterKey=InstanceType,ParameterValue=t4g.small \
ParameterKey=InstanceProfileName,ParameterValue=$(aws cloudformation describe-stacks --stack-name n8n-iam --query 'Stacks[0].Outputs[?OutputKey==`InstanceProfileName`].OutputValue' --output text) \
ParameterKey=S3BucketName,ParameterValue=$(aws cloudformation describe-stacks --stack-name n8n-s3 --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text)- Get your instance ID:
# Wait a minute for the instance to launch, then:
ASG_NAME=$(aws cloudformation describe-stacks \
--stack-name n8n-ec2 \
--query 'Stacks[0].Outputs[?OutputKey==`AutoScalingGroupName`].OutputValue' \
--output text)
INSTANCE_ID=$(aws autoscaling describe-auto-scaling-groups \
--auto-scaling-group-names "$ASG_NAME" \
--query 'AutoScalingGroups[0].Instances[0].InstanceId' \
--output text)
echo "Your instance ID is: $INSTANCE_ID"- Connect to n8n:
# Terminal 1 - SSH into the instance (optional)
aws ssm start-session --target $INSTANCE_ID
# Terminal 2 - Port forward n8n (requires Session Manager plugin)
aws ssm start-session \
--target $INSTANCE_ID \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["5678"],"localPortNumber":["5678"]}'Note: If you get an error about the Session Manager plugin, make sure you've installed it from the prerequisites above.
- Access n8n at http://localhost:5678
The deployment script now supports namespaced deployments for hosting multiple isolated n8n instances:
cd infra
# Deploy for a specific client/namespace
./deploy.sh --namespace acme-corp --vpc-id vpc-xxxxxx --subnet-id subnet-xxxxxx
# Deploy another instance for a different client
./deploy.sh --namespace contoso-ltd --vpc-id vpc-xxxxxx --subnet-id subnet-xxxxxxEach namespace creates completely isolated resources:
- Separate EC2 instance
- Dedicated S3 bucket
- Isolated IAM roles and permissions
- Independent n8n installation
To remove a deployment:
./teardown.sh --namespace acme-corpn8n-aws-self-hosting/
├── docker-compose.yml # Docker Compose configuration
├── install-n8n.sh # Installation script
├── .env # Environment configuration (created on first run)
├── n8n_data/ # n8n persistent data
├── local-files/ # Local file storage
├── workflows/ # n8n workflow exports
├── prompts/ # Prompt templates
└── infra/ # AWS CloudFormation templates
├── s3.yaml # S3 bucket configuration
├── ec2.yaml # EC2 instance configuration
├── iam.yaml # IAM roles and policies
├── deploy.sh # Namespaced deployment script
└── teardown.sh # Cleanup script for namespaced deployments
Key environment variables in .env (see .env.example for all options):
N8N_ENCRYPTION_KEY: Auto-generated encryption key for credentialsDB_TYPE: Database type (sqliteorpostgresdb)DB_POSTGRESDB_*: PostgreSQL connection settingsGENERIC_TIMEZONE: Timezone setting (default: UTC)
For a complete list of configuration options, check .env.example which includes:
- External database connections (RDS)
- Basic authentication
- Email/SMTP configuration
- Webhook settings
- S3 file storage
- Execution timeouts
SSM (Systems Manager) provides several advantages over traditional SSH access:
- No SSH keys to manage - Access controlled via AWS IAM
- No public IPs needed - Instances stay in private subnets
- Session auditing - All access logged via AWS CloudTrail
- Easy automation - Simple
aws ssm start-sessioncommands
| Feature | Description |
|---|---|
| 🔐 No Public IPs | Instances deployed in private subnets only |
| 🔑 IAM-Based Access | All access controlled via instance profiles |
| 📦 Encrypted Secrets | Auto-generated encryption keys and passwords |
| 🛡️ SSM Access Only | No SSH keys, auditable access via SSM |
| 🔒 Outbound Only | Security group blocks all inbound traffic |
| 🎲 Random Passwords | Auto-generated PostgreSQL and encryption keys |
This deployment uses ARM64-based t4g instances for cost efficiency. All Docker images in the stack support ARM64 architecture.
SQLite:
# Backup
cp -r n8n_data n8n_data_backup
# Restore
cp -r n8n_data_backup n8n_dataPostgreSQL:
# Manual backup
docker compose exec postgres pg_dump -U n8n n8n > backup.sql
# Manual restore
docker compose exec -T postgres psql -U n8n n8n < backup.sqlAutomated Backup Strategy:
The deployment now includes a comprehensive automated backup system:
- Daily backups at 2 AM automatically
- 30-day retention policy with automatic cleanup
- Backup monitoring with CloudWatch metrics
- Easy restore with built-in utilities
- Backup health checks every 6 hours
Once deployed, you can manage backups using these commands:
# SSH into your instance
aws ssm start-session --target <instance-id>
# List all available backups
n8n-backup list
# Show latest backup information
n8n-backup latest
# Create a manual backup immediately
n8n-backup backup-now
# Check backup system status
n8n-backup status
# Test a backup file (without actually restoring)
n8n-backup test-restore n8n_backup_acme-corp_20240101_020000.sql.gz
# View backup logs
n8n-backup logs# List available backups and select one to restore
n8n-restore
# Restore a specific backup
n8n-restore n8n_backup_acme-corp_20240101_020000.sql.gzThe system automatically:
- Creates CloudWatch metrics for backup success/failure
- Monitors backup age and alerts if backups are missing
- Logs all backup operations to
/var/log/n8n-backup.log - Tracks backup count and storage usage
Backups are stored in your namespace's S3 bucket under:
s3://n8n-files-{namespace}-{account}-{region}/backups/{namespace}/
├── n8n_backup_{namespace}_20240101_020000.sql.gz
├── n8n_backup_{namespace}_20240102_020000.sql.gz
└── n8n_backup_{namespace}_20240103_020000.sql.gz
For additional disaster recovery, you can also create EBS snapshots:
# Create a snapshot via AWS CLI (run from your local machine)
INSTANCE_ID="i-1234567890abcdef0" # Your instance ID
VOLUME_ID=$(aws ec2 describe-instances --instance-ids $INSTANCE_ID \
--query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId' --output text)
aws ec2 create-snapshot --volume-id $VOLUME_ID \
--description "n8n Weekly Backup $(date +%Y-%m-%d)" \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=n8n-backup},{Key=Namespace,Value=your-namespace}]'- Port already in use: Change the port mapping in
docker-compose.yml - Permission denied: Ensure proper permissions on
n8n_dataandlocal-filesdirectories - Database connection failed: Check PostgreSQL container health and credentials in
.env - Encryption key mismatch: If you see encryption key errors after changing
.env:This removes the old config but preserves your workflows./install-n8n.sh --clean
- The host should be
postgres(the Docker service name), notlocalhost - The database name is
n8nby default - Connection happens over Docker's internal network
- To connect from your host machine for debugging:
docker compose exec postgres psql -U n8n -d n8n
To connect from local database tools:
- Host: localhost
- Port: 5432 (see note below if you have conflicts)
- Database: n8n
- Username: n8n
- Password: (from your .env file)
Port Conflict Note: If you already have PostgreSQL running locally on port 5432, you'll need to change the port mapping in
docker-compose.ymlfrom"5432:5432"to something like"5433:5432", then connect to port 5433 instead.
View logs:
# All services
docker compose logs -f
# Specific service
docker compose logs -f n8n
docker compose logs -f postgresQ: Can I expose n8n to the public internet? A: Yes, but you'll need to alter the architecture slightly. One strategy is to add an Application Load Balancer, use Route53 and ACM for DNS and SSL certificate management. For enterprise deployments, included a layer 7 web application firewall would be a good idea. Another strategy is to use Cloudflare which is a cheaper option.
The current setup prioritizes security with private access only - exposing n8n across the public internet comes with some security risks that you should be wary of.
Q: How do I update n8n to the latest version?
A: For local: docker compose pull && docker compose down && docker compose up -d. For AWS: redeploy the stack or SSH in and run the same commands.
Q: Can I use RDS instead of local PostgreSQL?
A: Yes! Set the DB_POSTGRESDB_HOST to your RDS endpoint in the .env file and remove the postgres service from docker-compose.yml.
Q: What if I already have PostgreSQL running on port 5432?
A: Change the port mapping in docker-compose.yml from "5432:5432" to something like "5433:5432".
Q: How do I backup my data? A: See the Backup and Restore section above for detailed instructions.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- 📦 Replace local Postgres with external RDS or Aurora
- 🔁 Use GitHub Actions + SAM CLI for CI/CD deploys
- 📊 Send logs to CloudWatch or Loki
- 🤖 Integrate Ollama or Bedrock into workflows
- 🔐 Add secrets management via AWS Parameter Store
- 🌐 Put behind Application Load Balancer for team access
- 📈 Add CloudWatch dashboards for monitoring
- 🔄 Implement automated backups to S3