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:
duthink 2025-11-13 15:32:30 +05:30
parent a3fad9b9c7
commit f167e83bda
9 changed files with 2605 additions and 0 deletions

1637
production/README.md Normal file

File diff suppressed because it is too large Load diff

View 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

View 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

View 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!"

View 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"

View 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"

View 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

View 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"

View 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 "$@"