scaffold-deply/deploy-project.sh

451 lines
12 KiB
Bash
Executable File

#!/bin/bash
# =============================================================================
# deploy-project.sh - Automated Docker Deployment Script
# =============================================================================
# This script automates the deployment of Docker containers.
# It handles four scenarios:
# - No .env, No container: Setup new instance with wizard
# - Has .env, No container: Setup new instance using .env
# - No .env, Has container: Renew instance with wizard
# - Has .env, Has container: Renew instance using .env
#
# Uses /dev/tty for interactive prompts, enabling wizard to work even when
# script is piped from curl.
#
# Builds Docker image locally from embedded Dockerfile (ephemeral runtime).
# All configuration is stored in .env file. Docker volumes are used for
# persistent storage without disturbing the host file system.
# =============================================================================
set -e
# --- Configuration ---
# When script is piped from curl, BASH_SOURCE is unreliable, so we use PWD
SCRIPT_DIR="${PWD}"
ENV_FILE="${SCRIPT_DIR}/.env"
DEFAULT_INSTANCE_NAME="my-app"
IMAGE_NAME="my-app"
IMAGE_TAG="latest"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
BUILD_CONTEXT_DIR="${SCRIPT_DIR}/.app-build"
# --- Colors for output ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# --- Helper Functions ---
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if .env file exists
has_env_file() {
[ -f "${ENV_FILE}" ]
}
# Check if container exists
has_container() {
docker ps -a --format '{{.Names}}' | grep -q "^${INSTANCE_NAME}$" 2>/dev/null || false
}
# Check if container is running
is_container_running() {
docker ps --format '{{.Names}}' | grep -q "^${INSTANCE_NAME}$" 2>/dev/null || false
}
# Load environment variables from .env file
load_env_file() {
if has_env_file; then
set -a
source "${ENV_FILE}"
set +a
log_info "Loaded configuration from .env file"
fi
}
# Save environment variables to .env file
save_env_file() {
cat > "${ENV_FILE}" << EOF
# Application Configuration
# Generated by deploy-project.sh
# Instance name
INSTANCE_NAME="${INSTANCE_NAME}"
# Docker image configuration (built locally)
IMAGE_NAME="${IMAGE_NAME}"
IMAGE_TAG="${IMAGE_TAG}"
# Volume mappings (format: "VOLUME_NAME:CONTAINER_PATH")
VOLUMES=(
"_data-vol:/app/data"
"_config-vol:/app/config"
)
# Network configuration
NETWORKS=(
"_internal-net"
)
# System settings
DEBUG=false
TZ="UTC"
EOF
log_success "Configuration saved to .env file"
}
# Interactive prompt - read from /dev/tty to work even when script is piped
prompt_user() {
local prompt_text="$1"
local default_value="$2"
local var_name="$3"
echo -n "${prompt_text}"
if [ -n "${default_value}" ]; then
echo -n " [${default_value}]"
fi
echo -n ": "
local user_input
if [ -r /dev/tty ]; then
read user_input </dev/tty
else
read user_input
fi
if [ -z "${user_input}" ] && [ -n "${default_value}" ]; then
user_input="${default_value}"
fi
eval "${var_name}='${user_input}'"
}
# Interactive wizard to collect configuration
interactive_wizard() {
echo ""
echo "=========================================="
echo " Setup Wizard"
echo "=========================================="
echo ""
log_info "Please provide the following configuration:"
echo ""
# Configuration: Instance name
prompt_user "Enter instance name" "${DEFAULT_INSTANCE_NAME}" "INSTANCE_NAME"
# Configuration: Image name
prompt_user "Enter Docker image name" "${IMAGE_NAME}" "IMAGE_NAME"
# Configuration: Image tag
prompt_user "Enter image tag" "${IMAGE_TAG}" "IMAGE_TAG"
# Configuration: Volume mappings
echo ""
log_info "Configure volume mappings:"
prompt_user "Data volume name" "_data-vol" "DATA_VOLUME"
prompt_user "Data mount path" "/app/data" "DATA_PATH"
# Configuration: Debug mode
echo ""
prompt_user "Enable debug mode" "false" "DEBUG"
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
log_success "Configuration complete!"
echo ""
}
# Create embedded Dockerfile in build context
create_build_context() {
log_info "Creating ephemeral build context..."
# Create build directory
mkdir -p "${BUILD_CONTEXT_DIR}"
# Create Dockerfile
cat > "${BUILD_CONTEXT_DIR}/Dockerfile" << 'DOCKERFILE'
# Base image
FROM alpine:3.19
# Install any required dependencies
RUN apk add --no-cache bash coreutils
# Create application directory
WORKDIR /app
# Create data and config directories
RUN mkdir -p /app/data /app/config
# Keep container running indefinitely without starting any application
# tail -f /dev/null runs forever, allowing users to exec in and run commands manually
CMD ["tail", "-f", "/dev/null"]
DOCKERFILE
log_success "Build context created"
}
# Build Docker image locally
build_image() {
log_info "Building Docker image: ${FULL_IMAGE_NAME}"
# Create build context
create_build_context
# Build the image
docker build -t "${FULL_IMAGE_NAME}" "${BUILD_CONTEXT_DIR}"
log_success "Docker image built successfully"
}
# Cleanup build context (ephemeral)
cleanup_build_context() {
if [ -d "${BUILD_CONTEXT_DIR}" ]; then
rm -rf "${BUILD_CONTEXT_DIR}"
log_info "Cleaned up ephemeral build context"
fi
}
# Build Docker run arguments for volumes
build_volume_args() {
local args=""
for vol in "${VOLUMES[@]}"; do
local src=$(echo "$vol" | cut -d: -f1)
local dst=$(echo "$vol" | cut -d: -f2)
# Handle named volumes (start with _)
if [[ "$src" == _* ]]; then
src="${INSTANCE_NAME}${src}"
# Create volume if not exists
docker volume inspect "${src}" &>/dev/null || \
docker volume create "${src}" >/dev/null
fi
args="$args -v ${src}:${dst}"
done
echo "$args"
}
# Build Docker run arguments for networks
build_network_args() {
local args=""
for net in "${NETWORKS[@]}"; do
# Handle named networks (start with _)
if [[ "$net" == _* ]]; then
net="${INSTANCE_NAME}${net}"
# Create network if not exists
docker network inspect "${net}" &>/dev/null || \
docker network create "${net}" >/dev/null
fi
args="$args --network ${net}"
done
echo "$args"
}
# Remove existing container
remove_container() {
if has_container; then
log_info "Stopping existing container..."
docker stop "${INSTANCE_NAME}" 2>/dev/null || true
log_info "Removing existing container..."
docker rm "${INSTANCE_NAME}" 2>/dev/null || true
log_success "Existing container removed"
fi
}
# Deploy container
deploy_container() {
log_info "Deploying container '${INSTANCE_NAME}'..."
# Build image locally (ephemeral runtime policy)
build_image
# Remove existing container if present
remove_container
# Build volume and network arguments
local volume_args=$(build_volume_args)
local network_args=$(build_network_args)
# Create and start container
docker run -d \
--name "${INSTANCE_NAME}" \
--restart unless-stopped \
${volume_args} \
${network_args} \
-e TZ="${TZ:-UTC}" \
"${FULL_IMAGE_NAME}"
# Cleanup ephemeral build context
cleanup_build_context
log_success "Container '${INSTANCE_NAME}' deployed successfully"
}
# Show deployment complete output
show_deployment_complete() {
echo ""
echo "============================================"
echo " Deployment Complete! Container Operation Commands"
echo ""
echo " Re-Run Container:"
echo " - Prepare your \`.env\`"
echo " - curl -s \"<URL>/deploy-project.sh\" | bash"
echo " ------------------------------------------------------------"
echo " Container Accessing:"
echo " docker exec -it -u root ${INSTANCE_NAME} bash"
echo " docker exec -it -u node ${INSTANCE_NAME} bash"
echo " ------------------------------------------------------------"
echo " View the Logs: docker logs --tail 50 ${INSTANCE_NAME}"
echo " ------------------------------------------------------------"
echo " Inspect: docker inspect ${INSTANCE_NAME}"
echo " ------------------------------------------------------------"
echo " Remove Container: docker rm -f ${INSTANCE_NAME}"
echo " ------------------------------------------------------------"
echo " Remove Volumes (if needed):"
# List volumes associated with this instance
local volumes=""
for vol in "${VOLUMES[@]}"; do
local src=$(echo "$vol" | cut -d: -f1)
if [[ "$src" == _* ]]; then
src="${INSTANCE_NAME}${src}"
fi
volumes="${volumes} ${src}"
done
if [ -n "${volumes}" ]; then
echo " docker volume rm${volumes}"
fi
echo "============================================"
}
# Validate deployment
validate_deployment() {
log_info "Validating deployment..."
sleep 2
if is_container_running; then
log_success "Container is running"
# Show deployment complete output with commands
show_deployment_complete
return 0
else
log_error "Container failed to start"
log_info "Check logs with: docker logs ${INSTANCE_NAME}"
return 1
fi
}
# Display usage information
usage() {
cat << EOF
Usage: curl -s "<url>/deploy-project.sh" | bash
Automated Docker deployment script with ephemeral runtime policy.
The script automatically detects your setup and:
- First run (no .env): Starts interactive wizard
- With .env: Uses existing configuration
- Container exists: Recreates with current settings
Key Features:
- Builds Docker image locally from embedded Dockerfile
- Ephemeral build context (cleaned up after build)
- Container runs with 'tail -f /dev/null' for manual exec
- Persistent data stored in Docker volumes
Environment Variables (in .env file):
INSTANCE_NAME Name of the Docker container
IMAGE_NAME Docker image name (built locally)
IMAGE_TAG Image version tag
VOLUMES Volume mappings (array)
NETWORKS Network connections (array)
DEBUG Enable verbose logging
Examples:
# First run - will trigger interactive wizard
curl -s "<url>/deploy-project.sh" | bash
# With existing .env file
curl -s "<url>/deploy-project.sh" | bash
# Access the running container
docker exec -it <instance-name> bash
Note: Configuration is stored in .env file in the current directory.
The Docker image is built locally and build context is cleaned up immediately.
EOF
}
# Main execution
main() {
# Check for help options
if [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "help" ]; then
usage
exit 0
fi
# Load existing configuration
load_env_file
# Determine action based on state
local has_env=$(has_env_file && echo "yes" || echo "no")
local has_ctn=$(has_container && echo "yes" || echo "no")
log_info "Configuration: .env=${has_env}, Container=${has_ctn}"
if [ "$has_env" = "no" ]; then
# No .env file - run wizard
log_info "No configuration found. Starting interactive setup..."
interactive_wizard
save_env_file
deploy_container
else
# Has .env file - use existing config
log_info "Using existing configuration..."
# Validate required variables
if [ -z "${INSTANCE_NAME}" ]; then
log_error "INSTANCE_NAME not set in .env"
exit 1
fi
# Rebuild image name if components changed
if [ -n "${IMAGE_NAME}" ]; then
FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG:-latest}"
fi
deploy_container
fi
# Validate deployment
validate_deployment
log_success "Deployment complete!"
}
# Run main function
main "$@"