Files
ansible-gitea/roles/docker/templates/manage-docker.sh.j2
T
2025-12-05 11:18:26 +07:00

1780 lines
62 KiB
Django/Jinja
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================================
# ENHANCED DOCKER MANAGEMENT SCRIPT WITH AUTO-SCALING
# Application: {{ app_name }}
# Environment: {{ deploy_env }}{{ env_tag | default('') }}
# Generated: {{ ansible_date_time.iso8601 }}
# ============================================================================
set -eo pipefail
# ============================================================================
# CONFIGURATION & ENVIRONMENT DETECTION
# ============================================================================
# Extract APP_NAME and ENVIRONMENT from script name
SCRIPT_NAME=$(basename "$0")
SCRIPT_NAME="${SCRIPT_NAME/-worker/}"
if [[ "$SCRIPT_NAME" =~ manage-(.+)-(.+)\.sh ]]; then
APP_NAME="${BASH_REMATCH[1]}"
ENVIRONMENT="${BASH_REMATCH[2]}"
else
echo "❌ Error: Could not extract APP_NAME and ENVIRONMENT from script name: $SCRIPT_NAME"
echo "Expected format: manage-<APP_NAME>-<ENVIRONMENT>.sh"
exit 1
fi
# Default configuration
FRAMEWORK={{ framework }}
DEFAULT_REPLICAS={{ default_replicas | default(2) }}
MAX_REPLICAS={{ max_replicas | default(5) }}
HEALTH_CHECK_TIMEOUT={{ health_check_timeout | default(60) }}
DEPLOYMENT_TIMEOUT={{ deployment_timeout | default(300) }}
DEFAULT_LOG_LINES=100
HOME="/home/{{ server_user }}"
# Environment-specific configuration
CONFIG_DIR="$HOME/ci/configs/${APP_NAME}/${ENVIRONMENT}"
DATA_DIR="$HOME/ci/data/${APP_NAME}/${ENVIRONMENT}"
BACKUP_DIR="$HOME/ci/backups/${APP_NAME}/${ENVIRONMENT}"
LOG_DIR="$HOME/ci/logs/${APP_NAME}/${ENVIRONMENT}"
# Docker configuration
DOCKER_REGISTRY="{{ docker_registry | default('registry.gitlab.com') }}"
DOCKER_IMAGE="{{ docker_image_name }}"
DOCKER_NETWORK="{{ docker_network_name }}"
CONTAINER_PREFIX="{{ container_name }}"
CURRENT_TAG="{{ docker_image_tag }}"
# Port management
BASE_PORT={{ app_port }}
CONTAINER_PORT={{ container_port }}
BINDING_PORT=0
# Resource configuration
RESTART_POLICY="{{ container_restart_policy | default('unless-stopped') }}"
MEMORY_LIMIT="{{ container_memory_limit | default('') }}"
CPU_LIMIT="{{ container_cpu_limit | default('') }}"
CONTAINER_MAPS_FILE="/var/lib/container_maps.json"
CONTAINER_LOCK_FILE="/var/lib/container_maps.lock"
declare -A IS_CONTAINER_MAP_NEW
# Khởi tạo associative array
declare -A container_map
NGINX_CONF_DIR="/etc/nginx/conf.d"
NGINX_CONF_FILE="$NGINX_CONF_DIR/$CONTAINER_PREFIX-upstream.conf"
# Ensure directories exist
mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$BACKUP_DIR" "$LOG_DIR"
IS_WORKER=0
if [[ "$CONTAINER_PREFIX" == *worker* ]]; then
IS_WORKER=1
fi
# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_DIR/manage.log"
}
log_error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ❌ ERROR: $*" | tee -a "$LOG_DIR/manage.log" >&2
}
log_success() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ✅ SUCCESS: $*" | tee -a "$LOG_DIR/manage.log"
}
log_warning() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ⚠️ WARNING: $*" | tee -a "$LOG_DIR/manage.log"
}
log_info() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ℹ️ INFO: $*" | tee -a "$LOG_DIR/manage.log"
}
# ============================================================================
# CONTAINER LOG ARCHIVAL FUNCTIONS
# ============================================================================
# Archive running container logs to timestamped files
archive_running_container_logs() {
{% raw %}
local container_logs_dir="$LOG_DIR/archives"
mkdir -p "$container_logs_dir"
local running_containers
running_containers=$(get_running_containers)
if [[ -z "$running_containers" ]]; then
return 0
fi
# Get current timestamp for all logs in this batch
local batch_timestamp=$(date +%Y%m%d-%H%M%S)
echo "$running_containers" | while read -r container; do
if [[ -z "$container" ]]; then
continue
fi
# Extract container prefix (everything before the last -)
# e.g., gemba-api-1-40001 → gemba-api
local container_prefix_name=$(echo "$container" | sed 's/-[0-9]*$//')
# Create subdirectory per prefix
local prefix_log_dir="$container_logs_dir/$container_prefix_name"
mkdir -p "$prefix_log_dir"
# Log file: container_name_timestamp.log
local log_file="$prefix_log_dir/${container}_${batch_timestamp}.log"
# Capture running logs from container
docker logs "$container" > "$log_file" 2>&1
CONTAINER_ID_FULL=$(docker inspect --format='{{.Id}}' "$container" 2>/dev/null)
CONTAINERLOG_PATH="/var/lib/docker/containers/${CONTAINER_ID_FULL}/${CONTAINER_ID_FULL}-json.log"
sudo sh -c "cat /dev/null > $CONTAINERLOG_PATH"
echo "[$(date +'%Y-%m-%d %H:%M:%S')] 📝 Container logs saved: $log_file" >> "$LOG_DIR/manage.log"
done
{% endraw %}
}
# Rotate container logs (keep only last N logs per container)
rotate_container_runtime_logs() {
local container_logs_dir="$LOG_DIR/archives"
local max_logs_per_container=30 # Keep last 30 log files per container
if [[ ! -d "$container_logs_dir" ]]; then
return 0
fi
# For each container prefix directory
for prefix_dir in "$container_logs_dir"/*; do
if [[ ! -d "$prefix_dir" ]]; then
continue
fi
# For each container in this prefix directory
for container_logs in "$prefix_dir"/*; do
if [[ ! -d "$container_logs" ]]; then
# Count log files for this container pattern
local container_name=$(basename "$container_logs" | sed 's/_[0-9]*\.[0-9]*\.log$//')
local log_count=$(ls "$prefix_dir"/${container_name}_*.log 2>/dev/null | wc -l)
if [[ $log_count -gt $max_logs_per_container ]]; then
local remove_count=$((log_count - max_logs_per_container))
# Remove oldest logs (keep newest)
ls -t "$prefix_dir"/${container_name}_*.log 2>/dev/null | tail -n $remove_count | xargs -r rm -f
fi
fi
done
# Also cleanup at prefix level
local total_logs=$(ls "$prefix_dir"/*.log 2>/dev/null | wc -l)
if [[ $total_logs -gt $((max_logs_per_container * 10)) ]]; then
# If too many logs, remove oldest ones
local remove_count=$((total_logs - max_logs_per_container * 10))
ls -t "$prefix_dir"/*.log 2>/dev/null | tail -n $remove_count | xargs -r rm -f
fi
done
}
# Setup cron job to periodically archive container logs (one-time for all apps)
setup_container_log_archive_cron() {
local cron_file="/etc/cron.d/docker-container-logs-archive"
local script_path="$HOME/scripts/archive-all-container-logs.sh"
# Check if cron job already exists
if [[ -f "$cron_file" ]]; then
return 0
fi
log_info "Setting up global container log archive cron job (every 15 minutes)"
# Create archive script
mkdir -p "$HOME/scripts"
cat > "$script_path" << SCRIPT_EOF
#!/bin/bash
# Global container log archival script for all applications
for manage_script in $HOME/scripts/manage-*.sh; do
if [[ -f "\$manage_script" && -x "\$manage_script" ]]; then
"\$manage_script" archive-logs 2>/dev/null || true
fi
done
SCRIPT_EOF
chmod 755 "$script_path"
# Create cron file (one-time, global for all apps)
sudo tee "$cron_file" > /dev/null << CRON_EOF
# Global Docker container log archival (runs every 15 minutes)
*/15 * * * * root "$script_path" >/dev/null 2>&1
CRON_EOF
sudo chmod 644 "$cron_file"
log_success "Global cron job created: $cron_file"
log_success "Archive script created: $script_path"
}
setup_container_log_archive_cron
# ==== Function: Check and install jq ====
check_and_install_jq() {
if ! command -v jq &>/dev/null; then
echo "[INFO] jq not found. Installing..."
if command -v apt-get &>/dev/null; then
sudo apt-get update -y && sudo apt-get install -y jq
elif command -v yum &>/dev/null; then
sudo yum install -y jq
elif command -v brew &>/dev/null; then
brew install jq
else
echo "[ERROR] Could not detect package manager to install jq."
exit 1
fi
echo "[INFO] jq installed successfully."
fi
}
check_and_install_jq
show_active_ports() {
{% raw %}
mapfile -t containers < <(docker ps --format '{{.Names}}' | grep "^${CONTAINER_PREFIX}")
# Nếu vẫn rỗng thì thông báo lỗi
if [ ${#containers[@]} -eq 0 ]; then
echo "❌ No containers found with prefix: $CONTAINER_PREFIX" >&2
return 1
fi
local ports=()
for c in "${containers[@]}"; do
local port=$(docker port "$c" 3000/tcp | awk -F: '{print $2}' | xargs)
if [ -n "$port" ]; then
ports+=("$port")
fi
done
# Nếu vẫn rỗng sau khi lấy port thì báo lỗi
if [ ${#ports[@]} -eq 0 ]; then
echo "❌ No ports found for containers: ${containers[*]}" >&2
return 1
fi
{% endraw %}
# In tất cả port cách nhau bằng dấu cách
echo "${ports[@]}"
}
# Get port for specific replica using base port + replica number pattern
get_replica_port() {
local base_port=$1
local replica_number=$2
# Create port by concatenating base_port + replica_number
local port="${base_port}${replica_number}"
echo $port
}
# Get random port
get_random_port() {
local port
local max_attempts=100
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
port=$(( RANDOM % (65500 - 40000 + 1) + 40000 )) # Random từ 40000-65500
# Check if port is available (both netstat and Docker)
if is_port_available "$port"; then
echo "$port"
return 0
fi
attempt=$((attempt + 1))
done
# Fallback: find next available port from 40000
log_warning "Random port selection exhausted, finding next available..."
get_next_available_port 40000
}
# Check if port is available
is_port_available() {
local port=$1
{% raw %}
# Check if port is in use by network stack
if netstat -tuln 2>/dev/null | grep -qE ":$port\s"; then
return 1 # Port is busy
fi
# Check if port is reserved by any Docker container (running or stopped)
if docker ps -a --format '{{.Ports}}' 2>/dev/null | grep -qE "0\.0\.0\.0:$port->|:::$port->"; then
return 1 # Port is reserved by Docker
fi
# Double check with lsof if available
if command -v lsof &>/dev/null; then
if lsof -i tcp:"$port" &>/dev/null 2>&1; then
return 1
fi
fi
{% endraw %}
return 0 # Port is available
}
# Get next available port (fallback method)
get_next_available_port() {
local start_port=$1
local port=$start_port
local max_port=$((start_port + 10000))
while [[ $port -lt $max_port ]]; do
if is_port_available $port; then
echo $port
return 0
fi
((port++))
done
log_error "No available port found starting from $start_port"
return 1
}
remove_timestamp() { [[ $1 =~ ^(.*-[0-9]+)-([0-9]+)$ ]] && echo "${BASH_REMATCH[1]}" || echo "$1"; }
# Get all containers for this app
get_app_containers() {
docker ps -a --filter "name=${CONTAINER_PREFIX}" --format "{{ "{{" }}.Names{{ "}}" }}" | sort
}
# Get running containers only
get_running_containers() {
docker ps --filter "name=${CONTAINER_PREFIX}" --format "{{ "{{" }}.Names{{ "}}" }}" | sort
}
# Get container count
get_container_count() {
docker ps --filter "name=${CONTAINER_PREFIX}" --quiet | wc -l
}
# Hàm kiểm tra và giải phóng port
check_and_free_port() {
local port="$1"
local container_old=${2:-}
# Validate port
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "❌ Port không hợp lệ: $port"
fi
# Tìm PID process đang giữ port
local pids
pids=$(lsof -ti tcp:"$port" 2>/dev/null | tr '\n' ' ' | xargs)
if [ -n "$pids" ]; then
# Kiểm tra container nào publish port này
local container_name
container_name=$(docker ps --format '{{ "{{" }}.Names{{ "}}" }}' --filter "publish=$port" | head -n 1)
if [ -n "$container_name" ]; then
container_old="$container_name"
else
echo "🛑 Kill process PID(s): $pids (đang dùng port $port)"
kill -9 $pids 2>/dev/null || true
fi
fi
if [ -n "$container_old" ] && [ "$container_old" != "" ]; then
if docker ps -a --format '{{ "{{" }}.Names{{ "}}" }}' | grep -qx "$container_old"; then
echo "🛑 Dừng container $container_old (đang dùng port $port)"
docker stop "$container_old" >/dev/null 2>&1 || true
docker rm "$container_old" >/dev/null 2>&1 || true
fi
fi
}
# Health check for a container
health_check() {
local container_name=$1
local timeout=${HEALTH_CHECK_TIMEOUT:-30}
if [ "$FRAMEWORK" = "reactjs" ] || [ "$FRAMEWORK" = "laravel" ] || [ "$FRAMEWORK" = "magento" ] || [ "$FRAMEWORK" = "wordpress" ]; then
timeout=180
fi
local max_attempts=$((timeout / 5))
[ "$max_attempts" -gt 5 ] && max_attempts=5
local sleep_time=$((timeout / max_attempts))
if [[ "$IS_WORKER" -eq 1 ]]; then
return 0
fi
log_info "Performing health check for $container_name (timeout: ${timeout}s)"
for ((i=1; i<=max_attempts; i++)); do
# Lấy mã trạng thái HTTP từ endpoint /health
health_status=$(docker exec "$container_name" curl -s -o /dev/null -w "%{http_code}" http://localhost:${CONTAINER_PORT})
# Kiểm tra nếu mã trạng thái là 200 hoặc 404
if [[ "$health_status" -eq 200 || "$health_status" -eq 404 ]]; then
log_success "Health check passed for $container_name on /health with status $health_status (attempt $i/$max_attempts)"
return 0
fi
if [[ $i -lt $max_attempts ]]; then
# Cung cấp thêm thông tin về lỗi để dễ debug
log_info "Health check attempt $i/$max_attempts failed (statuses: $health_status), retrying in ${sleep_time}s..."
sleep $sleep_time
fi
done
log_error "Kiểm tra trạng thái máy chủ ở container $container_name thất bại sau $max_attempts lần thử"
#local docker_logs=`docker logs --tail 10 "$container_name"`
#echo $docker_logs
#log_error $docker_logs
return 1
}
# Get current image version
get_current_version() {
# Tìm container đang chạy có prefix
local container_name
container_name=$(docker ps --filter "name=^${CONTAINER_PREFIX}" --format '{{ "{{" }}.Names{{ "}}" }}' | head -n 1)
if [ -n "$container_name" ]; then
docker inspect "$container_name" --format '{{ "{{" }}index .Config.Labels "version"{{ "}}" }}' 2>/dev/null || echo "unknown"
else
echo "none"
fi
}
# Get available versions
get_available_versions() {
docker images "${DOCKER_IMAGE}" --format "{{ "{{" }}.Tag{{ "}}" }}" | grep -v "latest" | sort -V -r | head -10
}
# Backup current configuration
backup_config() {
local backup_file="$BACKUP_DIR/config-$(date +%Y%m%d-%H%M%S).tar.gz"
log_info "Creating configuration backup: $backup_file"
if [[ -d "$CONFIG_DIR" ]] && [[ -n "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]]; then
tar -czf "$backup_file" -C "$CONFIG_DIR" . 2>/dev/null
log_success "Configuration backed up to: $backup_file"
# Keep only last 10 backups
ls -t "$BACKUP_DIR"/config-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f
else
log_info "No configuration files to backup"
fi
}
# Create Docker network if it doesn't exist
ensure_network() {
if ! docker network ls | grep -q "$DOCKER_NETWORK"; then
log_info "Creating Docker network: $DOCKER_NETWORK"
docker network create "$DOCKER_NETWORK" || {
log_info "Network creation failed or network already exists"
}
fi
}
# Show port mapping information
show_port_mapping() {
local replicas=${1:-$DEFAULT_REPLICAS}
echo "🔌 PORT MAPPING PREVIEW"
echo "======================"
echo "Base Port: $BASE_PORT"
echo "Pattern: ${base_port}${replica_number:-1}"
echo ""
echo "Replica Mapping:"
for ((i=1; i<=replicas; i++)); do
local container_name="${CONTAINER_PREFIX}-${i}"
local port=$(get_replica_port "$BASE_PORT" "$i")
printf " %-25s → localhost:%-6s → container:%-6s\n" "$container_name" "$port" "$CONTAINER_PORT"
done
echo ""
}
# Start a single container
start_single_container() {
local container_name=$1
local image_tag=${2:-$CURRENT_TAG}
local port=${3:-""}
if [[ "$IS_WORKER" -ne 1 ]]; then
# Get port if not provided
if [[ -z "$port" ]]; then
local replica_number="1"
if [[ "$container_name" =~ -([0-9]+)$ ]]; then
replica_number="${BASH_REMATCH[1]}"
fi
# Use base port + replica number pattern (e.g., 3003 + 1 = 30031)
port=$(get_replica_port "$BASE_PORT" "$replica_number")
fi
# Check if calculated port is available
if ! is_port_available "$port"; then
log_warning "Calculated port $port is busy, finding alternative..."
port=$(get_random_port)
BINDING_PORT=$port
fi
fi
log_info "Starting container: $container_name with image ${DOCKER_IMAGE}:${image_tag} on port $port"
ensure_network
# Build docker run command
local docker_cmd="docker run -d --ulimit nofile=65535:65535 -e TZ=Asia/Ho_Chi_Minh"
docker_cmd+=" --name $container_name"
if [[ "$FRAMEWORK" == "nextjs" && "$container_name" != *rolling* ]]; then
mkdir -p /var/cache/nginx/ 2>/dev/null
#create volume if not exists
local docker_volume="${APP_NAME}-${ENVIRONMENT}"
#local docker_volume="$container_name"
local docker_volume_cmd="docker volume inspect $docker_volume > /dev/null 2>&1 || docker volume create $docker_volume"
eval "$docker_volume_cmd"
docker_cmd+=" --mount type=volume,source=$docker_volume,target=/app/.next/cache/images"
elif [[ "$APP_NAME" == "kamgift-cms" ]]; then
mkdir -p /var/cache/nginx/ 2>/dev/null
# Create volume for uploads
local docker_volume_uploads="${APP_NAME}-${ENVIRONMENT}"
local docker_volume_cmd_uploads="docker volume inspect $docker_volume_uploads > /dev/null 2>&1 || docker volume create $docker_volume_uploads"
eval "$docker_volume_cmd_uploads"
# Create volume for storage
local docker_volume_storage="${APP_NAME}-${ENVIRONMENT}-storage"
local docker_volume_cmd_storage="docker volume inspect $docker_volume_storage > /dev/null 2>&1 || docker volume create $docker_volume_storage"
eval "$docker_volume_cmd_storage"
docker_cmd+=" --mount type=volume,source=$docker_volume_uploads,target=/var/www/html/public/cms/backend/uploads"
docker_cmd+=" --mount type=volume,source=$docker_volume_storage,target=/var/www/html/storage"
elif [[ "$FRAMEWORK" == "laravel" ]]; then
mkdir -p /var/cache/nginx/ 2>/dev/null
#create volume if not exists
local docker_volume="${APP_NAME}-${ENVIRONMENT}"
#local docker_volume="$container_name"
local docker_volume_cmd="docker volume inspect $docker_volume > /dev/null 2>&1 || docker volume create $docker_volume"
eval "$docker_volume_cmd"
docker_cmd+=" --mount type=volume,source=$docker_volume,target=/var/www/html/storage"
fi
docker_cmd+=" --network $DOCKER_NETWORK"
docker_cmd+=" --restart $RESTART_POLICY"
# Add environment file if exists
if [[ -f "$CONFIG_DIR/.env" ]]; then
if [[ "$FRAMEWORK" == "laravel" ]]; then
docker_cmd+=" --env-file $CONFIG_DIR/.env"
fi
# docker_cmd+=" --env-file $CONFIG_DIR/.env"
log_info "Using environment file: $CONFIG_DIR/.env"
else
log_warning "No .env file found in $CONFIG_DIR"
fi
# Add resource limits
if [[ -n "$MEMORY_LIMIT" ]]; then
docker_cmd+=" --memory $MEMORY_LIMIT"
fi
if [[ -n "$CPU_LIMIT" ]]; then
docker_cmd+=" --cpus $CPU_LIMIT"
fi
# Add labels
docker_cmd+=" --label app=$APP_NAME"
docker_cmd+=" --label environment=$ENVIRONMENT"
docker_cmd+=" --label version=$image_tag"
# Add health check
if [[ "$IS_WORKER" -ne 1 ]]; then
docker_cmd+=" --health-cmd=\"curl -s -o /dev/null -w '%{http_code}' http://localhost:${CONTAINER_PORT}/ | grep -qE '^(200|404)$' || exit 1\""
docker_cmd+=" --health-interval=10s"
docker_cmd+=" --health-timeout=10s"
docker_cmd+=" --health-start-period=10s"
docker_cmd+=" --health-retries=3"
fi
local docker_cmd_eval=$docker_cmd
if [[ "$IS_WORKER" -ne 1 ]]; then
docker_cmd_eval+=" -p ${port}:${CONTAINER_PORT}"
fi
docker_cmd_eval+=" ${DOCKER_IMAGE}:${image_tag}"
#log_info "$docker_cmd_eval"
# Stop old container (frees up original port)
if [[ "$IS_WORKER" -eq 1 ]]; then
container_name_original="${container_name%-*}"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name_original}$"; then
docker stop "$container_name_original" &>/dev/null || true
docker rm "$container_name_original" &>/dev/null || true
fi
fi
# Execute command
if eval "$docker_cmd_eval"; then
log_success "Container started: $container_name on port $port"
BINDING_PORT=0
return 0
else
log_error "Failed to start container: $container_name"
# Check if it's a port conflict error and retry with random port
if docker logs "$container_name" 2>&1 | grep -q "address already in use"; then
log_warning "Port conflict detected, removing failed container and retrying with random port..."
docker rm -f "$container_name" &>/dev/null || true
# Retry up to 3 times with random ports
local retry_count=0
local max_retries=1
while [[ $retry_count -lt $max_retries ]]; do
retry_count=$((retry_count + 1))
local random_port=$(get_random_port)
log_info "Retry $retry_count/$max_retries with random port: $random_port"
local docker_cmd_eval=$docker_cmd
if [[ "$IS_WORKER" -ne 1 ]]; then
docker_cmd_eval+=" -p ${random_port}:${CONTAINER_PORT}"
fi
docker_cmd_eval+=" ${DOCKER_IMAGE}:${image_tag}"
if eval "$docker_cmd_eval"; then
log_success "Container started on retry with port $random_port: $container_name"
BINDING_PORT=$random_port
return 0
else
log_warning "Retry $retry_count failed, removing container..."
docker rm -f "$container_name" &>/dev/null || true
fi
done
log_error "All retry attempts failed for container: $container_name"
fi
BINDING_PORT=0
return 1
fi
}
# Smart deployment with auto-scaling
deploy_with_auto_scale() {
local target_replicas=${1:-$DEFAULT_REPLICAS}
local image_tag=${2:-$CURRENT_TAG}
local strategy=${3:-"rolling"}
local is_restart=${4:-"0"}
local restart_container=${5:-}
log ""
log_info "🚀 Starting smart deployment with auto-scaling"
log_info "Target: $target_replicas replicas, Version: $image_tag, Strategy: $strategy"
# Get current state
local current_count
current_count=$(get_container_count)
local current_version
current_version=$(get_current_version)
log_info "Current state: $current_count replicas running version $current_version"
if [[ "$is_restart" != "1" ]]; then
# Login to registry if token available
if [[ -n "{{ gitlab_ci_token | default('') }}" ]]; then
log_info "Logging into GitLab Container Registry..."
echo "{{ gitlab_ci_token }}" | docker login gitlab.forlife.one:5050 -u gitlab-ci-token --password-stdin >/dev/null 2>&1
fi
# Pull new image first
log_info "Pulling new image: ${DOCKER_IMAGE}:${image_tag}"
if ! docker pull "${DOCKER_IMAGE}:${image_tag}"; then
log_error "Failed to pull image: ${DOCKER_IMAGE}:${image_tag}"
rm -f "$CONTAINER_LOCK_FILE"
return 1
fi
fi
# Determine deployment strategy
if [[ $current_count -eq 0 ]]; then
# Fresh deployment
log_info "Fresh deployment: starting $target_replicas containers"
docker_start "$target_replicas" "$image_tag"
elif [[ "$current_version" != "$image_tag" || "$is_restart" == "1" ]]; then
# Version update with scaling
log_info "Version update: $current_version → $image_tag with scaling to $target_replicas"
case "$strategy" in
"rolling")
deploy_rolling_with_scale "$target_replicas" "$image_tag" "$is_restart" "$restart_container"
;;
"blue-green")
deploy_blue_green_with_scale "$target_replicas" "$image_tag"
;;
"recreate")
deploy_recreate_with_scale "$target_replicas" "$image_tag"
;;
*)
log_error "Unknown deployment strategy: $strategy"
rm -f "$CONTAINER_LOCK_FILE"
return 1
;;
esac
elif [[ $current_count -ne $target_replicas ]]; then
# Only scaling needed (same version)
log_info "Scaling only: $current_count → $target_replicas replicas (same version)"
docker_scale "$target_replicas"
else
deploy_recreate_with_scale "$target_replicas" "$image_tag"
# No changes needed
# log_success "No deployment needed: $current_count replicas already running version $image_tag"
# return 0
fi
# Verify final state
local final_count
final_count=$(get_container_count)
local final_version
final_version=$(get_current_version)
#if [[ $final_count -eq $target_replicas ]] && [[ "$final_version" == "$image_tag" ]]; then
if [[ "$final_version" == "$image_tag" ]]; then
log_success "✅ Deployment completed successfully: $final_count replicas running $final_version"
if [[ "$is_restart" != "1" ]]; then
# Show final status
echo ""
docker_status
fi
rm -f "$CONTAINER_LOCK_FILE"
return 0
else
rm -f "$CONTAINER_LOCK_FILE"
log_error "❌ Deployment verification failed: expected $target_replicas/$image_tag, got $final_count/$final_version"
return 1
fi
}
# Rolling deployment with scaling
deploy_rolling_with_scale_old() {
local target_replicas=$1
local image_tag=$2
local is_restart=${3:-"0"}
local restart_container=${4:-}
local current_count
current_count=$(get_container_count)
LOCK_DIR="/tmp/container_restart_locks" && mkdir -p "$LOCK_DIR"
log_info "Rolling deployment with scaling: $current_count → $target_replicas replicas"
# Phase 1: Update existing containers with zero-downtime
local existing_containers
existing_containers=$(get_running_containers)
if [[ -n "$existing_containers" ]]; then
log_info "Phase 1: Rolling update of existing containers"
local container_rolling_checked="?"
echo "$existing_containers" | while read -r container; do
if [ "$is_restart" = "1" ]; then
container_rolling_checked="OK"
if [ "$container" != "$restart_container" ]; then
continue
fi
fi
local timestamp=$(date +%s)
# Get replica number from container name
local replica_num="1"
if [[ "$container" =~ -([0-9]+)$ ]]; then
replica_num="${BASH_REMATCH[1]}"
fi
# Get original port
local original_port
original_port=$(get_replica_port "$BASE_PORT" "$replica_num")
# Find next available port in rolling range (BASE_PORT + 2000 to BASE_PORT + 2999)
local rolling_port
rolling_port=$(get_next_available_port "$((BASE_PORT + 2000))")
if [[ "$IS_WORKER" -ne 1 ]]; then
if [[ -z "$rolling_port" ]]; then
log_error "Cannot find available rolling port for $container"
return 1
fi
fi
# Start new container with temp name on rolling port
local temp_name="${container}-rolling"
log_info "🔄 Rolling update: $container → $temp_name (port $rolling_port → $original_port)"
if [[ "$container_rolling_checked" == "OK" ]] || start_single_container "$temp_name" "$image_tag" "$rolling_port"; then
if [[ "$container_rolling_checked" == "OK" ]] || health_check "$temp_name"; then
log_info "✅ New container healthy, switching traffic..."
date +%s > "$LOCK_DIR/$container.lock"
# Stop old container (frees up original port)
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container}$"; then
docker stop "$container" &>/dev/null || true
docker rm "$container" &>/dev/null || true
fi
if [[ "$container_rolling_checked" != "OK" ]]; then
# Stop temp container
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${temp_name}$"; then
docker stop "$temp_name" &>/dev/null || true
docker rm "$temp_name" &>/dev/null || true
fi
fi
container_rolling_checked="OK"
# Start new container with original name and port
if start_single_container "$container" "$image_tag" "$original_port"; then
if health_check "$container"; then
rm -f "$LOCK_DIR/$container.lock"
log_success "✅ Rolling update completed: $container"
else
log_error "Health check failed for final container: $container"
return 1
fi
else
log_error "Failed to start final container: $container"
return 1
fi
else
log_error "Không thể bắt đầu container mới, giữ nguyên container phiên bản cũ !!!"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${temp_name}$"; then
docker stop "$temp_name" &>/dev/null || true
docker rm "$temp_name" &>/dev/null || true
fi
return 1
fi
else
log_error "Failed to start rolling container: $temp_name"
return 1
fi
log_info "⏳ Waiting before next container update..."
sleep 5 # Brief pause between updates
done
fi
# Phase 2: Scale up if needed
if [[ $target_replicas -gt $current_count ]]; then
log_info "Phase 2: Scaling up from $current_count to $target_replicas"
for ((i=$((current_count + 1)); i<=target_replicas; i++)); do
local container_name="${CONTAINER_PREFIX}-${i}"
local port
port=$(get_replica_port "$BASE_PORT" "$i")
if start_single_container "$container_name" "$image_tag" "$port"; then
if ! health_check "$container_name"; then
log_error "Health check failed for new container: $container_name"
return 1
fi
log_success "✅ Scaled up: $container_name on port $port"
else
return 1
fi
sleep 2
done
fi
# Phase 3: Scale down if needed
if [[ $target_replicas -lt $current_count ]]; then
log_info "Phase 3: Scaling down from $current_count to $target_replicas"
local scale_down_num=$((current_count - target_replicas))
mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}")
for container_name in "${containers[@]}"; do
if (( scale_down_num <= 0 )); then
log_info "No more containers to scale down"
break
fi
oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs)
sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE
nginx -t && /etc/init.d/nginx reload
scale_down_num=$((scale_down_num - 1))
log_info "Removing container: $container_name"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then
docker stop "$container_name" &>/dev/null || true
docker rm "$container_name" &>/dev/null || true
fi
done
fi
log_success "🎉 Rolling deployment with scaling completed successfully"
}
deploy_rolling_with_scale() {
local target_replicas=$1
local image_tag=$2
local is_restart=${3:-"0"}
local restart_container=${4:-}
local current_count
current_count=$(get_container_count)
local current_version
current_version=$(get_current_version)
LOCK_DIR="/tmp/container_restart_locks" && mkdir -p "$LOCK_DIR"
log_info "Rolling deployment with scaling: $current_count → $target_replicas replicas"
# Scale down
if [[ $target_replicas -lt $current_count ]]; then
log_info "Scaling down from $current_count to $target_replicas"
local scale_down_num=$((current_count - target_replicas))
mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}")
for container_name in "${containers[@]}"; do
if (( scale_down_num <= 0 )); then
log_info "No more containers to scale down"
break
fi
oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs)
sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE
nginx -t && /etc/init.d/nginx reload
scale_down_num=$((scale_down_num - 1))
log_info "Removing container: $container_name"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then
docker stop "$container_name" &>/dev/null || true
docker rm "$container_name" &>/dev/null || true
fi
done
fi
# Update existing containers with zero-downtime
local existing_containers
existing_containers=$(get_running_containers)
local existing_updated_count=0
if [[ -n "$existing_containers" ]]; then
log_info "Rolling update of existing containers"
local container_rolling_checked="?"
echo "$existing_containers" | while read -r container; do
existing_updated_count=$((existing_updated_count + 1))
if [[ $existing_updated_count -gt $target_replicas ]]; then
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container}$"; then
docker stop "$container" &>/dev/null || true
docker rm "$container" &>/dev/null || true
fi
continue
fi
local container_old="$container"
container="$(remove_timestamp "$container_old")"
if [ "$is_restart" = "1" ]; then
container_rolling_checked="OK"
if [ "$container_old" != "$restart_container" ]; then
continue
fi
fi
# Get old port trước khi tạo container mới
local oldport=""
# if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then
# oldport=$(docker port "$container_old" 3000/tcp 2>/dev/null | awk -F: '{print $2}' | xargs)
# fi
local timestamp=$(date +%s)
local replica_num="1"
if [[ "$container" =~ -([0-9]+)$ ]]; then
replica_num="${BASH_REMATCH[1]}"
fi
# local original_port
# original_port=$(get_replica_port "$BASE_PORT" "$replica_num")
# local rolling_port
# rolling_port=$(get_next_available_port "$((BASE_PORT + 2000))")
local random_port=$(get_random_port)
if [[ "$IS_WORKER" -ne 1 ]]; then
if [[ -z "$random_port" ]]; then
log_error "Cannot find available rolling port for $container"
return 1
fi
fi
local container_new="${container}-${random_port}"
date +%s > "$LOCK_DIR/$container.lock"
log_info "🔄 Rolling update: $container → $container_new (new port: $random_port)"
if start_single_container "$container_new" "$image_tag" "$random_port"; then
if [[ "$BINDING_PORT" -ne 0 ]]; then
random_port=${BINDING_PORT}
fi
if [[ "$IS_WORKER" -ne 1 ]]; then
if health_check "$container_new"; then
oldport=$(docker port $container_old 3000/tcp | awk -F: '{print $2}' | xargs)
newport=${random_port}
log_info "✅ New container healthy, switching traffic..."
# Stop old container (frees up original port)
if [[ -n "$oldport" ]]; then
sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE
fi
sed -i "\|server localhost:|a\ server localhost:${newport};" $NGINX_CONF_FILE
nginx -t && /etc/init.d/nginx reload
# remove old container
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then
docker stop "$container_old" &>/dev/null || true
docker rm "$container_old" &>/dev/null || true
fi
log_success "✅ Rolling update completed: $container"
else
log_error "Health check failed for final container: $container"
return 1
fi
else
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_old}$"; then
docker stop "$container_old" &>/dev/null || true
docker rm "$container_old" &>/dev/null || true
fi
fi
else
rm -f "$LOCK_DIR/$container.lock"
log_error "Không thể bắt đầu container mới, giữ nguyên container phiên bản cũ !!!"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_new}$"; then
docker stop "$container_new" &>/dev/null || true
docker rm "$container_new" &>/dev/null || true
fi
return 1
fi
log_info "⏳ Waiting before next container update..."
sleep 2
done
fi
# Scale up
if [[ $target_replicas -gt $current_count ]]; then
log_info "Scaling up from $current_count to $target_replicas"
i=$((current_count + 1))
while [[ $i -le $target_replicas ]]; do
local port
port=$(get_random_port)
local timestamp
timestamp=$(date +%s)
local container="${CONTAINER_PREFIX}-${i}"
local container_name="${container}-${port}"
date +%s > "$LOCK_DIR/$container.lock"
if start_single_container "$container_name" "$image_tag" "$port"; then
if ! health_check "$container_name"; then
log_error "Health check failed for new container: $container_name"
return 1
fi
log_success "✅ Scaled up: $container_name on port $port"
else
return 1
fi
sleep 2
i=$((i + 1)) # tự tăng i
done
fi
log_success "🎉 Rolling deployment with scaling completed successfully"
}
# Blue-green deployment with scaling
deploy_blue_green_with_scale() {
local target_replicas=$1
local image_tag=$2
log_info "Blue-green deployment with scaling: $target_replicas replicas"
# Create green environment
local green_containers=()
log_info "Creating green environment with $target_replicas containers"
for ((i=1; i<=target_replicas; i++)); do
local container_name="${CONTAINER_PREFIX}-green-${i}"
local port
port=$(get_replica_port "$((BASE_PORT + 1000))" "$i") # Use different port range for green
if start_single_container "$container_name" "$image_tag" "$port"; then
if health_check "$container_name"; then
green_containers+=("$container_name")
log_success "✅ Green container ready: $container_name on port $port"
else
log_error "Health check failed for green container: $container_name"
# Cleanup green containers
for green_container in "${green_containers[@]}"; do
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${green_container}$"; then
docker stop "$green_container" || true
docker rm "$green_container" || true
fi
done
return 1
fi
else
return 1
fi
done
# Switch traffic (rename containers)
log_info "Switching traffic to green environment"
# Stop and remove all blue containers
local blue_containers
blue_containers=$(get_running_containers)
if [[ -n "$blue_containers" ]]; then
echo "$blue_containers" | while read -r blue_container; do
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${blue_container}$"; then
docker stop "$blue_container" || true
docker rm "$blue_container" || true
fi
done
fi
# Rename green containers to blue
for ((i=1; i<=target_replicas; i++)); do
local green_name="${CONTAINER_PREFIX}-green-${i}"
local blue_name="${CONTAINER_PREFIX}-${i}"
local original_port
original_port=$(get_replica_port "$BASE_PORT" "$i")
# Stop green container and restart with correct port
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${green_name}$"; then
docker stop "$green_name" || true
docker rm "$green_name" || true
fi
# Start with final name and port
start_single_container "$blue_name" "$image_tag" "$original_port"
done
log_success "Blue-green deployment with scaling completed"
}
# Recreate deployment with scaling
deploy_recreate_with_scale() {
local target_replicas=$1
local image_tag=$2
log_info "Recreate deployment with scaling: $target_replicas replicas"
# Stop all existing containers
docker_stop
# Remove all containers
local containers
containers=$(get_app_containers)
if [[ -n "$containers" ]]; then
echo "$containers" | while read -r container; do
docker rm "$container" &>/dev/null || true
done
fi
# Start new containers
docker_start "$target_replicas" "$image_tag"
log_success "Recreate deployment with scaling completed"
}
function docker_start() {
local replicas=${1:-$DEFAULT_REPLICAS}
local image_tag=${2:-$CURRENT_TAG}
log_info "Starting $replicas containers with image tag: $image_tag"
for ((i=1; i<=replicas; i++)); do
# local port=$(get_replica_port "$BASE_PORT" "$i")
local port=$(get_random_port)
local container_name="${CONTAINER_PREFIX}-${i}-${port}"
# check_and_free_port $port $container_name
if start_single_container "$container_name" "$image_tag" "$port"; then
echo ""
fi;
sleep 5
done
log_success "All containers started successfully"
}
function docker_stop() {
local containers
containers=$(get_running_containers)
if [[ -z "$containers" ]]; then
log_info "No running containers to stop"
return 0
fi
log_info "Stopping containers..."
echo "$containers" | while read -r container; do
log_info "Stopping container: $container"
docker stop "$container" || log_warning "Failed to stop container: $container"
done
log_success "All containers stopped"
}
function docker_restart() {
local replicas=${1:-$(get_container_count)}
local image_tag=${2:-$(get_current_version)}
local strategy=${3:-"rolling"}
local restart_container=${4:-}
if [[ $replicas -eq 0 ]]; then
replicas=$DEFAULT_REPLICAS
fi
if [[ $replicas -gt $DEFAULT_REPLICAS ]]; then
replicas=$DEFAULT_REPLICAS
fi
if [[ "$image_tag" == "unknown" || "$image_tag" == "none" ]]; then
image_tag=$CURRENT_TAG
log_info "Using current deployment tag: $image_tag"
fi
log_info "Restarting with $replicas replicas, image tag: $image_tag, strategy: $strategy"
# Use smart deployment with auto-scaling
if [[ "$restart_container" != "" ]]; then
deploy_with_auto_scale "$replicas" "$image_tag" "$strategy" "1" "$restart_container"
else
deploy_with_auto_scale "$replicas" "$image_tag" "$strategy" "1"
fi
}
function docker_restart_single() {
local restart_container=${1:-}
local replicas=${2:-$(get_container_count)}
local image_tag=${3:-$(get_current_version)}
local strategy=${4:-"rolling"}
# Use smart deployment with auto-scaling
docker_restart "$replicas" "$image_tag" "$strategy" "$restart_container"
#docker restart "$restart_container"
}
function docker_scale() {
local target_replicas=$1
if [[ -z "$target_replicas" || ! "$target_replicas" =~ ^[0-9]+$ ]]; then
log_error "Usage: scale <number_of_replicas>"
return 1
fi
if [[ $target_replicas -gt $MAX_REPLICAS ]]; then
log_error "Cannot scale beyond maximum replicas: $MAX_REPLICAS"
return 1
fi
local current_count
current_count=$(get_container_count)
log_info "Scaling from $current_count to $target_replicas replicas"
if [[ $target_replicas -eq $current_count ]]; then
log_info "Already at target replica count: $target_replicas"
return 0
fi
local current_version
current_version=$(get_current_version)
if [[ "$current_version" == "none" && $target_replicas -gt 0 ]]; then
log_error "No containers running, use 'start' command instead"
return 1
fi
if [[ $target_replicas -gt $current_count ]]; then
# Scale up
log_info "Scaling up from $current_count to $target_replicas"
for ((i=$((current_count + 1)); i<=target_replicas; i++)); do
local port=$(get_random_port)
local timestamp=$(date +%s)
local container="${CONTAINER_PREFIX}-${i}"
local container_name="${container}-${port}"
date +%s > "$LOCK_DIR/$container.lock"
if start_single_container "$container_name" "$current_version" "$port"; then
if ! health_check "$container_name"; then
log_error "Health check failed for new container: $container_name"
return 1
fi
log_success "Container $container_name started on port $port"
else
return 1
fi
done
else
# Scale down
log_info "Scaling down from $current_count to $target_replicas"
local scale_down_num=$((current_count - target_replicas))
mapfile -t containers < <(docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep "^${CONTAINER_PREFIX}")
for container_name in "${containers[@]}"; do
if (( scale_down_num <= 0 )); then
log_info "No more containers to scale down"
break
fi
oldport=$(docker port $container_name 3000/tcp | awk -F: '{print $2}' | xargs)
sed -i "\|server localhost:${oldport}|d" $NGINX_CONF_FILE
nginx -t && /etc/init.d/nginx reload
scale_down_num=$((scale_down_num - 1))
log_info "Removing container: $container_name"
if docker ps -a --format '{% raw %}{{.Names}}{% endraw %}' | grep -q "^${container_name}$"; then
docker stop "$container_name" &>/dev/null || true
docker rm "$container_name" &>/dev/null || true
fi
done
fi
log_success "Scaling completed: $(get_container_count) replicas running"
}
function docker_rollback() {
local target_version=${1:-""}
local replicas=${2:-$(get_container_count)}
if [[ $replicas -eq 0 ]]; then
replicas=$DEFAULT_REPLICAS
fi
log_info "Rolling back application (replicas: $replicas)"
if [[ -n "$target_version" ]]; then
log_info "Rolling back to specific version: $target_version"
# Check if version exists
if ! docker images "${DOCKER_IMAGE}:${target_version}" --quiet | head -1 &>/dev/null; then
log_info "Pulling rollback image: ${DOCKER_IMAGE}:${target_version}"
if ! docker pull "${DOCKER_IMAGE}:${target_version}"; then
log_error "Failed to pull rollback image: ${DOCKER_IMAGE}:${target_version}"
return 1
fi
fi
else
# Find previous version
local available_versions
available_versions=$(get_available_versions)
if [[ -z "$available_versions" ]]; then
log_error "No previous versions available for rollback"
return 1
fi
# Get the second version (first is current)
target_version=$(echo "$available_versions" | sed -n '2p')
if [[ -z "$target_version" ]]; then
target_version=$(echo "$available_versions" | head -1)
log_info "Only one version available, using: $target_version"
else
log_info "Rolling back to previous version: $target_version"
fi
fi
# Create backup before rollback
backup_config
# Perform rollback
deploy_with_auto_scale "$replicas" "$target_version" "rolling"
if [[ $? -eq 0 ]]; then
log_success "Rollback completed to version: $target_version"
else
log_error "Rollback failed"
return 1
fi
}
function docker_status() {
local containers
containers=$(get_app_containers)
echo "📊 APPLICATION STATUS"
echo "====================="
echo "App: $APP_NAME"
echo "Environment: $ENVIRONMENT"
echo "Current Version: $(get_current_version)"
echo "Running Containers: $(get_container_count)"
echo "Base Port: $BASE_PORT (Pattern: ${BASE_PORT}${replica_number:-1})"
echo "Container Port: $CONTAINER_PORT"
echo ""
if [[ -z "$containers" ]]; then
echo "❌ No containers found"
echo ""
echo "📋 PORT MAPPING PREVIEW (if containers were running):"
show_port_mapping 3
return 0
fi
echo "📋 CONTAINER DETAILS"
echo "===================="
printf "%-25s %-15s %-20s %-10s %-15s\n" "NAME" "STATUS" "VERSION" "PORT" "HEALTH"
echo "$(printf '%.0s-' {1..95})"
echo "$containers" | while read -r container; do
local status
status=$(docker inspect "$container" --format '{{ "{{" }}.State.Status{{ "}}" }}' 2>/dev/null || echo "unknown")
local version
version=$(docker inspect "$container" --format '{{ "{{" }}index .Config.Labels "version"{{ "}}" }}' 2>/dev/null || echo "unknown")
local port
port=$(docker port "$container" 2>/dev/null | grep "${CONTAINER_PORT}/tcp" | cut -d':' -f2 || echo "none")
local health="unknown"
if [[ "$status" == "running" ]]; then
if health_check "$container" 5; then
health="healthy"
else
health="unhealthy"
fi
fi
printf "%-25s %-15s %-20s %-10s %-15s\n" "$container" "$status" "$version" "$port" "$health"
done
echo ""
echo "📡 LOAD BALANCER TARGETS"
echo "========================"
echo "$containers" | while read -r container; do
local port
port=$(docker port "$container" 2>/dev/null | grep "${CONTAINER_PORT}/tcp" | cut -d':' -f2 || echo "none")
if [[ "$port" != "none" ]]; then
echo " server localhost:$port;"
fi
done
}
function docker_logs() {
local lines=${1:-$DEFAULT_LOG_LINES}
local follow=${2:-false}
local containers
containers=$(get_running_containers)
if [[ -z "$containers" ]]; then
log_error "No running containers found"
return 1
fi
log_info "Viewing logs (last $lines lines, follow: $follow)"
if [[ "$follow" == "true" ]]; then
# Follow logs from all containers
echo "$containers" | xargs -I {} sh -c 'echo "=== {} ===" && docker logs -f --tail '"$lines"' {} &'
wait
else
# Show logs from all containers
echo "$containers" | while read -r container; do
echo "=== $container ==="
docker logs --tail "$lines" "$container" 2>&1
echo ""
done
fi
}
function docker_health() {
local containers
containers=$(get_running_containers)
if [[ -z "$containers" ]]; then
log_error "No running containers found"
return 1
fi
local all_healthy=true
echo "$containers" | while read -r container; do
if health_check "$container"; then
log_success "Health check passed: $container"
else
log_error "Health check failed: $container"
all_healthy=false
fi
done
if [[ "$all_healthy" == "true" ]]; then
log_success "All containers are healthy"
return 0
else
log_error "Some containers are unhealthy"
return 1
fi
}
function docker_clean() {
echo "Cleaning up Docker images and containers..."
# Remove stopped containers
docker container prune -f
# Remove unused images
docker image prune -a -f
# Xóa volume không còn dùng
docker volume prune -f
# Xóa network không còn dùng
docker network prune -f
# delete unused all
#docker system prune -a --volumes -f
# Remove old versions (keep last 5)
local old_images
old_images=$(docker images "${DOCKER_IMAGE}" --format "{{ "{{" }}.Tag{{ "}}" }}" | grep -v "latest" | sort -V -r | tail -n +6)
if [[ -n "$old_images" ]]; then
echo "$old_images" | while read -r tag; do
log_info "Removing old image: ${DOCKER_IMAGE}:${tag}"
docker rmi "${DOCKER_IMAGE}:${tag}" &>/dev/null || true
done
fi
log_success "Cleanup completed"
}
function docker_info() {
echo "🔧 DOCKER CONTAINER INFORMATION"
echo "==============================="
echo "- Container Base Name: $CONTAINER_PREFIX"
echo "- Image: $DOCKER_IMAGE"
echo "- Current Tag: $CURRENT_TAG"
echo "- Host Port Base: $BASE_PORT"
echo "- Port Pattern: ${BASE_PORT}${replica_number:-1}"
echo "- Container Port: $CONTAINER_PORT"
echo "- Memory Limit: ${MEMORY_LIMIT:-unlimited}"
echo "- CPU Limit: ${CPU_LIMIT:-unlimited}"
echo "- Environment: $ENVIRONMENT"
echo "- Docker Network: $DOCKER_NETWORK"
echo "- Default Replicas: $DEFAULT_REPLICAS"
echo "- Max Replicas: $MAX_REPLICAS"
echo "- Docker Version: $(docker --version)"
echo ""
# Show port mapping preview
show_port_mapping $DEFAULT_REPLICAS
echo "📋 AVAILABLE VERSIONS"
echo "===================="
local versions
versions=$(get_available_versions)
if [[ -n "$versions" ]]; then
echo "$versions"
else
echo "No versions found"
fi
echo ""
echo "🌐 NETWORK INFORMATION"
echo "====================="
if docker network ls | grep -q "$DOCKER_NETWORK"; then
docker network inspect "$DOCKER_NETWORK" --format '{{ "{{" }}.Name{{ "}}" }}: {{ "{{" }}.Driver{{ "}}" }} ({{ "{{" }}.Scope{{ "}}" }})'
else
echo "Network $DOCKER_NETWORK not found"
fi
echo ""
docker_status
}
function show_help() {
cat << EOF
🚀 Enhanced Docker Management Script with Auto-Scaling
======================================================
App: $APP_NAME
Environment: $ENVIRONMENT
Port Pattern: ${BASE_PORT}${replica_number:-1}
USAGE:
$0 [COMMAND] [OPTIONS]
DEPLOYMENT COMMANDS:
deploy [replicas] [version] [strategy]
Smart deployment with auto-scaling (recommended for CI/CD)
- replicas: Target number of replicas (default: $DEFAULT_REPLICAS)
- version: Docker image version (default: $CURRENT_TAG)
- strategy: rolling|blue-green|recreate (default: rolling)
start [replicas] [version] [strategy]
Alias for deploy command
restart [replicas] [version] [strategy]
Restart with smart deployment and auto-scaling
restart_single [container_name]
Restart with smart deployment and auto-scaling
MANUAL OPERATIONS:
stop
Stop all running containers
scale <replicas>
Manual scale to specified number of replicas (max: $MAX_REPLICAS)
rollback [version] [replicas]
Rollback to previous or specific version
MONITORING COMMANDS:
status
Show application status and health
ports [replicas]
Show port mapping preview for specified replicas
logs [lines] [follow]
View container logs
archive-logs
Archive running container logs to timestamped files organized by prefix
(Usually called automatically by cron job every 15 minutes)
health
Perform health check on all containers
clean
Remove old images and containers
info
Show detailed container and environment information
help
Show this help message
PORT ALLOCATION:
Base Port: $BASE_PORT
Pattern: ${base_port}${replica_number:-1}
Examples:
- 1 replica: ${BASE_PORT}1 ($(get_replica_port "$BASE_PORT" "1"))
- 2 replicas: ${BASE_PORT}1, ${BASE_PORT}2 ($(get_replica_port "$BASE_PORT" "1"), $(get_replica_port "$BASE_PORT" "2"))
- 3 replicas: ${BASE_PORT}1, ${BASE_PORT}2, ${BASE_PORT}3 ($(get_replica_port "$BASE_PORT" "1"), $(get_replica_port "$BASE_PORT" "2"), $(get_replica_port "$BASE_PORT" "3"))
EXAMPLES:
$0 deploy 5 v1.2.0 rolling
$0 scale 8
$0 ports 4
$0 rollback v1.1.0 2
$0 logs 100 true
EOF
}
# ============================================================================
# MAIN SCRIPT EXECUTION
# ============================================================================
function docker_manage() {
case "$1" in
deploy|start)
local replicas=${2:-$DEFAULT_REPLICAS}
local image_tag=${3:-$CURRENT_TAG}
local strategy=${4:-"rolling"}
date +%s > "$CONTAINER_LOCK_FILE"
deploy_with_auto_scale "$replicas" "$image_tag" "$strategy"
;;
stop)
docker_stop
;;
restart)
local replicas=${2:-$(get_container_count)}
local image_tag=${3:-$(get_current_version)}
local strategy=${4:-"rolling"}
docker_restart "$replicas" "$image_tag" "$strategy"
;;
restart_single)
local restart_container=${2:-}
docker_restart_single "$restart_container"
;;
scale)
if [[ -z "$2" ]]; then
log_error "Usage: $0 scale <number_of_replicas>"
exit 1
fi
docker_scale "$2"
;;
rollback)
docker_rollback "$2" "$3"
;;
status)
docker_status
;;
ports)
local replicas=${2:-$DEFAULT_REPLICAS}
if [[ ! "$replicas" =~ ^[0-9]+$ ]] || [[ $replicas -lt 1 ]] || [[ $replicas -gt $MAX_REPLICAS ]]; then
log_error "Invalid replica count. Must be between 1 and $MAX_REPLICAS"
exit 1
fi
show_port_mapping "$replicas"
;;
deploy_active_ports)
show_active_ports
;;
logs)
docker_logs "$2" "$3"
;;
health)
docker_health
;;
shell)
local container_num=${2:-1}
local container_name="${CONTAINER_PREFIX}-${container_num}"
echo "Entering shell of $container_name container..."
if docker ps --filter "name=$container_name" --quiet | head -1 &>/dev/null; then
docker exec -it "$container_name" /bin/sh
else
log_error "Container $container_name is not running"
exit 1
fi
;;
clean)
docker_clean
;;
info)
docker_info
;;
archive-logs)
archive_running_container_logs
rotate_container_runtime_logs
;;
help|--help|-h)
show_help
;;
*)
echo "❌ Unknown command: $1"
echo ""
show_help
exit 1
;;
esac
}
# Execute main function with all arguments
docker_manage "$@"
exit $?