1#!/usr/bin/env bash
2set -euo pipefail
3
4# Docker Entrypoint Script
5# Handles initialization, configuration, and graceful shutdown for containerized applications
6
7# ============================================================================
8# Configuration
9# ============================================================================
10
11APP_NAME="${APP_NAME:-myapp}"
12APP_ENV="${APP_ENV:-production}"
13CONFIG_DIR="${CONFIG_DIR:-/app/config}"
14DATA_DIR="${DATA_DIR:-/app/data}"
15
16# Colors (for logs)
17RED='\033[0;31m'
18GREEN='\033[0;32m'
19YELLOW='\033[1;33m'
20BLUE='\033[0;34m'
21NC='\033[0m'
22
23# ============================================================================
24# Logging Functions
25# ============================================================================
26
27log_info() {
28 echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]${NC} $*"
29}
30
31log_success() {
32 echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS]${NC} $*"
33}
34
35log_error() {
36 echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR]${NC} $*" >&2
37}
38
39log_warning() {
40 echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING]${NC} $*"
41}
42
43# ============================================================================
44# Environment Variable Validation
45# ============================================================================
46
47validate_required_vars() {
48 log_info "Validating required environment variables..."
49
50 local required_vars=(
51 "APP_NAME"
52 "APP_ENV"
53 )
54
55 local missing_vars=()
56
57 for var in "${required_vars[@]}"; do
58 if [[ -z "${!var:-}" ]]; then
59 missing_vars+=("$var")
60 else
61 log_info " ✓ $var=${!var}"
62 fi
63 done
64
65 if [[ ${#missing_vars[@]} -gt 0 ]]; then
66 log_error "Missing required environment variables:"
67 for var in "${missing_vars[@]}"; do
68 log_error " - $var"
69 done
70 exit 1
71 fi
72
73 log_success "All required variables present"
74}
75
76validate_optional_vars() {
77 log_info "Optional configuration:"
78
79 local optional_vars=(
80 "DATABASE_URL"
81 "REDIS_URL"
82 "LOG_LEVEL"
83 "MAX_WORKERS"
84 )
85
86 for var in "${optional_vars[@]}"; do
87 if [[ -n "${!var:-}" ]]; then
88 # Mask sensitive values
89 local display_value="${!var}"
90 if [[ "$var" =~ URL ]]; then
91 display_value=$(echo "${!var}" | sed 's/:[^@]*@/:***@/')
92 fi
93 log_info " ✓ $var=$display_value"
94 fi
95 done
96}
97
98# ============================================================================
99# Configuration Template Processing
100# ============================================================================
101
102process_templates() {
103 log_info "Processing configuration templates..."
104
105 local template_dir="/app/templates"
106
107 if [[ ! -d "$template_dir" ]]; then
108 log_warning "Template directory not found: $template_dir"
109 return 0
110 fi
111
112 # Create config directory if it doesn't exist
113 mkdir -p "$CONFIG_DIR"
114
115 # Process all .template files
116 find "$template_dir" -name "*.template" -type f | while read -r template_file; do
117 local config_file
118 config_file="$CONFIG_DIR/$(basename "${template_file%.template}")"
119
120 log_info " Processing: $(basename "$template_file") -> $(basename "$config_file")"
121
122 # Use envsubst to replace environment variables
123 envsubst < "$template_file" > "$config_file"
124
125 # Set appropriate permissions
126 chmod 644 "$config_file"
127 done
128
129 log_success "Configuration templates processed"
130}
131
132# ============================================================================
133# Wait for Dependencies
134# ============================================================================
135
136wait_for_service() {
137 local host=$1
138 local port=$2
139 local timeout=${3:-30}
140
141 log_info "Waiting for $host:$port (timeout: ${timeout}s)..."
142
143 local start_time
144 start_time=$(date +%s)
145
146 while ! nc -z "$host" "$port" 2>/dev/null; do
147 local current_time
148 current_time=$(date +%s)
149 local elapsed=$((current_time - start_time))
150
151 if [[ $elapsed -ge $timeout ]]; then
152 log_error "Timeout waiting for $host:$port"
153 return 1
154 fi
155
156 sleep 1
157 done
158
159 log_success "$host:$port is available"
160}
161
162wait_for_dependencies() {
163 log_info "Waiting for service dependencies..."
164
165 # Parse DATABASE_URL if present
166 if [[ -n "${DATABASE_URL:-}" ]]; then
167 # Extract host and port from URL (simplified)
168 # Example: postgresql://user:pass@host:5432/db
169 local db_host
170 local db_port
171
172 db_host=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p')
173 db_port=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
174
175 if [[ -n "$db_host" ]] && [[ -n "$db_port" ]]; then
176 wait_for_service "$db_host" "$db_port" 60
177 fi
178 fi
179
180 # Parse REDIS_URL if present
181 if [[ -n "${REDIS_URL:-}" ]]; then
182 local redis_host
183 local redis_port
184
185 redis_host=$(echo "$REDIS_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p')
186 redis_port=$(echo "$REDIS_URL" | sed -n 's/.*:\([0-9]*\).*/\1/p')
187
188 # Default Redis port if not specified
189 redis_port=${redis_port:-6379}
190
191 if [[ -n "$redis_host" ]]; then
192 wait_for_service "$redis_host" "$redis_port" 30
193 fi
194 fi
195
196 log_success "All dependencies are ready"
197}
198
199# ============================================================================
200# Signal Handling for Graceful Shutdown
201# ============================================================================
202
203cleanup() {
204 log_warning "Received shutdown signal, cleaning up..."
205
206 # If a child process is running, send it SIGTERM
207 if [[ -n "${APP_PID:-}" ]]; then
208 log_info "Sending SIGTERM to application (PID: $APP_PID)"
209 kill -TERM "$APP_PID" 2>/dev/null || true
210
211 # Wait for graceful shutdown (max 30 seconds)
212 local timeout=30
213 local elapsed=0
214
215 while kill -0 "$APP_PID" 2>/dev/null && [[ $elapsed -lt $timeout ]]; do
216 sleep 1
217 elapsed=$((elapsed + 1))
218 done
219
220 # Force kill if still running
221 if kill -0 "$APP_PID" 2>/dev/null; then
222 log_warning "Application didn't stop gracefully, forcing shutdown"
223 kill -KILL "$APP_PID" 2>/dev/null || true
224 fi
225 fi
226
227 log_success "Cleanup complete"
228 exit 0
229}
230
231# Trap signals for graceful shutdown
232trap cleanup SIGTERM SIGINT SIGQUIT
233
234# ============================================================================
235# Application Initialization
236# ============================================================================
237
238initialize_app() {
239 log_info "Initializing application..."
240
241 # Create necessary directories
242 mkdir -p "$DATA_DIR" "$CONFIG_DIR"
243
244 # Set permissions
245 chmod 755 "$DATA_DIR" "$CONFIG_DIR"
246
247 # Run any database migrations (if needed)
248 if [[ -x "/app/bin/migrate" ]]; then
249 log_info "Running database migrations..."
250 /app/bin/migrate
251 fi
252
253 log_success "Application initialized"
254}
255
256# ============================================================================
257# Main Entry Point
258# ============================================================================
259
260main() {
261 log_info "=== Starting $APP_NAME ($APP_ENV) ==="
262 echo
263
264 # Step 1: Validate environment
265 validate_required_vars
266 validate_optional_vars
267 echo
268
269 # Step 2: Process configuration templates
270 process_templates
271 echo
272
273 # Step 3: Wait for dependencies
274 wait_for_dependencies
275 echo
276
277 # Step 4: Initialize application
278 initialize_app
279 echo
280
281 # Step 5: Start the application
282 log_info "Starting application..."
283
284 # Default command if none provided
285 if [[ $# -eq 0 ]]; then
286 set -- "/app/bin/server"
287 fi
288
289 log_info "Command: $*"
290 echo
291
292 # Execute the application
293 # Use exec to replace the shell with the application
294 # This ensures signals are properly forwarded
295 exec "$@" &
296 APP_PID=$!
297
298 # Wait for the application process
299 wait $APP_PID
300}
301
302# Run main with all arguments
303main "$@"