451 lines
12 KiB
Bash
Executable File
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 "$@"
|