#!/bin/bash # TankStopp Docker Deployment Script # This script deploys the TankStopp application using Docker Compose set -e # Exit on any error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' NC='\033[0m' # No Color # Default values ENVIRONMENT="production" ACTION="deploy" SERVICE_NAME="tankstopp" COMPOSE_FILE="docker-compose.yml" COMPOSE_OVERRIDE="" PROJECT_NAME="tankstopp" BACKUP_BEFORE_DEPLOY=true WAIT_TIMEOUT=300 HEALTH_CHECK_RETRIES=10 ROLLBACK_ON_FAILURE=true SCALE_REPLICAS=1 # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" DATA_DIR="/var/lib/tankstopp" BACKUP_DIR="/var/lib/tankstopp/backups" LOG_DIR="/var/log/tankstopp" # Functions log_info() { echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1" } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1" } log_debug() { if [ "$DEBUG" = true ]; then echo -e "${PURPLE}[DEBUG]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1" fi } show_help() { cat << EOF TankStopp Docker Deployment Script Usage: $0 [ACTION] [OPTIONS] ACTIONS: deploy Deploy the application (default) start Start existing containers stop Stop running containers restart Restart containers update Update to latest images rollback Rollback to previous version status Show deployment status logs Show application logs backup Create database backup restore Restore from backup scale Scale service replicas cleanup Clean up unused resources OPTIONS: -h, --help Show this help message -e, --env ENV Environment: development|production|staging (default: production) -f, --file FILE Docker compose file (default: docker-compose.yml) -o, --override FILE Docker compose override file -p, --project NAME Project name (default: tankstopp) -s, --service NAME Service name (default: tankstopp) -r, --replicas COUNT Number of replicas for scaling (default: 1) -t, --timeout SECONDS Wait timeout in seconds (default: 300) --no-backup Skip backup before deployment --no-rollback Don't rollback on failure --force Force action without confirmation --debug Enable debug logging EXAMPLES: # Deploy to production $0 deploy --env production # Deploy with custom compose file $0 deploy --file docker-compose.prod.yml # Start development environment $0 start --env development # Scale production service $0 scale --replicas 3 # View logs $0 logs --service tankstopp # Backup database $0 backup # Rollback deployment $0 rollback ENVIRONMENT CONFIGURATIONS: development - Local development with debug enabled staging - Staging environment for testing production - Production deployment with optimizations EOF } check_requirements() { log_info "Checking deployment requirements..." # Check Docker if ! command -v docker &> /dev/null; then log_error "Docker is not installed or not in PATH" exit 1 fi # Check Docker Compose if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then log_error "Docker Compose is not installed or not in PATH" exit 1 fi # Set Docker Compose command if docker compose version &> /dev/null; then DOCKER_COMPOSE_CMD="docker compose" else DOCKER_COMPOSE_CMD="docker-compose" fi # Check Docker daemon if ! docker info &> /dev/null; then log_error "Docker daemon is not running" exit 1 fi # Check compose file if [ ! -f "$PROJECT_ROOT/$COMPOSE_FILE" ]; then log_error "Docker Compose file not found: $PROJECT_ROOT/$COMPOSE_FILE" exit 1 fi log_success "Requirements check passed" } setup_environment() { log_info "Setting up environment: $ENVIRONMENT" # Create necessary directories create_directories # Set compose files based on environment case "$ENVIRONMENT" in "development") COMPOSE_OVERRIDE="docker-compose.override.yml" ;; "staging") COMPOSE_OVERRIDE="docker-compose.staging.yml" ;; "production") COMPOSE_OVERRIDE="docker-compose.prod.yml" ;; esac # Build compose command COMPOSE_CMD="$DOCKER_COMPOSE_CMD -p $PROJECT_NAME -f $COMPOSE_FILE" if [ -n "$COMPOSE_OVERRIDE" ] && [ -f "$PROJECT_ROOT/$COMPOSE_OVERRIDE" ]; then COMPOSE_CMD="$COMPOSE_CMD -f $COMPOSE_OVERRIDE" log_info "Using override file: $COMPOSE_OVERRIDE" fi log_success "Environment setup completed" } create_directories() { log_info "Creating necessary directories..." # Create directories with proper permissions sudo mkdir -p "$DATA_DIR/data" "$BACKUP_DIR" "$LOG_DIR" sudo chown -R 1001:1001 "$DATA_DIR" 2>/dev/null || true sudo chmod -R 755 "$DATA_DIR" 2>/dev/null || true log_success "Directories created" } backup_database() { if [ "$BACKUP_BEFORE_DEPLOY" = false ]; then log_info "Skipping backup (disabled)" return 0 fi log_info "Creating database backup..." local backup_file="$BACKUP_DIR/fuel_stops_$(date +%Y%m%d_%H%M%S).db" # Check if database exists if [ ! -f "$DATA_DIR/data/fuel_stops.db" ]; then log_warning "Database file not found, skipping backup" return 0 fi # Create backup if cp "$DATA_DIR/data/fuel_stops.db" "$backup_file"; then log_success "Database backup created: $backup_file" # Keep only last 10 backups find "$BACKUP_DIR" -name "fuel_stops_*.db" -type f | sort -r | tail -n +11 | xargs rm -f 2>/dev/null || true else log_error "Failed to create database backup" return 1 fi } pull_images() { log_info "Pulling latest images..." cd "$PROJECT_ROOT" if $COMPOSE_CMD pull; then log_success "Images pulled successfully" else log_error "Failed to pull images" return 1 fi } deploy_application() { log_info "Deploying TankStopp application..." cd "$PROJECT_ROOT" # Backup database backup_database # Pull latest images pull_images # Deploy with docker-compose log_info "Starting containers..." if $COMPOSE_CMD up -d --remove-orphans; then log_success "Containers started successfully" else log_error "Failed to start containers" if [ "$ROLLBACK_ON_FAILURE" = true ]; then log_info "Attempting rollback..." rollback_deployment fi return 1 fi # Wait for services to be healthy wait_for_health # Run post-deployment checks post_deployment_checks log_success "Application deployed successfully" } wait_for_health() { log_info "Waiting for services to become healthy..." local retries=0 local max_retries=$HEALTH_CHECK_RETRIES while [ $retries -lt $max_retries ]; do if check_service_health; then log_success "All services are healthy" return 0 fi retries=$((retries + 1)) log_info "Health check attempt $retries/$max_retries..." sleep 10 done log_error "Services failed to become healthy within timeout" return 1 } check_service_health() { cd "$PROJECT_ROOT" # Check container status local unhealthy_containers=$($COMPOSE_CMD ps --filter "status=unhealthy" --format "{{.Service}}" 2>/dev/null | wc -l) local running_containers=$($COMPOSE_CMD ps --filter "status=running" --format "{{.Service}}" 2>/dev/null | wc -l) log_debug "Running containers: $running_containers, Unhealthy containers: $unhealthy_containers" if [ "$unhealthy_containers" -gt 0 ]; then log_warning "Found $unhealthy_containers unhealthy containers" return 1 fi if [ "$running_containers" -eq 0 ]; then log_warning "No running containers found" return 1 fi # Test HTTP endpoint if command -v curl &> /dev/null; then local port=$(get_service_port) if curl -sf "http://localhost:$port/" > /dev/null 2>&1; then return 0 else log_debug "HTTP health check failed" return 1 fi fi return 0 } get_service_port() { case "$ENVIRONMENT" in "development") echo "8081" ;; *) echo "8080" ;; esac } post_deployment_checks() { log_info "Running post-deployment checks..." cd "$PROJECT_ROOT" # Check container logs for errors local error_count=$($COMPOSE_CMD logs --tail=50 "$SERVICE_NAME" 2>/dev/null | grep -i "error\|panic\|fatal" | wc -l) if [ "$error_count" -gt 0 ]; then log_warning "Found $error_count error messages in logs" log_info "Recent errors:" $COMPOSE_CMD logs --tail=10 "$SERVICE_NAME" | grep -i "error\|panic\|fatal" || true fi # Check resource usage check_resource_usage log_success "Post-deployment checks completed" } check_resource_usage() { log_info "Checking resource usage..." cd "$PROJECT_ROOT" # Get container stats local stats=$($COMPOSE_CMD exec -T "$SERVICE_NAME" sh -c " echo 'Memory usage:' free -h | grep Mem echo 'Disk usage:' df -h /app/data echo 'CPU info:' uptime " 2>/dev/null || echo "Could not retrieve stats") log_debug "Resource stats: $stats" } start_services() { log_info "Starting services..." cd "$PROJECT_ROOT" if $COMPOSE_CMD start; then log_success "Services started successfully" wait_for_health else log_error "Failed to start services" return 1 fi } stop_services() { log_info "Stopping services..." cd "$PROJECT_ROOT" if $COMPOSE_CMD stop; then log_success "Services stopped successfully" else log_error "Failed to stop services" return 1 fi } restart_services() { log_info "Restarting services..." cd "$PROJECT_ROOT" if $COMPOSE_CMD restart; then log_success "Services restarted successfully" wait_for_health else log_error "Failed to restart services" return 1 fi } update_application() { log_info "Updating application..." backup_database pull_images cd "$PROJECT_ROOT" if $COMPOSE_CMD up -d --remove-orphans; then log_success "Application updated successfully" wait_for_health post_deployment_checks else log_error "Failed to update application" return 1 fi } rollback_deployment() { log_info "Rolling back deployment..." cd "$PROJECT_ROOT" # Stop current containers $COMPOSE_CMD down # Restore from latest backup local latest_backup=$(find "$BACKUP_DIR" -name "fuel_stops_*.db" -type f | sort -r | head -n 1) if [ -n "$latest_backup" ] && [ -f "$latest_backup" ]; then log_info "Restoring database from: $latest_backup" cp "$latest_backup" "$DATA_DIR/data/fuel_stops.db" log_success "Database restored" else log_warning "No backup found for rollback" fi # Start with previous configuration if $COMPOSE_CMD up -d; then log_success "Rollback completed successfully" else log_error "Rollback failed" return 1 fi } show_status() { log_info "Deployment status:" cd "$PROJECT_ROOT" echo "" echo "=== Container Status ===" $COMPOSE_CMD ps echo "" echo "=== Service Health ===" if check_service_health; then echo -e "${GREEN}✓ Services are healthy${NC}" else echo -e "${RED}✗ Services are unhealthy${NC}" fi echo "" echo "=== Resource Usage ===" check_resource_usage echo "" echo "=== Recent Logs ===" $COMPOSE_CMD logs --tail=5 "$SERVICE_NAME" } show_logs() { log_info "Showing application logs..." cd "$PROJECT_ROOT" $COMPOSE_CMD logs -f "$SERVICE_NAME" } scale_service() { log_info "Scaling service to $SCALE_REPLICAS replicas..." cd "$PROJECT_ROOT" if $COMPOSE_CMD up -d --scale "$SERVICE_NAME=$SCALE_REPLICAS"; then log_success "Service scaled to $SCALE_REPLICAS replicas" wait_for_health else log_error "Failed to scale service" return 1 fi } cleanup_resources() { log_info "Cleaning up unused resources..." # Remove unused containers docker container prune -f # Remove unused images docker image prune -f # Remove unused volumes (be careful!) if [ "$FORCE" = true ]; then docker volume prune -f fi # Remove unused networks docker network prune -f log_success "Cleanup completed" } restore_backup() { log_info "Available backups:" ls -la "$BACKUP_DIR"/fuel_stops_*.db 2>/dev/null || { log_error "No backups found in $BACKUP_DIR" return 1 } echo "" read -p "Enter backup filename to restore (or 'latest' for most recent): " backup_choice local backup_file if [ "$backup_choice" = "latest" ]; then backup_file=$(find "$BACKUP_DIR" -name "fuel_stops_*.db" -type f | sort -r | head -n 1) else backup_file="$BACKUP_DIR/$backup_choice" fi if [ ! -f "$backup_file" ]; then log_error "Backup file not found: $backup_file" return 1 fi log_info "Restoring from: $backup_file" # Stop services stop_services # Restore database cp "$backup_file" "$DATA_DIR/data/fuel_stops.db" # Start services start_services log_success "Database restored successfully" } main() { # Parse command line arguments while [[ $# -gt 0 ]]; do case $1 in deploy|start|stop|restart|update|rollback|status|logs|backup|restore|scale|cleanup) ACTION="$1" shift ;; -h|--help) show_help exit 0 ;; -e|--env) ENVIRONMENT="$2" shift 2 ;; -f|--file) COMPOSE_FILE="$2" shift 2 ;; -o|--override) COMPOSE_OVERRIDE="$2" shift 2 ;; -p|--project) PROJECT_NAME="$2" shift 2 ;; -s|--service) SERVICE_NAME="$2" shift 2 ;; -r|--replicas) SCALE_REPLICAS="$2" shift 2 ;; -t|--timeout) WAIT_TIMEOUT="$2" shift 2 ;; --no-backup) BACKUP_BEFORE_DEPLOY=false shift ;; --no-rollback) ROLLBACK_ON_FAILURE=false shift ;; --force) FORCE=true shift ;; --debug) DEBUG=true shift ;; *) log_error "Unknown option: $1" show_help exit 1 ;; esac done # Validate environment if [[ "$ENVIRONMENT" != "development" && "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then log_error "Invalid environment: $ENVIRONMENT" exit 1 fi # Start deployment process log_info "Starting TankStopp deployment process..." log_info "Action: $ACTION" log_info "Environment: $ENVIRONMENT" log_info "Project: $PROJECT_NAME" check_requirements setup_environment # Execute action case "$ACTION" in "deploy") deploy_application ;; "start") start_services ;; "stop") stop_services ;; "restart") restart_services ;; "update") update_application ;; "rollback") rollback_deployment ;; "status") show_status ;; "logs") show_logs ;; "backup") backup_database ;; "restore") restore_backup ;; "scale") scale_service ;; "cleanup") cleanup_resources ;; *) log_error "Unknown action: $ACTION" show_help exit 1 ;; esac log_success "Deployment process completed successfully!" } # Execute main function main "$@"