- Created docker-compose.docker-local.yml for local testing of frontend and backend services. - Added .env.development for development environment configuration. - Introduced .env.docker-local for local Docker environment settings. - Added .env.production for production environment configuration for Synology deployment.
606 lines
20 KiB
Bash
Executable File
606 lines
20 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
###############################################################################
|
||
# Pokedex.Online Deployment Automation Script
|
||
#
|
||
# This script automates the deployment process with pre-deployment checks,
|
||
# build verification, and rollback capability.
|
||
#
|
||
# Usage:
|
||
# ./deploy.sh [options]
|
||
#
|
||
# Options:
|
||
# --target <local|production> Deployment target (default: local)
|
||
# --port <number> Frontend HTTP port (default: 8099)
|
||
# --backend-port <number> Backend port (default: 3099)
|
||
# --skip-tests Skip test execution
|
||
# --skip-build Skip build step (use existing dist/)
|
||
# --no-backup Skip backup creation
|
||
# --dry-run Show what would be deployed without deploying
|
||
#
|
||
# Deployment Strategies:
|
||
# local - Docker deployment on localhost:8099 for production testing
|
||
# production - Deploy to Synology at https://app.pokedex.online
|
||
#
|
||
# Examples:
|
||
# ./deploy.sh --target local
|
||
# ./deploy.sh --target production
|
||
# ./deploy.sh --dry-run
|
||
###############################################################################
|
||
|
||
set -e # Exit on error
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Default configuration
|
||
TARGET="local"
|
||
PORT=8099
|
||
BACKEND_PORT=3099
|
||
SKIP_TESTS=false
|
||
SKIP_BUILD=false
|
||
NO_BACKUP=false
|
||
DRY_RUN=false
|
||
|
||
# Script directory
|
||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||
PROJECT_ROOT="$SCRIPT_DIR"
|
||
|
||
###############################################################################
|
||
# Helper Functions
|
||
###############################################################################
|
||
|
||
log_info() {
|
||
echo -e "${BLUE}ℹ${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}✅${NC} $1"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}❌${NC} $1"
|
||
}
|
||
|
||
log_step() {
|
||
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${BLUE}$1${NC}"
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||
}
|
||
|
||
###############################################################################
|
||
# Parse Arguments
|
||
###############################################################################
|
||
|
||
parse_args() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--target)
|
||
TARGET="$2"
|
||
shift 2
|
||
;;
|
||
--port)
|
||
PORT="$2"
|
||
shift 2
|
||
;;
|
||
--backend-port)
|
||
BACKEND_PORT="$2"
|
||
shift 2
|
||
;;
|
||
--skip-tests)
|
||
SKIP_TESTS=true
|
||
shift
|
||
;;
|
||
--skip-build)
|
||
SKIP_BUILD=true
|
||
shift
|
||
;;
|
||
--no-backup)
|
||
NO_BACKUP=true
|
||
shift
|
||
;;
|
||
--dry-run)
|
||
DRY_RUN=true
|
||
shift
|
||
;;
|
||
--help)
|
||
head -n 30 "$0" | tail -n 25
|
||
exit 0
|
||
;;
|
||
*)
|
||
log_error "Unknown option: $1"
|
||
echo "Use --help for usage information"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
###############################################################################
|
||
# Pre-Deployment Checks
|
||
###############################################################################
|
||
|
||
check_prerequisites() {
|
||
log_step "🔍 Checking Prerequisites"
|
||
|
||
# Check Node.js
|
||
if ! command -v node &> /dev/null; then
|
||
log_error "Node.js is not installed"
|
||
exit 1
|
||
fi
|
||
log_success "Node.js $(node --version)"
|
||
|
||
# Check npm
|
||
if ! command -v npm &> /dev/null; then
|
||
log_error "npm is not installed"
|
||
exit 1
|
||
fi
|
||
log_success "npm $(npm --version)"
|
||
|
||
# Check if dependencies are installed
|
||
if [ ! -d "$PROJECT_ROOT/node_modules" ]; then
|
||
log_error "Dependencies not installed. Run 'npm install' first"
|
||
exit 1
|
||
fi
|
||
log_success "Dependencies installed"
|
||
|
||
# Check if server dependencies are installed (workspaces hoist to root node_modules)
|
||
# Check for key server dependencies in root node_modules
|
||
if [ ! -d "$PROJECT_ROOT/node_modules/express" ] || [ ! -d "$PROJECT_ROOT/node_modules/cors" ]; then
|
||
log_error "Server dependencies not installed. Run 'npm install' first"
|
||
exit 1
|
||
fi
|
||
log_success "Server dependencies installed"
|
||
}
|
||
|
||
check_environment() {
|
||
log_step "🔧 Checking Environment Configuration"
|
||
|
||
# Validate target
|
||
if [ "$TARGET" != "local" ] && [ "$TARGET" != "production" ]; then
|
||
log_error "Invalid target: $TARGET"
|
||
log_info "Valid targets: local, production"
|
||
exit 1
|
||
fi
|
||
|
||
# Check for mode-specific env files
|
||
local mode
|
||
if [ "$TARGET" = "local" ]; then
|
||
mode="docker-local"
|
||
else
|
||
mode="production"
|
||
fi
|
||
|
||
if [ ! -f "$PROJECT_ROOT/.env.$mode" ]; then
|
||
log_error "Environment file .env.$mode not found"
|
||
exit 1
|
||
fi
|
||
log_success "Frontend environment configuration found (.env.$mode)"
|
||
|
||
if [ ! -f "$PROJECT_ROOT/server/.env.$mode" ]; then
|
||
log_error "Environment file server/.env.$mode not found"
|
||
exit 1
|
||
fi
|
||
log_success "Backend environment configuration found (server/.env.$mode)"
|
||
}
|
||
|
||
run_tests() {
|
||
if [ "$SKIP_TESTS" = true ]; then
|
||
log_warning "Skipping tests (--skip-tests flag set)"
|
||
return
|
||
fi
|
||
|
||
log_step "🧪 Running Tests"
|
||
|
||
log_info "Running frontend tests..."
|
||
npm run test:run || {
|
||
log_error "Frontend tests failed"
|
||
exit 1
|
||
}
|
||
log_success "Frontend tests passed"
|
||
|
||
log_info "Running backend tests..."
|
||
npm run test:run --workspace=server || {
|
||
log_warning "Backend tests failed or not found - continuing anyway"
|
||
}
|
||
log_success "Backend checks completed"
|
||
}
|
||
|
||
###############################################################################
|
||
# Build
|
||
###############################################################################
|
||
|
||
prepare_build_mode() {
|
||
# Determine Vite build mode based on deployment target
|
||
if [ "$TARGET" = "local" ]; then
|
||
BUILD_MODE="docker-local"
|
||
else
|
||
BUILD_MODE="production"
|
||
fi
|
||
|
||
log_info "Preparing build for ${TARGET} deployment..."
|
||
log_info "Vite build mode: ${BUILD_MODE}"
|
||
log_info "Environment file: .env.${BUILD_MODE}"
|
||
}
|
||
|
||
build_application() {
|
||
if [ "$SKIP_BUILD" = true ]; then
|
||
log_warning "Skipping build (--skip-build flag set)"
|
||
|
||
# Check if dist exists
|
||
if [ ! -d "$PROJECT_ROOT/dist" ]; then
|
||
log_error "dist/ directory not found and --skip-build is set"
|
||
log_info "Remove --skip-build or run 'npm run build' first"
|
||
exit 1
|
||
fi
|
||
return
|
||
fi
|
||
|
||
log_step "🔨 Building Application"
|
||
|
||
# Prepare build mode
|
||
prepare_build_mode
|
||
|
||
log_info "Building frontend with mode: $BUILD_MODE..."
|
||
npx vite build --mode "$BUILD_MODE" || {
|
||
log_error "Frontend build failed"
|
||
exit 1
|
||
}
|
||
log_success "Frontend built successfully"
|
||
|
||
log_info "Verifying build..."
|
||
BUILD_TARGET="$TARGET" npm run build:verify || {
|
||
log_error "Build verification failed"
|
||
exit 1
|
||
}
|
||
log_success "Build verified"
|
||
}
|
||
|
||
###############################################################################
|
||
# Backup
|
||
###############################################################################
|
||
|
||
create_backup() {
|
||
if [ "$NO_BACKUP" = true ]; then
|
||
log_warning "Skipping backup (--no-backup flag set)"
|
||
return
|
||
fi
|
||
|
||
log_step "💾 Creating Backup"
|
||
|
||
BACKUP_DIR="$PROJECT_ROOT/backups"
|
||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||
BACKUP_FILE="$BACKUP_DIR/backup_${TIMESTAMP}.tar.gz"
|
||
|
||
mkdir -p "$BACKUP_DIR"
|
||
|
||
log_info "Creating backup of current deployment..."
|
||
tar -czf "$BACKUP_FILE" \
|
||
--exclude='node_modules' \
|
||
--exclude='dist' \
|
||
--exclude='.git' \
|
||
--exclude='backups' \
|
||
-C "$PROJECT_ROOT" . || {
|
||
log_error "Backup creation failed"
|
||
exit 1
|
||
}
|
||
|
||
log_success "Backup created: $BACKUP_FILE"
|
||
|
||
# Keep only last 5 backups
|
||
BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | wc -l)
|
||
if [ "$BACKUP_COUNT" -gt 5 ]; then
|
||
log_info "Cleaning old backups (keeping last 5)..."
|
||
ls -1t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +6 | xargs rm -f
|
||
fi
|
||
}
|
||
|
||
###############################################################################
|
||
# Deployment
|
||
###############################################################################
|
||
|
||
deploy_to_server() {
|
||
log_step "🚀 Deploying to Server"
|
||
|
||
if [ "$DRY_RUN" = true ]; then
|
||
log_info "DRY RUN - Would deploy with following configuration:"
|
||
log_info " Target: $TARGET"
|
||
log_info " Frontend Port: $PORT"
|
||
log_info " Backend Port: $BACKEND_PORT"
|
||
log_success "Dry run completed"
|
||
return
|
||
fi
|
||
|
||
if [ "$TARGET" = "local" ]; then
|
||
log_info "Deploying to local Docker..."
|
||
|
||
# Copy server env file
|
||
cp "$PROJECT_ROOT/server/.env.docker-local" "$PROJECT_ROOT/server/.env" || {
|
||
log_error "Failed to copy server environment file"
|
||
exit 1
|
||
}
|
||
|
||
# Use docker-compose.docker-local.yml
|
||
log_info "Starting Docker containers..."
|
||
docker compose -f docker-compose.docker-local.yml down || true
|
||
docker compose -f docker-compose.docker-local.yml up -d --build || {
|
||
log_error "Docker deployment failed"
|
||
exit 1
|
||
}
|
||
|
||
else
|
||
log_info "Deploying to production (Synology)..."
|
||
deploy_to_synology
|
||
fi
|
||
|
||
log_success "Deployment completed successfully"
|
||
}
|
||
|
||
deploy_to_synology() {
|
||
# SSH Configuration
|
||
local SSH_HOST="10.0.0.81"
|
||
local SSH_PORT="2323"
|
||
local SSH_USER="GregRJacobs"
|
||
local SSH_KEY="$HOME/.ssh/ds3627xs_gregrjacobs"
|
||
local REMOTE_PATH="/volume1/docker/pokedex-online"
|
||
|
||
log_info "SSH Config: ${SSH_USER}@${SSH_HOST}:${SSH_PORT}"
|
||
|
||
# Verify SSH key exists
|
||
if [ ! -f "$SSH_KEY" ]; then
|
||
log_error "SSH key not found: $SSH_KEY"
|
||
exit 1
|
||
fi
|
||
|
||
# Test SSH connection
|
||
log_info "Testing SSH connection..."
|
||
if ! ssh -i "$SSH_KEY" -p "$SSH_PORT" -o ConnectTimeout=10 -o BatchMode=yes "$SSH_USER@$SSH_HOST" "echo 'Connection successful'" > /dev/null 2>&1; then
|
||
log_error "SSH connection failed"
|
||
log_info "Please verify:"
|
||
log_info " 1. SSH key exists: $SSH_KEY"
|
||
log_info " 2. Key has correct permissions: chmod 600 $SSH_KEY"
|
||
log_info " 3. Public key is in authorized_keys on server"
|
||
log_info " 4. You can connect manually: ssh -i $SSH_KEY -p $SSH_PORT $SSH_USER@$SSH_HOST"
|
||
exit 1
|
||
fi
|
||
log_success "SSH connection verified"
|
||
|
||
# Create remote directory
|
||
log_info "Creating remote directory..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "mkdir -p $REMOTE_PATH/dist $REMOTE_PATH/server" || {
|
||
log_error "Failed to create remote directory"
|
||
exit 1
|
||
}
|
||
|
||
# Transfer dist directory using tar over SSH
|
||
log_info "Transferring frontend build (dist/)..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "rm -rf $REMOTE_PATH/dist && mkdir -p $REMOTE_PATH/dist"
|
||
tar -czf - -C "$PROJECT_ROOT" dist 2>/dev/null | \
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"tar -xzf - -C $REMOTE_PATH --strip-components=0 2>/dev/null" || {
|
||
log_error "Failed to transfer dist directory"
|
||
exit 1
|
||
}
|
||
log_success "Frontend build transferred"
|
||
|
||
# Transfer server directory (excluding node_modules) using tar over SSH
|
||
log_info "Transferring backend code (server/)..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "rm -rf $REMOTE_PATH/server && mkdir -p $REMOTE_PATH/server"
|
||
tar -czf - -C "$PROJECT_ROOT" \
|
||
--exclude='node_modules' \
|
||
--exclude='.env*' \
|
||
--exclude='data' \
|
||
--exclude='logs' \
|
||
server 2>/dev/null | \
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"tar -xzf - -C $REMOTE_PATH --strip-components=0 2>/dev/null" || {
|
||
log_error "Failed to transfer server directory"
|
||
exit 1
|
||
}
|
||
log_success "Backend code transferred"
|
||
|
||
# Copy production environment file to server (after server code transfer)
|
||
log_info "Copying server environment file..."
|
||
cat "$PROJECT_ROOT/server/.env.production" | \
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cat > $REMOTE_PATH/server/.env" || {
|
||
log_error "Failed to copy server .env file"
|
||
exit 1
|
||
}
|
||
log_success "Server environment configured"
|
||
|
||
# Create required directories for volume mounts
|
||
log_info "Creating volume mount directories..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"mkdir -p $REMOTE_PATH/server/data $REMOTE_PATH/server/logs" || {
|
||
log_error "Failed to create volume mount directories"
|
||
exit 1
|
||
}
|
||
|
||
# Transfer Docker configuration files
|
||
log_info "Transferring Docker configuration..."
|
||
for file in docker-compose.production.yml nginx.conf Dockerfile.frontend; do
|
||
cat "$PROJECT_ROOT/$file" | \
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cat > $REMOTE_PATH/$file" || {
|
||
log_error "Failed to transfer $file"
|
||
exit 1
|
||
}
|
||
done
|
||
|
||
cat "$PROJECT_ROOT/server/Dockerfile" | \
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cat > $REMOTE_PATH/server/Dockerfile" || {
|
||
log_error "Failed to transfer server Dockerfile"
|
||
exit 1
|
||
}
|
||
log_success "Docker configuration transferred"
|
||
|
||
# Find Docker on remote system
|
||
log_info "Locating Docker on remote system..."
|
||
DOCKER_PATH=$(ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"which docker 2>/dev/null || echo /usr/local/bin/docker")
|
||
log_info "Using Docker at: $DOCKER_PATH"
|
||
|
||
# Stop and remove existing containers (force cleanup)
|
||
log_info "Stopping and removing existing containers..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml down --remove-orphans 2>/dev/null || true"
|
||
|
||
# Force remove any lingering containers with matching names
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"$DOCKER_PATH rm -f pokedex-frontend pokedex-backend 2>/dev/null || true"
|
||
|
||
# Start containers
|
||
log_info "Starting Docker containers..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml up -d --build" || {
|
||
log_error "Failed to start Docker containers"
|
||
log_info "Rolling back..."
|
||
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" \
|
||
"cd $REMOTE_PATH && $DOCKER_PATH compose -f docker-compose.production.yml down --remove-orphans"
|
||
exit 1
|
||
}
|
||
log_success "Containers started"
|
||
}
|
||
|
||
###############################################################################
|
||
# Post-Deployment
|
||
###############################################################################
|
||
|
||
verify_deployment() {
|
||
log_step "🏥 Verifying Deployment"
|
||
|
||
if [ "$DRY_RUN" = true ]; then
|
||
log_info "DRY RUN - Skipping verification"
|
||
return
|
||
fi
|
||
|
||
# Determine host based on target
|
||
if [ "$TARGET" = "production" ]; then
|
||
HOST="10.0.0.81"
|
||
else
|
||
HOST="localhost"
|
||
fi
|
||
|
||
log_info "Checking frontend health..."
|
||
sleep 3 # Give containers time to start
|
||
|
||
if curl -f -s "http://$HOST:$PORT/health" > /dev/null; then
|
||
log_success "Frontend is responding"
|
||
else
|
||
log_warning "Frontend health check failed"
|
||
fi
|
||
|
||
log_info "Checking backend health..."
|
||
if curl -f -s "http://$HOST:$BACKEND_PORT/health" > /dev/null; then
|
||
log_success "Backend is responding"
|
||
else
|
||
log_warning "Backend health check failed"
|
||
fi
|
||
}
|
||
|
||
print_summary() {
|
||
log_step "📊 Deployment Summary"
|
||
|
||
if [ "$DRY_RUN" = true ]; then
|
||
log_info "DRY RUN completed - no changes made"
|
||
return
|
||
fi
|
||
|
||
# Determine URLs based on target
|
||
if [ "$TARGET" = "production" ]; then
|
||
FRONTEND_URL="https://app.pokedex.online"
|
||
BACKEND_URL="http://10.0.0.81:$BACKEND_PORT"
|
||
else
|
||
FRONTEND_URL="http://localhost:$PORT"
|
||
BACKEND_URL="http://localhost:$BACKEND_PORT"
|
||
fi
|
||
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${GREEN} 🎉 Deployment Successful!${NC}"
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo -e " ${BLUE}Frontend:${NC} $FRONTEND_URL"
|
||
echo -e " ${BLUE}Backend:${NC} $BACKEND_URL"
|
||
echo -e " ${BLUE}Target:${NC} $TARGET"
|
||
echo ""
|
||
echo -e " ${YELLOW}Next Steps:${NC}"
|
||
echo -e " • Test the application manually"
|
||
if [ "$TARGET" = "local" ]; then
|
||
echo -e " • Check logs: docker compose -f docker-compose.local.yml logs -f"
|
||
echo -e " • Stop containers: docker compose -f docker-compose.local.yml down"
|
||
else
|
||
echo -e " • Check logs via SSH or Docker commands on Synology"
|
||
fi
|
||
echo -e " • Monitor backend: curl $BACKEND_URL/health"
|
||
echo ""
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
}
|
||
|
||
###############################################################################
|
||
# Rollback
|
||
###############################################################################
|
||
|
||
rollback() {
|
||
log_step "🔄 Rollback Instructions"
|
||
|
||
echo "To rollback to a previous version:"
|
||
echo ""
|
||
echo "1. List available backups:"
|
||
echo " ls -lh backups/"
|
||
echo ""
|
||
echo "2. Extract backup:"
|
||
echo " tar -xzf backups/backup_TIMESTAMP.tar.gz -C /tmp/restore"
|
||
echo ""
|
||
echo "3. Copy files back:"
|
||
echo " rsync -av /tmp/restore/ ./"
|
||
echo ""
|
||
echo "4. Redeploy:"
|
||
echo " ./deploy.sh --skip-tests --target $TARGET"
|
||
echo ""
|
||
echo "Or use the deployment script's built-in rollback:"
|
||
echo " node ../../utils/deploy-pokedex.js --target $TARGET"
|
||
echo " (will auto-rollback on failure)"
|
||
}
|
||
|
||
###############################################################################
|
||
# Main Execution
|
||
###############################################################################
|
||
|
||
main() {
|
||
echo -e "${BLUE}"
|
||
echo "╔════════════════════════════════════════════════════════════╗"
|
||
echo "║ ║"
|
||
echo "║ Pokedex.Online Deployment Automation ║"
|
||
echo "║ ║"
|
||
echo "╚════════════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}\n"
|
||
|
||
# Parse command line arguments
|
||
parse_args "$@"
|
||
|
||
# Run deployment pipeline
|
||
check_prerequisites
|
||
check_environment
|
||
run_tests
|
||
build_application
|
||
create_backup
|
||
deploy_to_server
|
||
verify_deployment
|
||
print_summary
|
||
|
||
log_success "All done! 🚀"
|
||
}
|
||
|
||
# Run main function
|
||
main "$@"
|