Files
elasticsearch-cloudron/start.sh
2025-11-07 06:25:38 -06:00

577 lines
21 KiB
Bash

#!/bin/bash
set -e
# Check and create .env file if it doesn't exist
if [ ! -f /app/data/.env ]; then
echo "Creating default .env file..."
cp /app/.env.template /app/data/.env
chown elasticsearch:elasticsearch /app/data/.env
chmod 600 /app/data/.env
fi
# Load environment variables from .env file if it exists
if [ -f /app/data/.env ]; then
echo "Loading environment variables from .env file..."
set -o allexport
source /app/data/.env
set +o allexport
else
echo "No .env file found, using default environment variables"
fi
# Set constants
ES_HOME=/usr/share/elasticsearch
ES_PATH_CONF=/app/data/config
export ES_HOME ES_PATH_CONF
# Create directory structure
mkdir -p /app/data/{elasticsearch,logs/gc,config,run,secrets,jdk/bin}
# Set proper permissions early
echo "Setting up directory permissions..."
chown -R elasticsearch:elasticsearch /app/data
# Handle password management
setup_password() {
# Check if password already exists
if [ -f /app/data/secrets/elastic_password ]; then
# Use -r flag to prevent backslash interpretation
ELASTIC_PASSWORD=$(cat /app/data/secrets/elastic_password)
echo "Using existing Elasticsearch password."
else
# Generate a more container-safe password (alphanumeric only)
# Avoid special characters that could cause issues with command interpretation
ELASTIC_PASSWORD=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 20)
echo "Generated new secure password for Elasticsearch."
# Store password
echo "$ELASTIC_PASSWORD" > /app/data/secrets/elastic_password
chmod 600 /app/data/secrets/elastic_password
chown elasticsearch:elasticsearch /app/data/secrets/elastic_password
fi
# Make password available to other functions
export ELASTIC_PASSWORD
}
# Set up Java environment
setup_java() {
mkdir -p /app/data/jdk/bin
if [ -x /app/data/jdk/bin/java ]; then
echo "Java already configured: $(/app/data/jdk/bin/java -version 2>&1 | head -n 1)"
return 0
fi
echo "Setting up Java environment..."
JAVA_FOUND=0
for candidate in /usr/lib/jvm/java-21-openjdk-amd64/bin /usr/lib/jvm/java-17-openjdk-amd64/bin; do
if [ -x "$candidate/java" ]; then
ln -sf "$candidate/java" /app/data/jdk/bin/java
for tool in javac javadoc jar; do
[ -x "$candidate/$tool" ] && ln -sf "$candidate/$tool" /app/data/jdk/bin/$tool
done
JAVA_FOUND=1
break
fi
done
if [ $JAVA_FOUND -eq 0 ]; then
JAVA_PATH=$(command -v java || true)
if [ -n "$JAVA_PATH" ]; then
ln -sf "$JAVA_PATH" /app/data/jdk/bin/java
for tool in javac javadoc jar; do
TOOL_PATH=$(command -v $tool 2>/dev/null || true)
[ -n "$TOOL_PATH" ] && ln -sf "$TOOL_PATH" /app/data/jdk/bin/$tool
done
else
echo "ERROR: No Java found on system. Elasticsearch requires Java to run."
exit 1
fi
fi
if [ ! -x /app/data/jdk/bin/java ]; then
echo "ERROR: Failed to link Java executable"
ls -l /app/data/jdk/bin || true
exit 1
fi
echo "Java version: $(/app/data/jdk/bin/java -version 2>&1 | head -n 1)"
chown -R elasticsearch:elasticsearch /app/data/jdk
}
# Configure Elasticsearch
configure_elasticsearch() {
cd $ES_HOME
# Double-check permissions on config directory before proceeding
echo "Verifying config directory permissions..."
ls -la $ES_PATH_CONF
chmod 755 $ES_PATH_CONF
chown -R elasticsearch:elasticsearch $ES_PATH_CONF
# Copy configuration files if needed
if [ ! -f $ES_PATH_CONF/elasticsearch.yml ]; then
echo "Setting up configuration files..."
cp -r $ES_HOME/config/* $ES_PATH_CONF/ || true
cp /app/elasticsearch.yml $ES_PATH_CONF/elasticsearch.yml || true
chown -R elasticsearch:elasticsearch $ES_PATH_CONF
fi
ensure_directory_structure
clean_legacy_settings
ensure_setting "xpack.security.http.ssl.enabled" "false"
ensure_setting "network.host" "0.0.0.0"
ensure_setting "discovery.type" "single-node"
ensure_setting "path.data" "/app/data/elasticsearch"
ensure_setting "path.logs" "/app/data/logs"
# CRITICAL FIX: Remove any index-level settings from elasticsearch.yml to prevent startup failure
if [ -f $ES_PATH_CONF/elasticsearch.yml ]; then
echo "Checking elasticsearch.yml for index-level settings..."
# Create a temporary file
TEMP_FILE=$(mktemp)
# Filter out any index.* settings
grep -v "^index\." $ES_PATH_CONF/elasticsearch.yml > $TEMP_FILE
# Also remove other known problematic settings for 8.x
grep -v "^processors:" $TEMP_FILE > $TEMP_FILE.2 && mv $TEMP_FILE.2 $TEMP_FILE
grep -v "^bootstrap.system_call_filter:" $TEMP_FILE > $TEMP_FILE.2 && mv $TEMP_FILE.2 $TEMP_FILE
# Add warning comment
echo "" >> $TEMP_FILE
echo "# NOTE: The following settings have been removed from this file:" >> $TEMP_FILE
echo "# - All index.* settings (applied via index templates)" >> $TEMP_FILE
echo "# - processors setting (no longer supported in 8.x)" >> $TEMP_FILE
echo "# - bootstrap.system_call_filter (no longer supported in 8.x)" >> $TEMP_FILE
echo "# See the create_index_template function in start.sh for details" >> $TEMP_FILE
# Replace the original file
cat $TEMP_FILE > $ES_PATH_CONF/elasticsearch.yml
rm $TEMP_FILE
# Ensure proper ownership
chown elasticsearch:elasticsearch $ES_PATH_CONF/elasticsearch.yml
echo "Cleaned elasticsearch.yml configuration file"
fi
# Update JVM options for GC logs
if [ -f $ES_PATH_CONF/jvm.options ]; then
echo "Updating JVM options..."
sed -i 's|logs/gc.log|/app/data/logs/gc/gc.log|g' $ES_PATH_CONF/jvm.options
chown elasticsearch:elasticsearch $ES_PATH_CONF/jvm.options
fi
# Generate SSL certificates if needed
if [ ! -f $ES_PATH_CONF/elastic-certificates.p12 ]; then
echo "Generating self-signed certificates..."
mkdir -p /tmp/elastic-certs
chown elasticsearch:elasticsearch /tmp/elastic-certs
ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-certutil ca \
--out /tmp/elastic-certs/elastic-stack-ca.p12 \
--pass "cloudron" \
--silent
ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-certutil cert \
--ca /tmp/elastic-certs/elastic-stack-ca.p12 \
--ca-pass "cloudron" \
--out $ES_PATH_CONF/elastic-certificates.p12 \
--pass "cloudron" \
--silent
chown elasticsearch:elasticsearch $ES_PATH_CONF/elastic-certificates.p12
chmod 600 $ES_PATH_CONF/elastic-certificates.p12
# Make sure we update the keystore with the correct password after generating certificates
echo "Updating keystore with the new certificate password..."
setup_keystore
fi
# Create users file if needed
if [ ! -f $ES_PATH_CONF/users ]; then
echo "Creating users file..."
# Note: We'll reset the password after Elasticsearch starts, so this default password
# is just for initial bootstrap
echo 'elastic:$2a$10$BtVRGAoL8AbgEKnlvYj8cewQF3QkUz1pyL.Ga3j.jFKNUk2yh7.zW' > $ES_PATH_CONF/users
echo 'kibana_system:$2a$10$BtVRGAoL8AbgEKnlvYj8cewQF3QkUz1pyL.Ga3j.jFKNUk2yh7.zW' >> $ES_PATH_CONF/users
echo 'superuser:elastic' > $ES_PATH_CONF/users_roles
chown elasticsearch:elasticsearch $ES_PATH_CONF/{users,users_roles}
chmod 600 $ES_PATH_CONF/{users,users_roles}
fi
# Final permission check
echo "Final permission check on all data directories..."
chown -R elasticsearch:elasticsearch /app/data
chmod 755 /app/data /app/data/config
}
# Create index template with optimized settings
create_index_template() {
echo "Creating default index template with optimized settings..."
# Wait a moment to ensure Elasticsearch is fully operational
sleep 10
# Define the template JSON with all the index settings that were previously in elasticsearch.yml
template_json=$(cat <<EOF
{
"index_patterns": ["*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"merge": {
"scheduler": {
"max_thread_count": 1
},
"policy": {
"floor_segment": "2mb",
"max_merge_at_once": 4,
"segments_per_tier": 8
}
}
}
},
"priority": 1,
"_meta": {
"description": "Default template with Cloudron optimized settings"
}
}
EOF
)
# Try multiple times in case Elasticsearch is still initializing
for i in {1..5}; do
echo "Attempt $i to create index template..."
# Apply the template
response=$(curl -s -w "\n%{http_code}" -X PUT "http://localhost:9200/_index_template/cloudron_defaults" \
-H "Content-Type: application/json" \
-u "elastic:$ELASTIC_PASSWORD" \
-d "$template_json")
http_code=$(echo "$response" | tail -n1)
response_body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
echo "✅ Index template created successfully with HTTP code $http_code"
echo "Response: $response_body"
return 0
else
echo "⚠️ Failed to create index template on attempt $i. HTTP code: $http_code"
echo "Response: $response_body"
sleep 5
fi
done
echo "⚠️ Warning: Failed to create index template after multiple attempts."
echo "Default settings may not be applied to new indices, but Elasticsearch will still function."
echo "You can manually create the template later using Elasticsearch API."
# Don't fail the startup process if template creation fails
return 0
}
# Set system limits - be more tolerant of container restrictions
set_system_limits() {
echo "Setting system limits for Elasticsearch..."
# Try to set file descriptor limit, but don't fail if it doesn't work
ulimit -n 65536 2>/dev/null || echo "Warning: Could not set file descriptor limit (not critical)"
# Try to set memory lock limit, but don't fail if it doesn't work
ulimit -l unlimited 2>/dev/null || echo "Warning: Could not set memory lock limit (not critical)"
# Only try to update transparent huge pages if the file exists and is writable
if [ -w /sys/kernel/mm/transparent_hugepage/enabled ]; then
echo never > /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true
else
echo "Warning: Cannot modify transparent hugepage settings (read-only filesystem, not critical)"
fi
# Only try to update vm.max_map_count if sysctl is available and we have permission
if command -v sysctl >/dev/null && [ $(id -u) -eq 0 ]; then
sysctl -w vm.max_map_count=262144 2>/dev/null || echo "Warning: Could not set vm.max_map_count (not critical)"
else
echo "Warning: Could not set vm.max_map_count (not running as root or sysctl not available)"
fi
# Add a note about bootstrap.memory_lock if we couldn't set the memory lock
if ! ulimit -l unlimited 2>/dev/null; then
echo "Note: Memory locking unavailable. Setting bootstrap.memory_lock=false in elasticsearch.yml"
if grep -q "bootstrap.memory_lock:" $ES_PATH_CONF/elasticsearch.yml; then
sed -i 's/bootstrap.memory_lock:.*/bootstrap.memory_lock: false/' $ES_PATH_CONF/elasticsearch.yml
else
echo "bootstrap.memory_lock: false" >> $ES_PATH_CONF/elasticsearch.yml
fi
fi
}
# Add secure settings to the keystore
setup_keystore() {
echo "Setting up Elasticsearch keystore with secure settings..."
# Create or recreate the keystore if needed
if [ ! -f $ES_PATH_CONF/elasticsearch.keystore ]; then
echo "Creating new Elasticsearch keystore..."
su -c "ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-keystore create" elasticsearch
# Verify keystore was created
if [ ! -f $ES_PATH_CONF/elasticsearch.keystore ]; then
echo "ERROR: Failed to create keystore!"
return 1
fi
fi
# Add the certificate passwords to the keystore (as the elasticsearch user)
echo "Adding certificate passwords to keystore..."
echo "cloudron" | su -c "ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-keystore add -f xpack.security.transport.ssl.keystore.secure_password --stdin" elasticsearch || {
echo "ERROR: Failed to add keystore password to keystore. Will try to recreate keystore."
rm -f $ES_PATH_CONF/elasticsearch.keystore
su -c "ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-keystore create" elasticsearch
echo "cloudron" | su -c "ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-keystore add -f xpack.security.transport.ssl.keystore.secure_password --stdin" elasticsearch || {
echo "CRITICAL ERROR: Could not add keystore password to keystore after recreation."
return 1
}
}
echo "cloudron" | su -c "ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch-keystore add -f xpack.security.transport.ssl.truststore.secure_password --stdin" elasticsearch || {
echo "ERROR: Failed to add truststore password to keystore."
return 1
}
# Verify keystore permissions
chmod 600 $ES_PATH_CONF/elasticsearch.keystore
chown elasticsearch:elasticsearch $ES_PATH_CONF/elasticsearch.keystore
echo "✅ Elasticsearch keystore updated with secure settings"
return 0
}
# Ensure writable dirs exist
ensure_directory_structure() {
mkdir -p /app/data/elasticsearch
mkdir -p /app/data/elasticsearch/plugins
mkdir -p /app/data/logs
chown -R elasticsearch:elasticsearch /app/data/elasticsearch /app/data/logs
}
clean_legacy_settings() {
sed -i '/^path\.home:/d' "$ES_PATH_CONF/elasticsearch.yml"
sed -i '/^path\.data:/d' "$ES_PATH_CONF/elasticsearch.yml"
sed -i '/^path\.logs:/d' "$ES_PATH_CONF/elasticsearch.yml"
sed -i '/^path\.plugins:/d' "$ES_PATH_CONF/elasticsearch.yml"
sed -i '/^bootstrap\.password:/d' "$ES_PATH_CONF/elasticsearch.yml"
}
ensure_setting() {
local key="$1"
local value="$2"
if grep -q "^$key:" "$ES_PATH_CONF/elasticsearch.yml"; then
sed -i "s|^$key:.*|$key: $value|" "$ES_PATH_CONF/elasticsearch.yml"
else
echo "$key: $value" >> "$ES_PATH_CONF/elasticsearch.yml"
fi
}
default_gc_opts() {
echo "-XX:+UseG1GC -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -Xlog:gc*,gc+age=trace,safepoint:file=/app/data/logs/gc/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m"
}
normalize_heap_value() {
local raw="${1:-}"
raw=$(echo "$raw" | tr '[:upper:]' '[:lower:]')
if [[ "$raw" =~ ^[0-9]+[mg]$ ]]; then
echo "$raw"
return 0
fi
if [[ "$raw" =~ ^[0-9]+$ ]]; then
echo "${raw}m"
return 0
fi
return 1
}
detect_memory_limit_bytes() {
local limit=""
if [ -r /sys/fs/cgroup/memory.max ]; then
limit=$(cat /sys/fs/cgroup/memory.max)
if [ "$limit" != "max" ]; then
echo "$limit"
return 0
fi
fi
if [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
limit=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
echo "$limit"
return 0
fi
if [ -n "${CLOUDRON_MEMORY_LIMIT:-}" ]; then
# CLOUDRON_MEMORY_LIMIT is in MB
echo $((CLOUDRON_MEMORY_LIMIT * 1024 * 1024))
return 0
fi
echo ""
}
has_custom_heap_file() {
local heap_file="$ES_PATH_CONF/jvm.options.d/heap.options"
if [ -f "$heap_file" ] && grep -Eq '^-Xms|^-Xmx' "$heap_file"; then
return 0
fi
return 1
}
# Configure JVM heap size
configure_heap() {
if [ "${ES_SKIP_AUTO_HEAP:-0}" = "1" ]; then
echo "Skipping automatic heap sizing because ES_SKIP_AUTO_HEAP=1"
return 0
fi
if [ -n "${ES_JAVA_OPTS:-}" ]; then
echo "Detected user-specified ES_JAVA_OPTS; not overriding heap settings."
return 0
fi
if has_custom_heap_file; then
echo "Detected custom heap options in jvm.options.d; not overriding heap settings."
return 0
fi
local heap_value=""
if [ -n "${ES_JAVA_HEAP:-}" ]; then
if heap_value=$(normalize_heap_value "$ES_JAVA_HEAP"); then
echo "Using heap size from ES_JAVA_HEAP (${heap_value})."
else
echo "Warning: ES_JAVA_HEAP value '$ES_JAVA_HEAP' is invalid; falling back to automatic sizing."
heap_value=""
fi
fi
if [ -z "$heap_value" ]; then
local limit_bytes
limit_bytes=$(detect_memory_limit_bytes)
if [ -z "$limit_bytes" ] || [ "$limit_bytes" = "0" ]; then
limit_bytes=$((4 * 1024 * 1024 * 1024))
fi
# Treat huge values as "unlimited" and fall back to 4GB to avoid 31GB default
if [ "$limit_bytes" -gt $((256 * 1024 * 1024 * 1024)) ]; then
limit_bytes=$((4 * 1024 * 1024 * 1024))
fi
local heap_mb=$((limit_bytes / 1024 / 1024 / 2))
[ "$heap_mb" -gt 31744 ] && heap_mb=31744
[ "$heap_mb" -lt 512 ] && heap_mb=512
heap_value="${heap_mb}m"
echo "Auto-configured heap size to ${heap_value} based on container limit."
fi
export ES_JAVA_OPTS="-Xms${heap_value} -Xmx${heap_value} $(default_gc_opts)"
export PATH=$ES_HOME/bin:$PATH
}
# Start Elasticsearch
start_elasticsearch() {
# Create PID file
touch /app/data/run/elasticsearch.pid
chown elasticsearch:elasticsearch /app/data/run/elasticsearch.pid
# Command to start Elasticsearch
ES_START_CMD="ES_PATH_CONF=$ES_PATH_CONF ES_JAVA_HOME=/app/data/jdk $ES_HOME/bin/elasticsearch"
ES_START_CMD="$ES_START_CMD -E xpack.security.enabled=true"
# Add explicit settings for transport SSL
ES_START_CMD="$ES_START_CMD -E xpack.security.transport.ssl.enabled=true"
ES_START_CMD="$ES_START_CMD -E xpack.security.transport.ssl.verification_mode=certificate"
ES_START_CMD="$ES_START_CMD -E xpack.security.transport.ssl.keystore.path=elastic-certificates.p12"
ES_START_CMD="$ES_START_CMD -E xpack.security.transport.ssl.truststore.path=elastic-certificates.p12"
ES_START_CMD="$ES_START_CMD -d -p /app/data/run/elasticsearch.pid"
echo "Starting Elasticsearch..."
cd $ES_HOME
su -c "$ES_START_CMD" elasticsearch
# Wait for Elasticsearch to start
echo "Waiting for Elasticsearch to start..."
attempts=0
max_attempts=60
until $(curl --output /dev/null --silent --head --fail -u "elastic:$ELASTIC_PASSWORD" http://localhost:9200); do
if ! ps -p $(cat /app/data/run/elasticsearch.pid 2>/dev/null) > /dev/null 2>&1; then
echo "ERROR: Elasticsearch process is not running. Logs:"
cat /app/data/logs/*.log
exit 1
fi
printf '.'
sleep 5
attempts=$((attempts+1))
if [ $attempts -ge $max_attempts ]; then
echo "ERROR: Elasticsearch failed to start after 5 minutes. Logs:"
cat /app/data/logs/*.log
exit 1
fi
done
echo "Elasticsearch is up and running!"
# Reset the elastic user password
cd $ES_HOME
echo "y" | ES_JAVA_HOME=/app/data/jdk bin/elasticsearch-reset-password -u elastic --password "$ELASTIC_PASSWORD" --url "http://localhost:9200" || true
# Create index template with the settings we removed from elasticsearch.yml
echo "Applying index templates with settings removed from elasticsearch.yml..."
create_index_template
# Create credentials file
cat > /app/data/credentials.txt << EOL
Elasticsearch credentials:
URL: http://localhost:9200
User: elastic
Password: $ELASTIC_PASSWORD
EOL
chmod 600 /app/data/credentials.txt
echo "-----------------------------"
echo "Elasticsearch is ready to use!"
echo "URL: http://localhost:9200"
echo "User: elastic"
echo "Password: $ELASTIC_PASSWORD"
echo "Password is stored in: /app/data/credentials.txt"
echo "-----------------------------"
}
# Main execution flow
setup_password
setup_java
setup_keystore
configure_elasticsearch
[ ! -f /app/data/.initialized ] && touch /app/data/.initialized
# Final permission check
echo "Performing final permission check on all directories..."
chown -R elasticsearch:elasticsearch /app/data/{elasticsearch,logs,config,jdk,run,secrets}
chmod -R 755 /app/data
chmod 600 /app/data/secrets/elastic_password
set_system_limits
configure_heap
start_elasticsearch
# Keep container running
tail -f /app/data/logs/*.log 2>/dev/null || sleep infinity