Skip to content

Instantly share code, notes, and snippets.

@rhcarlosweb
Last active February 27, 2025 03:12
Show Gist options
  • Save rhcarlosweb/0bda9a02aaf0746d94a38cbb07a0adc3 to your computer and use it in GitHub Desktop.
Save rhcarlosweb/0bda9a02aaf0746d94a38cbb07a0adc3 to your computer and use it in GitHub Desktop.
#!/bin/bash
# n8n Docker Installation Script with Traefik, Redis, PostgreSQL, and Queue Mode
# This script sets up n8n with Docker Compose including:
# - Traefik as reverse proxy with Let's Encrypt SSL
# - Redis for caching
# - PostgreSQL for database
# - n8n in queue mode with 2 workers
# Force execution with bash
if [ -z "$BASH_VERSION" ]; then
echo "This script must be run with bash. Trying to switch to bash automatically..."
exec /bin/bash "$0" "$@"
# If exec fails, the script will continue but might fail later
echo "Failed to switch to bash. Please run the script with bash explicitly: bash $0"
fi
set -e
# Text formatting
BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
NC="\033[0m" # No Color
# Function to print colored messages
print_message() {
echo -e "${2}${1}${NC}"
}
# Function to check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if running as root
if [ "$(id -u)" -ne 0 ]; then
print_message "This script must be run as root" "$RED"
exit 1
fi
# Check system requirements
print_message "Checking system requirements..." "$BOLD"
# Create installation directory
INSTALL_DIR="/opt/n8n"
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
# Install dependencies if not already installed
print_message "Installing dependencies..." "$BOLD"
apt-get update
apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release \
apache2-utils \
pwgen
# Install Docker if not already installed
if ! command_exists docker; then
print_message "Installing Docker..." "$BOLD"
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
rm get-docker.sh
else
print_message "Docker already installed. Skipping..." "$GREEN"
fi
# Install Docker Compose if not already installed
if ! command_exists docker-compose; then
print_message "Installing Docker Compose..." "$BOLD"
LATEST_COMPOSE=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep -o -P '(?<="tag_name": ").+(?=")')
curl -L "https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
else
print_message "Docker Compose already installed. Skipping..." "$GREEN"
fi
# Interactive configuration
print_message "\nStarting interactive configuration...\n" "$BOLD"
# Domain configuration
while true; do
read -p "Enter domain name (e.g., example.com): " DOMAIN_NAME
if [ ! -z "$DOMAIN_NAME" ]; then
break
else
print_message "Domain name cannot be empty!" "$RED"
fi
done
# Subdomain configuration
read -p "Enter subdomain for n8n [n8n]: " SUBDOMAIN
SUBDOMAIN=${SUBDOMAIN:-n8n}
# Full domain
FULL_DOMAIN="${SUBDOMAIN}.${DOMAIN_NAME}"
# Frontend URL configuration (Vite)
read -p "Enter frontend URL for n8n [https://${FULL_DOMAIN}]: " FRONTEND_URL
FRONTEND_URL=${FRONTEND_URL:-https://${FULL_DOMAIN}}
# Email for Let's Encrypt
while true; do
read -p "Enter email address for Let's Encrypt notifications: " EMAIL
if [ ! -z "$EMAIL" ]; then
break
else
print_message "Email address cannot be empty!" "$RED"
fi
done
# Timezone configuration
read -p "Enter timezone [Europe/Berlin]: " TIMEZONE
TIMEZONE=${TIMEZONE:-Europe/Berlin}
# Local file access
read -p "Do you want to set up local file access for n8n? (y/n) [n]: " SETUP_LOCAL_FILES
SETUP_LOCAL_FILES=${SETUP_LOCAL_FILES:-n}
if [[ "$SETUP_LOCAL_FILES" == "y" || "$SETUP_LOCAL_FILES" == "Y" ]]; then
read -p "Enter local path for file access [/local-files]: " LOCAL_FILES_PATH
LOCAL_FILES_PATH=${LOCAL_FILES_PATH:-/local-files}
mkdir -p $LOCAL_FILES_PATH
chmod 777 $LOCAL_FILES_PATH
fi
# PostgreSQL configuration
read -p "Enter PostgreSQL username [n8nuser]: " PG_USER
PG_USER=${PG_USER:-n8nuser}
# Generate random PostgreSQL password if not provided
PG_PASS=$(pwgen -s 16 1)
read -p "Enter PostgreSQL password [generated: $PG_PASS]: " USER_PG_PASS
PG_PASS=${USER_PG_PASS:-$PG_PASS}
read -p "Enter PostgreSQL database name [n8n]: " PG_DB
PG_DB=${PG_DB:-n8n}
# n8n configuration
read -p "Enter n8n username for initial login: " N8N_USER
# Generate random n8n password if not provided
N8N_PASS=$(pwgen -s 16 1)
read -p "Enter n8n password [generated: $N8N_PASS]: " USER_N8N_PASS
N8N_PASS=${USER_N8N_PASS:-$N8N_PASS}
# Generate encryption key for n8n
ENCRYPTION_KEY=$(pwgen -s 32 1)
# Number of workers
read -p "Enter number of n8n workers [2]: " N8N_WORKERS
N8N_WORKERS=${N8N_WORKERS:-2}
# Create Docker volumes
print_message "Creating Docker volumes..." "$BOLD"
docker volume create traefik_data
docker volume create n8n_data
docker volume create postgres_data
docker volume create redis_data
# Create docker-compose.yml
print_message "Creating Docker Compose configuration..." "$BOLD"
cat > docker-compose.yml << EOL
version: '3.8'
networks:
traefik-public:
external: false
internal:
external: false
services:
traefik:
image: traefik:v2.10
container_name: traefik
restart: always
command:
- "--api=true"
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=${EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- traefik_data:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- traefik-public
postgres:
image: postgres:14-alpine
container_name: postgres
restart: always
environment:
- POSTGRES_USER=${PG_USER}
- POSTGRES_PASSWORD=${PG_PASS}
- POSTGRES_DB=${PG_DB}
- POSTGRES_NON_ROOT_USER=${PG_USER}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- internal
labels:
- "traefik.enable=false"
redis:
image: redis:7-alpine
container_name: redis
restart: always
command: redis-server --requirepass ${PG_PASS}
volumes:
- redis_data:/data
networks:
- internal
labels:
- "traefik.enable=false"
n8n:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n
restart: always
environment:
- N8N_HOST=${FULL_DOMAIN}
- N8N_PROTOCOL=https
- N8N_PORT=5678
- N8N_EDITOR_BASE_URL=${FRONTEND_URL}
- N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
- NODE_ENV=production
- WEBHOOK_URL=https://${FULL_DOMAIN}/
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${PG_DB}
- DB_POSTGRESDB_USER=${PG_USER}
- DB_POSTGRESDB_PASSWORD=${PG_PASS}
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${PG_PASS}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_DB=0
- N8N_METRICS=true
- N8N_USER_MANAGEMENT_DISABLED=false
- N8N_DIAGNOSTICS_ENABLED=false
- N8N_DEFAULT_USER_NAME=${N8N_USER}
- N8N_DEFAULT_USER_PASSWORD=${N8N_PASS}
- N8N_HIRING_BANNER_ENABLED=false
- GENERIC_TIMEZONE=${TIMEZONE}
volumes:
- n8n_data:/home/node/.n8n
EOL
# Add local file access if requested
if [[ "$SETUP_LOCAL_FILES" == "y" || "$SETUP_LOCAL_FILES" == "Y" ]]; then
cat >> docker-compose.yml << EOL
- ${LOCAL_FILES_PATH}:/files
EOL
fi
# Continue the n8n configuration
cat >> docker-compose.yml << EOL
networks:
- internal
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.n8n.rule=Host(\`${FULL_DOMAIN}\`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls=true"
- "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
- "traefik.http.middlewares.n8n-headers.headers.SSLRedirect=true"
- "traefik.http.middlewares.n8n-headers.headers.STSSeconds=315360000"
- "traefik.http.middlewares.n8n-headers.headers.browserXSSFilter=true"
- "traefik.http.middlewares.n8n-headers.headers.contentTypeNosniff=true"
- "traefik.http.middlewares.n8n-headers.headers.forceSTSHeader=true"
- "traefik.http.middlewares.n8n-headers.headers.SSLHost=${DOMAIN_NAME}"
- "traefik.http.middlewares.n8n-headers.headers.STSIncludeSubdomains=true"
- "traefik.http.middlewares.n8n-headers.headers.STSPreload=true"
- "traefik.http.routers.n8n.middlewares=n8n-headers"
depends_on:
- postgres
- redis
EOL
# Add worker nodes to docker-compose.yml
for i in $(seq 1 $N8N_WORKERS); do
cat >> docker-compose.yml << EOL
n8n-worker-${i}:
image: docker.n8n.io/n8nio/n8n:latest
container_name: n8n-worker-${i}
restart: always
environment:
- N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
- N8N_EDITOR_BASE_URL=${FRONTEND_URL}
- NODE_ENV=production
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${PG_DB}
- DB_POSTGRESDB_USER=${PG_USER}
- DB_POSTGRESDB_PASSWORD=${PG_PASS}
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${PG_PASS}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_DB=0
- N8N_METRICS=false
- WORKERS_DISABLED=false
- WEBHOOK_TUNNEL_URL=https://${FULL_DOMAIN}/
- GENERIC_TIMEZONE=${TIMEZONE}
command: worker
volumes:
- n8n_data:/home/node/.n8n
EOL
# Add local file access if requested
if [[ "$SETUP_LOCAL_FILES" == "y" || "$SETUP_LOCAL_FILES" == "Y" ]]; then
cat >> docker-compose.yml << EOL
- ${LOCAL_FILES_PATH}:/files
EOL
fi
# Continue the worker configuration
cat >> docker-compose.yml << EOL
networks:
- internal
labels:
- "traefik.enable=false"
depends_on:
- postgres
- redis
- n8n
EOL
done
# Add volumes to docker-compose.yml
cat >> docker-compose.yml << EOL
volumes:
traefik_data:
external: true
n8n_data:
external: true
postgres_data:
external: true
redis_data:
external: true
EOL
# Create a script to start the services
cat > start.sh << EOL
#!/bin/bash
docker-compose up -d
echo "n8n has been started!"
EOL
chmod +x start.sh
# Create a script to stop the services
cat > stop.sh << EOL
#!/bin/bash
docker-compose down
echo "n8n has been stopped!"
EOL
chmod +x stop.sh
# Create a script to restart the services
cat > restart.sh << EOL
#!/bin/bash
docker-compose down
docker-compose up -d
echo "n8n has been restarted!"
EOL
chmod +x restart.sh
# Create a script to check the logs
cat > logs.sh << EOL
#!/bin/bash
docker-compose logs -f \$1
EOL
chmod +x logs.sh
# Create a backup script
cat > backup.sh << EOL
#!/bin/bash
# Create timestamp for the backup
TIMESTAMP=\$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/n8n/backups/\$TIMESTAMP"
# Create backup directory
mkdir -p \$BACKUP_DIR
# Stop n8n if running
docker-compose down
# Backup PostgreSQL database
docker run --rm -v postgres_data:/var/lib/postgresql/data -v \$BACKUP_DIR:/backup postgres:14-alpine sh -c "pg_dump -U ${PG_USER} -d ${PG_DB} -f /backup/n8n_db.sql"
# Backup n8n data
docker run --rm -v n8n_data:/source -v \$BACKUP_DIR:/backup alpine sh -c "cp -r /source /backup/n8n_data"
# Restart n8n
docker-compose up -d
echo "Backup completed at \$BACKUP_DIR"
EOL
chmod +x backup.sh
# Create a configuration info file
cat > config-info.txt << EOL
N8N INSTALLATION INFORMATION
===========================
Domain: ${DOMAIN_NAME}
Subdomain: ${SUBDOMAIN}
Full Domain: ${FULL_DOMAIN}
Frontend URL (Vite): ${FRONTEND_URL}
Email for Let's Encrypt: ${EMAIL}
Timezone: ${TIMEZONE}
DATABASE INFORMATION
-------------------
PostgreSQL User: ${PG_USER}
PostgreSQL Password: ${PG_PASS}
PostgreSQL Database: ${PG_DB}
N8N LOGIN INFORMATION
-------------------
Username: ${N8N_USER}
Password: ${N8N_PASS}
MANAGEMENT SCRIPTS
----------------
./start.sh - Start all services
./stop.sh - Stop all services
./restart.sh - Restart all services
./logs.sh - View logs (use ./logs.sh n8n for n8n logs)
./backup.sh - Create a backup of the database and n8n data
N8N URL: ${FRONTEND_URL}
EOL
# Create .env file for reference
cat > .env << EOL
# The top level domain
DOMAIN_NAME=${DOMAIN_NAME}
# The subdomain for n8n
SUBDOMAIN=${SUBDOMAIN}
# Full domain for n8n
FULL_DOMAIN=${FULL_DOMAIN}
# Frontend URL for n8n (Vite)
FRONTEND_URL=${FRONTEND_URL}
# Timezone for n8n
GENERIC_TIMEZONE=${TIMEZONE}
# Email for SSL certificate
SSL_EMAIL=${EMAIL}
# Database configuration
PG_USER=${PG_USER}
PG_PASS=${PG_PASS}
PG_DB=${PG_DB}
# n8n user credentials
N8N_USER=${N8N_USER}
N8N_PASS=${N8N_PASS}
# n8n encryption key
ENCRYPTION_KEY=${ENCRYPTION_KEY}
# Number of workers
N8N_WORKERS=${N8N_WORKERS}
EOL
print_message "\nStarting services...\n" "$BOLD"
# Create backups directory
mkdir -p backups
# Start the services
./start.sh
print_message "\nInstallation completed successfully!" "$GREEN"
print_message "Your n8n instance will be available at: ${FRONTEND_URL}" "$GREEN"
print_message "Initial login credentials:" "$GREEN"
print_message " Username: ${N8N_USER}" "$GREEN"
print_message " Password: ${N8N_PASS}" "$GREEN"
print_message "\nConfiguration details have been saved to: ${INSTALL_DIR}/config-info.txt" "$YELLOW"
print_message "\nIt might take a few minutes for Let's Encrypt to issue the SSL certificate." "$YELLOW"
print_message "You can check the status with: ./logs.sh traefik" "$YELLOW"
# Final instructions
print_message "\nIMPORTANT: Make sure your domain (${FULL_DOMAIN}) is pointing to this server's IP address." "$RED"
print_message "Also ensure that ports 80 and 443 are open in your firewall." "$RED"
print_message "\nYour n8n instance has authentication enabled. Please keep your credentials safe." "$RED"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment