mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-18 14:15:09 +00:00
Add production environment configuration and deployment scripts for ERPNext
- Created example environment files for MariaDB and production settings. - Implemented backup script for ERPNext sites with options for file inclusion, compression, and encryption. - Developed site creation script to streamline ERPNext site setup with admin password handling. - Added deployment script to manage the deployment of ERPNext, MariaDB, and Traefik services. - Introduced log viewing script for monitoring ERPNext services. - Implemented stop script to manage stopping of ERPNext and its dependencies. - Added validation script to check environment configuration for common issues and security best practices.
This commit is contained in:
parent
a3fad9b9c7
commit
f167e83bda
9 changed files with 2605 additions and 0 deletions
1637
production/README.md
Normal file
1637
production/README.md
Normal file
File diff suppressed because it is too large
Load diff
16
production/mariadb.env.example
Normal file
16
production/mariadb.env.example
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# MariaDB Configuration for ERPNext Production
|
||||
# This database will be shared across all ERPNext benches on this server
|
||||
|
||||
# ============================================
|
||||
# Database Password
|
||||
# ============================================
|
||||
# IMPORTANT: Change this to a strong password
|
||||
# This password will be used for the MariaDB root user
|
||||
DB_PASSWORD=CHANGEME_strong_password_must_match_production_env
|
||||
|
||||
# SECURITY NOTE:
|
||||
# - Use a strong password with at least 16 characters
|
||||
# - Include uppercase, lowercase, numbers, and special characters
|
||||
# - Do not use common words or patterns
|
||||
# - Store this password securely (use a password manager)
|
||||
# - This same password must be set in production.env
|
||||
94
production/production.env.example
Normal file
94
production/production.env.example
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Production Environment Configuration for ERPNext
|
||||
# Reference: https://github.com/frappe/frappe_docker/blob/main/docs/environment-variables.md
|
||||
|
||||
# ============================================
|
||||
# ERPNext Version
|
||||
# ============================================
|
||||
ERPNEXT_VERSION=v15.82.1
|
||||
|
||||
# ============================================
|
||||
# Database Configuration
|
||||
# ============================================
|
||||
# IMPORTANT: Change this to a strong password
|
||||
DB_PASSWORD=CHANGEME_strong_password_16plus_chars
|
||||
DB_HOST=mariadb-database
|
||||
DB_PORT=3306
|
||||
|
||||
# Only if you use docker secrets for the db password
|
||||
# DB_PASSWORD_SECRETS_FILE=
|
||||
|
||||
# ============================================
|
||||
# Redis Configuration
|
||||
# ============================================
|
||||
# These will be set by the compose file overrides
|
||||
# REDIS_CACHE=redis-cache:6379
|
||||
# REDIS_QUEUE=redis-queue:6379
|
||||
|
||||
# ============================================
|
||||
# SSL/TLS Configuration
|
||||
# ============================================
|
||||
# IMPORTANT: Change this to your email for Let's Encrypt notifications
|
||||
LETSENCRYPT_EMAIL=CHANGEME_admin@yourdomain.com
|
||||
|
||||
# IMPORTANT: Change this to your actual domain(s)
|
||||
# Multiple sites should be separated by comma
|
||||
# Example: SITES=`erp.example.com`,`crm.example.com`
|
||||
SITES=CHANGEME_`erp.yourdomain.com`
|
||||
|
||||
# ============================================
|
||||
# Network Configuration
|
||||
# ============================================
|
||||
# Used for multi-bench setups
|
||||
ROUTER=erpnext-production
|
||||
BENCH_NETWORK=erpnext-production
|
||||
|
||||
# ============================================
|
||||
# Site Resolution
|
||||
# ============================================
|
||||
# Default value is `$$host` which resolves site by host.
|
||||
# For example, if your host is `example.com`, site's name should be `example.com`
|
||||
# Leave empty to use default behavior
|
||||
FRAPPE_SITE_NAME_HEADER=
|
||||
|
||||
# ============================================
|
||||
# Port Configuration
|
||||
# ============================================
|
||||
# Default value is `8080` - usually not needed in production with Traefik
|
||||
# HTTP_PUBLISH_PORT=8080
|
||||
|
||||
# ============================================
|
||||
# Nginx/Proxy Configuration
|
||||
# ============================================
|
||||
# Set IP address as trusted upstream address
|
||||
# Default: 127.0.0.1
|
||||
UPSTREAM_REAL_IP_ADDRESS=127.0.0.1
|
||||
|
||||
# Set request header field for client IP
|
||||
# Default: X-Forwarded-For
|
||||
UPSTREAM_REAL_IP_HEADER=X-Forwarded-For
|
||||
|
||||
# Enable/disable recursive search for real IP
|
||||
# Allowed values: on|off
|
||||
# Default: off
|
||||
UPSTREAM_REAL_IP_RECURSIVE=off
|
||||
|
||||
# Proxy read timeout for long-running requests
|
||||
# Default: 120s
|
||||
PROXY_READ_TIMEOUT=120s
|
||||
|
||||
# Maximum upload file size
|
||||
# Default: 50m
|
||||
CLIENT_MAX_BODY_SIZE=50m
|
||||
|
||||
# ============================================
|
||||
# Custom Image (Optional)
|
||||
# ============================================
|
||||
# If you're using custom images with additional apps
|
||||
# CUSTOM_IMAGE=frappe/erpnext
|
||||
# CUSTOM_TAG=v15.82.1
|
||||
# PULL_POLICY=always
|
||||
|
||||
# ============================================
|
||||
# Restart Policy
|
||||
# ============================================
|
||||
RESTART_POLICY=unless-stopped
|
||||
225
production/scripts/backup-site.sh
Normal file
225
production/scripts/backup-site.sh
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Backup ERPNext Site Script
|
||||
# Usage: ./backup-site.sh <site-name> [options]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||||
|
||||
# Helper functions
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
echo_debug() { [[ "${DEBUG:-}" == "1" ]] && echo -e "${BLUE}[DEBUG]${NC} $1" || true; }
|
||||
log_action() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "/tmp/erpnext-backup-$(date '+%Y%m%d').log"; }
|
||||
|
||||
# Cleanup on error
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
[[ $exit_code -ne 0 ]] && echo_error "Script failed with exit code $exit_code" && log_action "FAILED: ${SITE_NAME:-unknown}"
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Configuration
|
||||
PROJECT_NAME="${PROJECT_NAME:-erpnext-production}"
|
||||
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PRODUCTION_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Docker helpers
|
||||
dc_exec() { docker compose --project-name "$PROJECT_NAME" exec backend "$@"; }
|
||||
dc_cmd() { docker compose --project-name "$PROJECT_NAME" "$@"; }
|
||||
|
||||
cd "$PRODUCTION_DIR"
|
||||
|
||||
# Help function
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Usage: $0 <site-name> [options]
|
||||
|
||||
Options:
|
||||
--with-files Include files in backup
|
||||
--compress Compress the backup
|
||||
--auto-copy Copy backups to host ./backups/
|
||||
--cleanup-old Remove backups older than BACKUP_RETENTION_DAYS
|
||||
--encrypt Encrypt with GPG (requires BACKUP_PASSPHRASE env var)
|
||||
--debug Enable debug output
|
||||
-h, --help Show this help
|
||||
|
||||
Environment Variables:
|
||||
PROJECT_NAME Docker project name (default: erpnext-production)
|
||||
BACKUP_RETENTION_DAYS Keep backups for N days (default: 30)
|
||||
BACKUP_PASSPHRASE GPG encryption passphrase
|
||||
AUTO_COPY Auto-copy to host (set to 1)
|
||||
CLEANUP_OLD Auto-cleanup old backups (set to 1)
|
||||
|
||||
Examples:
|
||||
$0 erp.example.com --with-files --auto-copy
|
||||
BACKUP_PASSPHRASE='secret' $0 erp.example.com --encrypt --auto-copy
|
||||
|
||||
Decryption:
|
||||
gpg --decrypt backup-file.gpg > backup-file
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
SITE_NAME="" WITH_FILES="" COMPRESS=""
|
||||
AUTO_COPY="${AUTO_COPY:-0}" CLEANUP_OLD="${CLEANUP_OLD:-0}" ENCRYPT="${ENCRYPT:-0}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help) show_help; exit 0 ;;
|
||||
--with-files) WITH_FILES="--with-files"; shift ;;
|
||||
--compress) COMPRESS="--compress"; shift ;;
|
||||
--auto-copy) AUTO_COPY=1; shift ;;
|
||||
--cleanup-old) CLEANUP_OLD=1; shift ;;
|
||||
--encrypt) ENCRYPT=1; shift ;;
|
||||
--debug) DEBUG=1; shift ;;
|
||||
*) [[ -z "$SITE_NAME" ]] && SITE_NAME="$1" || { echo_error "Unknown: $1"; show_help; exit 1; }; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Get site name if not provided
|
||||
if [[ -z "$SITE_NAME" ]]; then
|
||||
echo_warn "Site name required"
|
||||
read -p "Enter site name (e.g., erp.example.com): " SITE_NAME
|
||||
fi
|
||||
|
||||
[[ -z "$SITE_NAME" ]] && { echo_error "Site name cannot be empty"; exit 1; }
|
||||
|
||||
# Validate site name format
|
||||
if [[ ! "$SITE_NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$ ]]; then
|
||||
echo_warn "Site name format looks unusual: $SITE_NAME"
|
||||
read -p "Continue? (y/N): " -n 1 -r; echo
|
||||
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
|
||||
fi
|
||||
|
||||
echo_debug "Site: $SITE_NAME, Project: $PROJECT_NAME"
|
||||
|
||||
# Check encryption requirements
|
||||
if [[ "$ENCRYPT" == "1" ]]; then
|
||||
[[ -z "${BACKUP_PASSPHRASE:-}" ]] && { echo_error "BACKUP_PASSPHRASE not set"; exit 1; }
|
||||
command -v gpg >/dev/null 2>&1 || { echo_error "GPG not installed"; exit 1; }
|
||||
echo_debug "Encryption ready"
|
||||
fi
|
||||
|
||||
# Validate environment
|
||||
[[ ! -f "production.yaml" ]] && { echo_error "production.yaml not found"; exit 1; }
|
||||
docker info >/dev/null 2>&1 || { echo_error "Docker not running"; exit 1; }
|
||||
dc_exec echo "test" >/dev/null 2>&1 || { echo_error "Backend container not running"; exit 1; }
|
||||
|
||||
# Verify site exists
|
||||
echo_info "Verifying site: $SITE_NAME"
|
||||
if ! dc_exec bench --site "$SITE_NAME" list-apps >/dev/null 2>&1; then
|
||||
echo_error "Site '$SITE_NAME' not found"
|
||||
echo_info "Available sites:"
|
||||
dc_exec find /home/frappe/frappe-bench/sites -maxdepth 1 -type d \( -name "*.local" -o -name "*.*" \) | \
|
||||
sed 's|.*/||' | grep -v "^$" || echo " None found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create backup
|
||||
echo_info "Creating backup for: $SITE_NAME"
|
||||
log_action "STARTED: $SITE_NAME"
|
||||
BACKUP_TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
|
||||
|
||||
if ! dc_exec bench --site "$SITE_NAME" backup $WITH_FILES $COMPRESS; then
|
||||
echo_error "Backup command failed!"
|
||||
log_action "FAILED: Backup command"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify backup files
|
||||
BACKUP_PATH="/home/frappe/frappe-bench/sites/$SITE_NAME/private/backups"
|
||||
BACKUP_FILES=$(dc_exec find "$BACKUP_PATH" -type f -mmin -2 2>/dev/null | tr -d '\r' || echo "")
|
||||
|
||||
if [[ -z "$BACKUP_FILES" ]]; then
|
||||
echo_error "No backup files found!"
|
||||
log_action "FAILED: No files"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display backup info
|
||||
BACKUP_COUNT=$(echo "$BACKUP_FILES" | wc -l)
|
||||
echo_info "✓ Created $BACKUP_COUNT file(s)"
|
||||
echo_info "Files:"
|
||||
|
||||
TOTAL_SIZE=0
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
SIZE_BYTES=$(dc_exec stat -c%s "$file" 2>/dev/null | tr -d '\r' || echo "0")
|
||||
SIZE_HR=$(numfmt --to=iec-i --suffix=B "$SIZE_BYTES" 2>/dev/null || echo "$SIZE_BYTES bytes")
|
||||
echo_info " - $(basename "$file") ($SIZE_HR)"
|
||||
TOTAL_SIZE=$((TOTAL_SIZE + SIZE_BYTES))
|
||||
done <<< "$BACKUP_FILES"
|
||||
|
||||
[[ $TOTAL_SIZE -gt 0 ]] && echo_info "Total size: $(numfmt --to=iec-i --suffix=B "$TOTAL_SIZE" 2>/dev/null || echo "$TOTAL_SIZE bytes")"
|
||||
|
||||
log_action "SUCCESS: $BACKUP_COUNT files, $TOTAL_SIZE bytes"
|
||||
|
||||
# Auto-copy to host
|
||||
if [[ "$AUTO_COPY" == "1" ]]; then
|
||||
echo_info "Copying to host ./backups/"
|
||||
mkdir -p ./backups
|
||||
|
||||
BACKEND_CONTAINER=$(dc_cmd ps -q backend)
|
||||
if docker cp "${BACKEND_CONTAINER}:${BACKUP_PATH}/." ./backups/ 2>/dev/null; then
|
||||
echo_info "✓ Copied to ./backups/"
|
||||
|
||||
# Encrypt if requested
|
||||
if [[ "$ENCRYPT" == "1" ]]; then
|
||||
echo_info "Encrypting backups..."
|
||||
TIMESTAMP_PREFIX="${BACKUP_TIMESTAMP:0:12}"
|
||||
RECENT_BACKUPS=$(find ./backups -type f -mmin -1 -name "${TIMESTAMP_PREFIX}*" ! -name "*.gpg" 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$RECENT_BACKUPS" ]]; then
|
||||
ENCRYPTED_COUNT=0
|
||||
while IFS= read -r backup_file; do
|
||||
[[ ! -f "$backup_file" ]] && continue
|
||||
|
||||
if echo "$BACKUP_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--symmetric --cipher-algo AES256 -o "${backup_file}.gpg" "$backup_file" 2>/dev/null; then
|
||||
|
||||
[[ -f "${backup_file}.gpg" ]] && rm -f "$backup_file" && ((ENCRYPTED_COUNT++))
|
||||
echo_info " ✓ Encrypted: $(basename "${backup_file}.gpg")"
|
||||
else
|
||||
echo_warn " ✗ Encryption failed: $(basename "$backup_file")"
|
||||
fi
|
||||
done <<< "$RECENT_BACKUPS"
|
||||
|
||||
echo_info "✓ Encrypted $ENCRYPTED_COUNT file(s)"
|
||||
log_action "SUCCESS: Encrypted $ENCRYPTED_COUNT files"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show latest files
|
||||
echo_info "Latest in ./backups/:"
|
||||
ls -lht ./backups/ | head -n 6 || true
|
||||
else
|
||||
echo_warn "Copy failed. Files remain in container."
|
||||
fi
|
||||
else
|
||||
echo_info "Backup location: ${BACKUP_PATH}/"
|
||||
echo_info "To copy: docker cp \$(docker compose -p $PROJECT_NAME ps -q backend):${BACKUP_PATH}/. ./backups/"
|
||||
fi
|
||||
|
||||
# Cleanup old backups
|
||||
if [[ "$CLEANUP_OLD" == "1" ]] && [[ "$BACKUP_RETENTION_DAYS" -gt 0 ]]; then
|
||||
echo_info "Cleaning backups older than $BACKUP_RETENTION_DAYS days"
|
||||
|
||||
# Container cleanup
|
||||
dc_exec find "$BACKUP_PATH" -type f -mtime +$BACKUP_RETENTION_DAYS -delete 2>/dev/null && \
|
||||
echo_info "✓ Container cleanup done" || echo_warn "Container cleanup failed"
|
||||
|
||||
# Host cleanup
|
||||
if [[ -d "./backups" ]]; then
|
||||
DELETED=$(find "./backups" -type f -mtime +$BACKUP_RETENTION_DAYS 2>/dev/null | wc -l)
|
||||
[[ "$DELETED" -gt 0 ]] && find "./backups" -type f -mtime +$BACKUP_RETENTION_DAYS -delete 2>/dev/null
|
||||
echo_info "✓ Host cleanup: removed $DELETED file(s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo_info "✓ Backup completed successfully!"
|
||||
96
production/scripts/create-site.sh
Normal file
96
production/scripts/create-site.sh
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Create ERPNext Site Script
|
||||
# Usage: ./create-site.sh <site-name> [admin-password]
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
|
||||
# Helper functions
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Navigate to production directory
|
||||
cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" || exit 1
|
||||
|
||||
PROJECT_NAME="erpnext-production"
|
||||
|
||||
# Get site name
|
||||
if [[ -z "$1" ]]; then
|
||||
echo_warn "Usage: $0 <site-name> [admin-password]"
|
||||
read -p "Enter site name (e.g., erp.example.com): " SITE_NAME
|
||||
elif [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
cat << EOF
|
||||
Usage: $0 <site-name> [admin-password]
|
||||
|
||||
Arguments:
|
||||
site-name Site domain (e.g., erp.example.com)
|
||||
admin-password Optional admin password (default: 'admin')
|
||||
|
||||
Examples:
|
||||
$0 erp.example.com
|
||||
$0 erp.example.com MySecurePass123
|
||||
|
||||
Notes:
|
||||
- Requires backend container to be running
|
||||
- DNS should point to your server IP
|
||||
- Change admin password after first login
|
||||
- SSL certificate may take a few minutes
|
||||
EOF
|
||||
exit 0
|
||||
else
|
||||
SITE_NAME=$1
|
||||
fi
|
||||
|
||||
[[ -z "$SITE_NAME" ]] && { echo_error "Site name cannot be empty"; exit 1; }
|
||||
|
||||
# Get admin password
|
||||
if [[ -z "$2" ]]; then
|
||||
read -sp "Enter admin password (Enter for 'admin'): " ADMIN_PASSWORD
|
||||
echo
|
||||
if [[ -z "$ADMIN_PASSWORD" ]]; then
|
||||
ADMIN_PASSWORD="admin"
|
||||
echo_warn "Using default password 'admin' - Change after login!"
|
||||
fi
|
||||
else
|
||||
ADMIN_PASSWORD=$2
|
||||
fi
|
||||
|
||||
# Get DB password from mariadb.env
|
||||
[[ ! -f "mariadb.env" ]] && { echo_error "mariadb.env not found!"; exit 1; }
|
||||
DB_ROOT_PASSWORD=$(grep "^DB_PASSWORD=" mariadb.env | cut -d'=' -f2)
|
||||
[[ -z "$DB_ROOT_PASSWORD" ]] && { echo_error "DB_PASSWORD not found in mariadb.env"; exit 1; }
|
||||
|
||||
# Check if backend is running
|
||||
docker ps | grep -q "$PROJECT_NAME-backend" || {
|
||||
echo_error "Backend not running! Run: ./scripts/deploy.sh"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create the site
|
||||
echo_info "Creating site: $SITE_NAME"
|
||||
docker compose --project-name "$PROJECT_NAME" exec backend \
|
||||
bench new-site \
|
||||
--mariadb-user-host-login-scope='%' \
|
||||
--db-root-password "$DB_ROOT_PASSWORD" \
|
||||
--install-app erpnext \
|
||||
--admin-password "$ADMIN_PASSWORD" \
|
||||
"$SITE_NAME"
|
||||
|
||||
# Success message
|
||||
echo ""
|
||||
echo_info "✓ Site created successfully!"
|
||||
echo_info "URL: https://$SITE_NAME"
|
||||
echo_info "Username: Administrator"
|
||||
echo_info "Password: $ADMIN_PASSWORD"
|
||||
echo ""
|
||||
echo_warn "Next steps:"
|
||||
echo_warn "1. Point DNS $SITE_NAME to your server IP"
|
||||
echo_warn "2. Update SITES in production.env"
|
||||
echo_warn "3. Change admin password after login"
|
||||
echo_warn "4. Wait for SSL certificate (few minutes)"
|
||||
echo ""
|
||||
echo_info "Set as default: docker compose -p $PROJECT_NAME exec backend bench use $SITE_NAME"
|
||||
155
production/scripts/deploy.sh
Normal file
155
production/scripts/deploy.sh
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ERPNext Production Deployment Script
|
||||
# Usage: ./deploy.sh [--setup|--regenerate]
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
|
||||
# Helpers
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Directories
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PRODUCTION_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
PROJECT_ROOT="$(dirname "$PRODUCTION_DIR")"
|
||||
|
||||
cd "$PRODUCTION_DIR"
|
||||
|
||||
# Parse arguments
|
||||
case "${1:-}" in
|
||||
--help|-h)
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--setup Setup environment files from templates
|
||||
--regenerate Only regenerate production.yaml (don't deploy)
|
||||
--help, -h Show this help
|
||||
|
||||
Examples:
|
||||
$0 # Normal deployment
|
||||
$0 --setup # Create env files first
|
||||
$0 --regenerate # Regenerate production.yaml only
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
--setup) MODE="setup" ;;
|
||||
--regenerate) MODE="regenerate" ;;
|
||||
"") MODE="deploy" ;;
|
||||
*) echo_error "Unknown: $1 (use --help)"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Setup mode: create env files
|
||||
if [[ "$MODE" == "setup" ]]; then
|
||||
echo_info "Setting up environment files..."
|
||||
|
||||
[[ ! -f "production.env.example" ]] && { echo_error "Template files missing!"; exit 1; }
|
||||
|
||||
for template in production.env.example traefik.env.example mariadb.env.example; do
|
||||
target="${template%.example}"
|
||||
if [[ -f "$target" ]]; then
|
||||
echo_warn "$target exists, skipping..."
|
||||
else
|
||||
cp "$template" "$target" && chmod 600 "$target"
|
||||
echo_info "✓ Created $target"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo_info "Edit these files before deploying:"
|
||||
echo_info " 1. production.env - SITES, passwords, email"
|
||||
echo_info " 2. mariadb.env - DB_PASSWORD"
|
||||
echo_info " 3. traefik.env - domain, email, password"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate prerequisites
|
||||
[[ $EUID -eq 0 ]] && { echo_error "Don't run as root"; exit 1; }
|
||||
command -v docker &> /dev/null || { echo_error "Docker not installed"; exit 1; }
|
||||
docker compose version &> /dev/null || { echo_error "Docker Compose V2 not installed"; exit 1; }
|
||||
|
||||
echo_info "ERPNext Production Deployment"
|
||||
|
||||
# Check env files exist
|
||||
for file in production.env traefik.env mariadb.env; do
|
||||
[[ ! -f "$file" ]] && { echo_error "$file not found! Run: $0 --setup"; exit 1; }
|
||||
done
|
||||
|
||||
# Validate configuration
|
||||
echo_info "Validating configuration..."
|
||||
./scripts/validate-env.sh || { echo_error "Validation failed!"; exit 1; }
|
||||
|
||||
# Warn about defaults
|
||||
if grep -q "changeit" production.env mariadb.env traefik.env 2>/dev/null; then
|
||||
echo_warn "Default passwords detected!"
|
||||
read -p "Updated all passwords? (yes/no): " confirm
|
||||
[[ "$confirm" != "yes" ]] && { echo_error "Update passwords first"; exit 1; }
|
||||
fi
|
||||
|
||||
if grep -q "yourdomain.com\|CHANGEME_" production.env traefik.env 2>/dev/null; then
|
||||
echo_warn "Default domains detected!"
|
||||
read -p "Updated all domains? (yes/no): " confirm
|
||||
[[ "$confirm" != "yes" ]] && { echo_error "Update domains first"; exit 1; }
|
||||
fi
|
||||
|
||||
# Generate production.yaml helper
|
||||
generate_yaml() {
|
||||
[[ -f "production.yaml" ]] && cp production.yaml "production.yaml.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
docker compose --project-name erpnext-production \
|
||||
--env-file production.env \
|
||||
-f "$PROJECT_ROOT/compose.yaml" \
|
||||
-f "$PROJECT_ROOT/overrides/compose.redis.yaml" \
|
||||
-f "$PROJECT_ROOT/overrides/compose.multi-bench.yaml" \
|
||||
-f "$PROJECT_ROOT/overrides/compose.multi-bench-ssl.yaml" \
|
||||
config > production.yaml
|
||||
}
|
||||
|
||||
# Regenerate mode: just regenerate yaml
|
||||
if [[ "$MODE" == "regenerate" ]]; then
|
||||
echo_info "Regenerating production.yaml..."
|
||||
generate_yaml
|
||||
echo_info "✓ Regenerated. Apply: docker compose -f production.yaml up -d"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Deploy services
|
||||
echo ""
|
||||
echo_info "Step 1: Deploying Traefik..."
|
||||
docker compose --project-name traefik \
|
||||
--env-file traefik.env \
|
||||
-f "$PROJECT_ROOT/overrides/compose.traefik.yaml" \
|
||||
-f "$PROJECT_ROOT/overrides/compose.traefik-ssl.yaml" \
|
||||
up -d
|
||||
echo_info "✓ Traefik deployed"
|
||||
|
||||
echo_info "Step 2: Deploying MariaDB..."
|
||||
docker compose --project-name mariadb \
|
||||
--env-file mariadb.env \
|
||||
-f "$PROJECT_ROOT/overrides/compose.mariadb-shared.yaml" \
|
||||
up -d
|
||||
echo_info "✓ MariaDB deployed. Waiting 30s for initialization..."
|
||||
sleep 30
|
||||
|
||||
echo_info "Step 3: Generating production.yaml..."
|
||||
generate_yaml
|
||||
echo_info "✓ Generated"
|
||||
|
||||
echo_info "Step 4: Deploying ERPNext..."
|
||||
docker compose --project-name erpnext-production -f production.yaml up -d
|
||||
echo_info "✓ ERPNext deployed"
|
||||
|
||||
# Success message
|
||||
TRAEFIK_DOMAIN=$(grep "^TRAEFIK_DOMAIN=" traefik.env | cut -d'=' -f2)
|
||||
echo ""
|
||||
echo_info "✓ Deployment complete!"
|
||||
echo_info "Next steps:"
|
||||
echo_info " 1. Check health: docker ps"
|
||||
echo_info " 2. Create site: ./scripts/create-site.sh"
|
||||
echo_info " 3. Traefik: https://$TRAEFIK_DOMAIN"
|
||||
echo_warn "Note: SSL certificates may take a few minutes"
|
||||
65
production/scripts/logs.sh
Normal file
65
production/scripts/logs.sh
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/bin/bash
|
||||
|
||||
# View ERPNext Logs Script
|
||||
# Usage: ./logs.sh [service-number-or-name]
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
|
||||
# Helper functions
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Navigate to production directory
|
||||
cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" || exit 1
|
||||
|
||||
# Show menu only if no argument provided
|
||||
if [ -z "$1" ]; then
|
||||
echo_info "Available services:"
|
||||
echo " 1. backend 2. frontend 3. websocket"
|
||||
echo " 4. queue-short 5. queue-long 6. scheduler 7. all"
|
||||
read -p "Enter number or name: " INPUT
|
||||
else
|
||||
INPUT=$1
|
||||
fi
|
||||
|
||||
# Map input to service name
|
||||
case "$INPUT" in
|
||||
-h|--help)
|
||||
cat << EOF
|
||||
Usage: $0 [service-number-or-name]
|
||||
|
||||
Services:
|
||||
1 or backend - Gunicorn backend
|
||||
2 or frontend - Nginx frontend
|
||||
3 or websocket - Socket.io service
|
||||
4 or queue-short - Short queue worker
|
||||
5 or queue-long - Long queue worker
|
||||
6 or scheduler - Background scheduler
|
||||
7 or all - All services
|
||||
|
||||
Examples:
|
||||
$0 # Interactive menu
|
||||
$0 1 # View backend logs
|
||||
$0 backend # Same as above
|
||||
$0 all # View all logs
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
1|backend) SERVICE="backend" ;;
|
||||
2|frontend) SERVICE="frontend" ;;
|
||||
3|websocket) SERVICE="websocket" ;;
|
||||
4|queue-short) SERVICE="queue-short" ;;
|
||||
5|queue-long) SERVICE="queue-long" ;;
|
||||
6|scheduler) SERVICE="scheduler" ;;
|
||||
7|all) SERVICE="all" ;;
|
||||
*) echo_error "Invalid: $INPUT. Use 1-7 or service name."; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Check if services are running
|
||||
docker ps | grep -q "erpnext-production" || { echo_error "ERPNext is not running!"; exit 1; }
|
||||
|
||||
# Show logs
|
||||
echo_info "Logs for: $SERVICE (Ctrl+C to exit)"
|
||||
[ "$SERVICE" = "all" ] && SERVICE=""
|
||||
docker compose --project-name erpnext-production -f production.yaml logs -f $SERVICE
|
||||
68
production/scripts/stop.sh
Normal file
68
production/scripts/stop.sh
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Stop ERPNext Production Services
|
||||
# Usage: ./stop.sh [--all]
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
||||
|
||||
# Helpers
|
||||
echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
|
||||
# Navigate to production directory
|
||||
cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" || exit 1
|
||||
|
||||
PROJECT_ROOT="$(dirname "$(pwd)")"
|
||||
|
||||
# Check for help first
|
||||
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
|
||||
cat << EOF
|
||||
Usage: $0 [--all]
|
||||
|
||||
Options:
|
||||
--all Stop ERPNext, MariaDB, and Traefik
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$0 # Stop ERPNext only (interactive)
|
||||
$0 --all # Stop all services (no prompt)
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stop service helper
|
||||
stop_service() {
|
||||
local name=$1 project=$2
|
||||
shift 2
|
||||
|
||||
if docker ps | grep -q "$name"; then
|
||||
echo_info "Stopping $name..."
|
||||
docker compose --project-name "$project" "$@" down
|
||||
echo_info "✓ $name stopped"
|
||||
else
|
||||
echo_warn "$name not running"
|
||||
fi
|
||||
}
|
||||
|
||||
echo_info "Stopping ERPNext services..."
|
||||
|
||||
# Stop ERPNext
|
||||
stop_service "erpnext-production" "erpnext-production" -f production.yaml
|
||||
|
||||
# Ask about stopping dependencies
|
||||
STOP_ALL="${1:-}"
|
||||
if [[ "$STOP_ALL" != "--all" ]]; then
|
||||
read -p "Stop MariaDB and Traefik too? (yes/no): " STOP_ALL
|
||||
fi
|
||||
|
||||
if [[ "$STOP_ALL" == "yes" ]] || [[ "$STOP_ALL" == "--all" ]]; then
|
||||
stop_service "mariadb" "mariadb" --env-file mariadb.env -f "$PROJECT_ROOT/overrides/compose.mariadb-shared.yaml"
|
||||
stop_service "traefik" "traefik" --env-file traefik.env -f "$PROJECT_ROOT/overrides/compose.traefik.yaml" -f "$PROJECT_ROOT/overrides/compose.traefik-ssl.yaml"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo_info "✓ Services stopped. Restart: ./scripts/deploy.sh"
|
||||
249
production/scripts/validate-env.sh
Normal file
249
production/scripts/validate-env.sh
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Validate Environment Configuration Script
|
||||
# Checks for common issues in production env files
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly NC='\033[0m'
|
||||
|
||||
# Counters
|
||||
errors=0
|
||||
warnings=0
|
||||
|
||||
# Logging functions
|
||||
echo_info() { echo -e "${GREEN}✓${NC} $1"; }
|
||||
echo_warn() { echo -e "${YELLOW}⚠${NC} $1"; ((warnings++)); }
|
||||
echo_error() { echo -e "${RED}✗${NC} $1"; ((errors++)); }
|
||||
|
||||
# Validation patterns
|
||||
readonly WEAK_PASSWORDS="changeit|123456|admin123|password123|qwerty|letmein|welcome"
|
||||
readonly PLACEHOLDER_DOMAINS="yourdomain\.com|example\.com|localhost"
|
||||
readonly EMAIL_REGEX="^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$"
|
||||
|
||||
# Get script directory and change to production directory
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly PRODUCTION_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PRODUCTION_DIR"
|
||||
|
||||
# Validate single environment file
|
||||
validate_env_file() {
|
||||
local file="$1"
|
||||
local required_vars=("${@:2}")
|
||||
|
||||
echo "Checking $file..."
|
||||
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo_error "$file not found"
|
||||
echo " Create it from ${file}.example or run setup script"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -s "$file" ]]; then
|
||||
echo_error "$file is empty"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo_info "File exists"
|
||||
|
||||
# Read file content once
|
||||
local content
|
||||
content=$(grep -v "^\s*#" "$file" 2>/dev/null | grep "=" || true)
|
||||
|
||||
# Check placeholders
|
||||
if echo "$content" | grep -q "CHANGEME_"; then
|
||||
echo_error "Contains CHANGEME_ placeholders"
|
||||
echo "$content" | grep "CHANGEME_" | sed 's/^/ /'
|
||||
return 1
|
||||
fi
|
||||
echo_info "No CHANGEME_ placeholders"
|
||||
|
||||
# Check weak passwords
|
||||
echo "$content" | cut -d'=' -f2- | grep -qi "$WEAK_PASSWORDS" && echo_warn "Contains weak passwords"
|
||||
|
||||
# Check required variables
|
||||
local missing_vars=()
|
||||
for var in "${required_vars[@]}"; do
|
||||
echo "$content" | grep -q "^$var=" || missing_vars+=("$var")
|
||||
done
|
||||
|
||||
[[ ${#missing_vars[@]} -gt 0 ]] && { echo_error "Missing: ${missing_vars[*]}"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate email
|
||||
validate_email() {
|
||||
local email="$1" var_name="$2"
|
||||
[[ ! "$email" =~ $EMAIL_REGEX ]] && { echo_error "$var_name invalid: $email"; return 1; }
|
||||
echo "$email" | grep -q "$PLACEHOLDER_DOMAINS" && { echo_error "$var_name has placeholder"; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate password strength
|
||||
validate_password_strength() {
|
||||
local password="$1" min_length="${2:-16}"
|
||||
if [[ ${#password} -lt $min_length ]]; then
|
||||
echo_warn "Password < $min_length chars (current: ${#password})"
|
||||
return 1
|
||||
fi
|
||||
echo_info "Password length good (${#password} chars)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extract value from env file
|
||||
get_env_value() {
|
||||
local file="$1"
|
||||
local var="$2"
|
||||
grep "^$var=" "$file" 2>/dev/null | cut -d'=' -f2- || echo ""
|
||||
}
|
||||
|
||||
# Main validation
|
||||
main() {
|
||||
# Handle help
|
||||
if [[ "${1:-}" == "-h" ]] || [[ "${1:-}" == "--help" ]]; then
|
||||
cat << EOF
|
||||
Usage: $0
|
||||
|
||||
Validates ERPNext production environment configuration files.
|
||||
|
||||
Checks:
|
||||
- production.env (DB_PASSWORD, SITES, LETSENCRYPT_EMAIL)
|
||||
- traefik.env (TRAEFIK_DOMAIN, EMAIL, HASHED_PASSWORD)
|
||||
- mariadb.env (DB_PASSWORD)
|
||||
- Password strength and cross-file consistency
|
||||
- Placeholder and weak password detection
|
||||
|
||||
Exit Codes:
|
||||
0 - Validation passed
|
||||
1 - Validation failed (errors found)
|
||||
|
||||
Examples:
|
||||
$0 # Validate all env files
|
||||
|
||||
Run before deployment to catch configuration issues.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Validating Production Environment Configuration"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
# Validate production.env
|
||||
if validate_env_file "production.env" "DB_PASSWORD" "DB_HOST" "LETSENCRYPT_EMAIL" "SITES"; then
|
||||
# Additional production.env validations
|
||||
local letsencrypt_email
|
||||
letsencrypt_email=$(get_env_value "production.env" "LETSENCRYPT_EMAIL")
|
||||
if [[ -n "$letsencrypt_email" ]]; then
|
||||
validate_email "$letsencrypt_email" "LETSENCRYPT_EMAIL"
|
||||
fi
|
||||
|
||||
# Check SITES format
|
||||
local sites
|
||||
sites=$(get_env_value "production.env" "SITES")
|
||||
if [[ -n "$sites" ]] && [[ ! "$sites" =~ ^\`.*\`$ ]]; then
|
||||
echo_warn "SITES should be wrapped in backticks: SITES=\`erp.example.com\`"
|
||||
fi
|
||||
|
||||
# Validate DB password strength
|
||||
local db_password
|
||||
db_password=$(get_env_value "production.env" "DB_PASSWORD")
|
||||
if [[ -n "$db_password" ]]; then
|
||||
validate_password_strength "$db_password"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Validate traefik.env
|
||||
if validate_env_file "traefik.env" "TRAEFIK_DOMAIN" "EMAIL" "HASHED_PASSWORD"; then
|
||||
# Additional traefik.env validations
|
||||
local traefik_email
|
||||
traefik_email=$(get_env_value "traefik.env" "EMAIL")
|
||||
if [[ -n "$traefik_email" ]]; then
|
||||
validate_email "$traefik_email" "EMAIL"
|
||||
fi
|
||||
|
||||
# Check if password is properly hashed
|
||||
local hashed_password
|
||||
hashed_password=$(get_env_value "traefik.env" "HASHED_PASSWORD")
|
||||
if [[ -n "$hashed_password" ]] && echo "$hashed_password" | grep -q "openssl\|changeit"; then
|
||||
echo_error "HASHED_PASSWORD not properly set"
|
||||
echo " Generate with: openssl passwd -apr1 yourpassword"
|
||||
fi
|
||||
|
||||
# Check if HASHED_PASSWORD has username prefix (it shouldn't)
|
||||
if [[ -n "$hashed_password" ]] && echo "$hashed_password" | grep -q "^admin:"; then
|
||||
echo_error "HASHED_PASSWORD should NOT include 'admin:' prefix"
|
||||
echo_warn "Remove 'admin:' from the hash in traefik.env"
|
||||
echo_warn "The compose file adds it automatically"
|
||||
fi
|
||||
|
||||
# Check domain format
|
||||
local traefik_domain
|
||||
traefik_domain=$(get_env_value "traefik.env" "TRAEFIK_DOMAIN")
|
||||
if [[ -n "$traefik_domain" ]] && echo "$traefik_domain" | grep -q "$PLACEHOLDER_DOMAINS"; then
|
||||
echo_error "TRAEFIK_DOMAIN still has placeholder domain"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Validate mariadb.env
|
||||
validate_env_file "mariadb.env" "DB_PASSWORD"
|
||||
|
||||
echo ""
|
||||
|
||||
# Cross-file validation
|
||||
echo "Cross-checking configurations..."
|
||||
if [[ -f "production.env" && -f "mariadb.env" ]]; then
|
||||
local prod_pass maria_pass
|
||||
prod_pass=$(get_env_value "production.env" "DB_PASSWORD")
|
||||
maria_pass=$(get_env_value "mariadb.env" "DB_PASSWORD")
|
||||
|
||||
if [[ -n "$prod_pass" && -n "$maria_pass" ]]; then
|
||||
if [[ "$prod_pass" == "$maria_pass" ]]; then
|
||||
echo_info "Database passwords match"
|
||||
else
|
||||
echo_error "Database passwords DO NOT match between files"
|
||||
# Don't expose actual passwords in logs
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Final summary
|
||||
echo ""
|
||||
echo "=================================================="
|
||||
echo "Validation Summary"
|
||||
echo "=================================================="
|
||||
echo ""
|
||||
|
||||
if (( errors > 0 )); then
|
||||
echo -e "${RED}❌ Validation Failed${NC}"
|
||||
echo " Errors: $errors"
|
||||
echo " Warnings: $warnings"
|
||||
echo ""
|
||||
echo "Please fix the errors above before deploying."
|
||||
exit 1
|
||||
elif (( warnings > 0 )); then
|
||||
echo -e "${YELLOW}⚠️ Validation Passed with Warnings${NC}"
|
||||
echo " Warnings: $warnings"
|
||||
echo ""
|
||||
echo "Consider addressing the warnings for better security."
|
||||
exit 0
|
||||
else
|
||||
echo -e "${GREEN}✅ Validation Passed${NC}"
|
||||
echo " No errors or warnings found."
|
||||
echo ""
|
||||
echo "You can now proceed with deployment:"
|
||||
echo " ./scripts/deploy.sh"
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
Loading…
Reference in a new issue