-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-entrypoint.sh
More file actions
543 lines (472 loc) · 21.1 KB
/
docker-entrypoint.sh
File metadata and controls
543 lines (472 loc) · 21.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
#!/usr/bin/env bash
set -e
# Security-hardened entrypoint with PUID/GUID support
# Supports both build-time users and runtime PUID/GUID configuration
# PUID/GUID support (legacy compatibility)
# Accepts PGID (Linuxserver.io / Unraid convention) as an alias for GUID.
# Bash nested expansion is used here because docker-compose does not evaluate nested ${VAR:-fallback} forms.
PUID=${PUID:-1000}
GUID=${PGID:-${GUID:-1000}}
# Build-time user/group names (for security hardening)
APP_USER=${USER:-appuser}
APP_GROUP=${GROUP:-appgroup}
# Function to check if running with read-only filesystem or restricted user management
is_readonly_fs() {
# Check if root filesystem is read-only
if mount | grep -q 'on / .*ro,'; then
return 0 # Read-only
fi
# Check if we can write to /tmp (basic filesystem test)
if ! touch /tmp/.write-test 2>/dev/null; then
return 0 # Read-only
fi
rm -f /tmp/.write-test 2>/dev/null
# Check if /etc/passwd and /etc/group are writable (critical for user management)
if [ ! -w /etc/passwd ] || [ ! -w /etc/group ]; then
return 0 # User management not possible
fi
return 1 # Writable
}
# Function to detect if container was started with --user directive
is_user_directive() {
# If we're not running as root, we were likely started with --user
if [ "$(id -u)" != "0" ]; then
return 0 # User directive used
fi
# Additional check: if PUID/GUID are set but we can't modify users, likely user directive
if [ -n "$PUID" ] && [ -n "$GUID" ] && is_readonly_fs; then
return 0 # Likely user directive scenario
fi
return 1 # Not user directive
}
# Function to handle PUID/GUID configuration with read-only and user directive support
setup_user_mapping() {
echo "🔧 Setting up user mapping..."
echo "Current user: $(id)"
echo "PUID=${PUID:-not set}, GUID=${GUID:-not set}"
echo "APP_USER=${APP_USER}, APP_GROUP=${APP_GROUP}"
# Detect deployment scenario
local readonly_detected=false
local user_directive_detected=false
if is_readonly_fs; then
readonly_detected=true
echo "🔒 Read-only filesystem or restricted user management detected"
fi
if is_user_directive; then
user_directive_detected=true
echo "👤 Container started with user directive (--user flag)"
fi
# Handle different scenarios
if [ "$user_directive_detected" = "true" ]; then
echo "📋 User directive mode: Running as $(id -u):$(id -g)"
echo "ℹ️ PUID/GUID variables ignored in user directive mode"
echo "✅ Using container's current user for all operations"
# Don't try to change users or use gosu
APP_USER="$(id -u)"
APP_GROUP="$(id -g)"
elif [ "$readonly_detected" = "true" ]; then
echo "🔒 Read-only filesystem mode"
if [ "$(id -u)" = "0" ]; then
echo "⚠️ Running as root but cannot create users in read-only filesystem"
echo "💡 For PUID/GUID support in read-only mode, use:"
echo " docker run --user $PUID:$GUID --read-only ..."
echo "✅ Will use build-time user for privilege dropping"
# Use build-time defaults since we can't create custom users
APP_USER="1000"
APP_GROUP="1000"
else
echo "✅ Already running as non-root user in read-only mode"
APP_USER="$(id -u)"
APP_GROUP="$(id -g)"
fi
elif [ "$(id -u)" = "0" ] && [ -n "$PUID" ] && [ -n "$GUID" ]; then
echo "🔧 Standard PUID/GUID mode: Setting up mapping $PUID:$GUID"
# Create or modify group to match GUID
if ! getent group "$GUID" >/dev/null 2>&1; then
if groupadd -g "$GUID" "$APP_GROUP" 2>/dev/null; then
echo "✅ Created group $APP_GROUP with GID $GUID"
else
echo "⚠️ Could not create group, will use existing"
fi
else
echo "ℹ️ Group with GID $GUID already exists"
fi
# Create or modify user to match PUID
if ! getent passwd "$PUID" >/dev/null 2>&1; then
if useradd -u "$PUID" -g "$GUID" -d /app -s /bin/bash "$APP_USER" 2>/dev/null; then
echo "✅ Created user $APP_USER with UID $PUID"
else
echo "⚠️ Could not create user, will use existing"
fi
else
# User exists, check if it's ours or handle gracefully
existing_user=$(getent passwd "$PUID" | cut -d: -f1)
if [ "$existing_user" = "$APP_USER" ]; then
if usermod -g "$GUID" "$APP_USER" 2>/dev/null; then
echo "✅ Updated user $APP_USER with GID $GUID"
else
echo "ℹ️ User $APP_USER already properly configured"
fi
else
echo "ℹ️ UID $PUID is used by $existing_user (will use numeric ID)"
fi
fi
# Use PUID/GUID for operations
APP_USER="$PUID"
APP_GROUP="$GUID"
echo "✅ User mapping configured: $APP_USER:$APP_GROUP"
elif [ "$(id -u)" = "0" ]; then
echo "� Root mode without PUID/GUID: Using build-time defaults"
APP_USER="1000"
APP_GROUP="1000"
echo "💡 To use custom IDs, set PUID and GUID environment variables"
else
echo "👤 Non-root mode: Using current user $(id -u):$(id -g)"
APP_USER="$(id -u)"
APP_GROUP="$(id -g)"
fi
echo "📋 Final configuration: $APP_USER:$APP_GROUP"
echo "🎯 Deployment mode: $([ "$readonly_detected" = "true" ] && echo "READ-ONLY" || echo "STANDARD") $([ "$user_directive_detected" = "true" ] && echo "+ USER-DIRECTIVE" || echo "")"
}
# Ensure writable directories exist for application data with comprehensive self-fixing
ensure_writable_dirs() {
echo "🔧 Setting up application directories and permissions..."
# Determine target user/group (PUID/GUID takes precedence)
local target_uid="${PUID:-1000}"
local target_gid="${GUID:-1000}"
local target_user="${APP_USER}"
local target_group="${APP_GROUP}"
echo "Target ownership: $target_uid:$target_gid ($target_user:$target_group)"
# Only attempt directory creation if we can write
if is_readonly_fs; then
echo "⚠️ Read-only filesystem detected"
# For read-only filesystem, only check that required dirs exist
if [ ! -d "/app/instance" ]; then
echo "❌ ERROR: /app/instance directory does not exist. Please mount it as a volume."
exit 1
fi
echo "✅ Instance directory exists on read-only filesystem"
else
# Create directories if needed
mkdir -p /app/instance
echo "📁 Created /app/instance directory"
# Fix ownership and permissions
if [ "$(id -u)" = "0" ] && ! is_user_directive; then
echo "🔑 Running as root - fixing ownership and permissions"
# Set directory ownership and permissions (with error handling for read-only)
if chown "$target_uid:$target_gid" /app/instance 2>/dev/null; then
chmod 755 /app/instance
echo "✅ Set /app/instance ownership to $target_uid:$target_gid with 755 permissions"
else
echo "⚠️ Could not change ownership (possibly read-only filesystem)"
if chmod 755 /app/instance 2>/dev/null; then
echo "✅ Set directory permissions to 755"
else
echo "ℹ️ Directory permissions unchanged (read-only filesystem)"
fi
fi
# Handle existing database file
if [ -f "/app/instance/subscriptions.db" ]; then
echo "🗄️ Database file exists - fixing permissions"
chown "$target_uid:$target_gid" /app/instance/subscriptions.db
chmod 664 /app/instance/subscriptions.db
# Test database write capability
if command -v sqlite3 >/dev/null 2>&1; then
if ! sudo -u "#$target_uid" sqlite3 /app/instance/subscriptions.db "CREATE TABLE IF NOT EXISTS permission_test (id INTEGER); DROP TABLE IF EXISTS permission_test;" 2>/dev/null; then
echo "⚠️ Database write test failed - attempting repair"
# Try to fix any corruption or permission issues
chown "$target_uid:$target_gid" /app/instance/subscriptions.db*
chmod 664 /app/instance/subscriptions.db*
else
echo "✅ Database write test passed"
fi
else
echo "ℹ️ sqlite3 not available for testing, will test in Python"
fi
# Fix WAL and SHM files if they exist
for ext in wal shm; do
if [ -f "/app/instance/subscriptions.db-$ext" ]; then
chown "$target_uid:$target_gid" "/app/instance/subscriptions.db-$ext"
chmod 664 "/app/instance/subscriptions.db-$ext"
echo "✅ Fixed permissions for subscriptions.db-$ext"
fi
done
else
echo "📝 Database file doesn't exist yet - will be created with proper permissions"
fi
else
echo "👤 Running as non-root user: $(id)"
# Test write capability
if [ ! -w "/app/instance" ]; then
echo "❌ ERROR: /app/instance is not writable by current user $(id -u):$(id -g)"
echo "Current directory permissions:"
ls -la /app/instance 2>/dev/null || ls -la /app/
echo ""
echo "🔧 To fix this issue, run on the host:"
echo " sudo chown -R $(id -u):$(id -g) ./data"
echo " chmod 755 ./data"
echo " docker-compose restart"
exit 1
fi
# Test actual write capability
if ! touch "/app/instance/write_test" 2>/dev/null; then
echo "❌ ERROR: Cannot write to /app/instance directory"
exit 1
else
rm -f "/app/instance/write_test"
echo "✅ Write test passed for /app/instance"
fi
# Check database file permissions if it exists
if [ -f "/app/instance/subscriptions.db" ]; then
if [ ! -w "/app/instance/subscriptions.db" ]; then
echo "❌ ERROR: Database file exists but is not writable"
ls -la /app/instance/subscriptions.db
echo ""
echo "🔧 To fix this issue, run on the host:"
echo " sudo chown $(id -u):$(id -g) ./data/subscriptions.db"
echo " chmod 664 ./data/subscriptions.db"
echo " docker-compose restart"
exit 1
else
echo "✅ Database file is writable"
fi
fi
fi
fi
echo "✅ Directory setup completed successfully"
}
# Set up temporary directories for application runtime
setup_temp_dirs() {
# Use /tmp for temporary files (usually writable even with read-only root)
export TMPDIR="/tmp/app-runtime"
export TEMP="/tmp/app-runtime"
export TMP="/tmp/app-runtime"
# Create temp directories if they don't exist and filesystem is writable
if ! is_readonly_fs; then
mkdir -p "$TMPDIR" 2>/dev/null || true
if [ "$(id -u)" = "0" ]; then
# Use PUID:GUID if available, otherwise use build-time user
local owner="${PUID:-$APP_USER}"
local group="${GUID:-$APP_GROUP}"
chown "$owner:$group" "$TMPDIR" 2>/dev/null || true
fi
fi
}
# Check if we need to drop privileges
should_drop_privileges() {
# Only drop privileges if we're running as root
[ "$(id -u)" = "0" ]
}
# Initialize database with proper permissions and comprehensive self-fixing
init_database() {
# Only run database initialization if we're starting the main application
if [[ "$1" == *"python"* ]] || [[ "$1" == *"gunicorn"* ]] || [[ "$1" == *"run.py"* ]]; then
echo "🗄️ Initializing database with self-fixing capabilities..."
# Use PUID/GUID if provided, otherwise use build-time defaults
local target_uid="${PUID:-1000}"
local target_gid="${GUID:-1000}"
# Create database directory if it doesn't exist
mkdir -p /app/instance
# Set proper permissions for database operations
if [ "$(id -u)" = "0" ]; then
echo "🔧 Root privileges available - performing comprehensive database setup"
# Set directory ownership using PUID/GUID
chown "$target_uid:$target_gid" /app/instance
chmod 755 /app/instance
echo "✅ Set /app/instance ownership to $target_uid:$target_gid"
# Handle database file creation and permissions
local db_file="/app/instance/subscriptions.db"
if [ -f "$db_file" ]; then
echo "📝 Existing database found - fixing permissions"
# Fix ownership and permissions
chown "$target_uid:$target_gid" "$db_file"
chmod 664 "$db_file"
# Comprehensive database repair and test
echo "🔍 Testing database integrity and write capability..."
# Test as the target user
if sudo -u "#$target_uid" python3 -c "
import sqlite3
import sys
try:
conn = sqlite3.connect('$db_file')
conn.execute('CREATE TABLE IF NOT EXISTS permission_test (id INTEGER PRIMARY KEY)')
conn.execute('INSERT INTO permission_test DEFAULT VALUES')
conn.execute('DELETE FROM permission_test')
conn.execute('DROP TABLE permission_test')
conn.commit()
conn.close()
print('✅ Database write test PASSED')
except Exception as e:
print(f'❌ Database write test FAILED: {e}')
sys.exit(1)
" 2>/dev/null; then
echo "✅ Database is fully functional"
else
echo "⚠️ Database write test failed - attempting repair"
# Try to fix any WAL mode issues
sudo -u "#$target_uid" python3 -c "
import sqlite3
try:
conn = sqlite3.connect('$db_file')
conn.execute('PRAGMA journal_mode=DELETE')
conn.execute('VACUUM')
conn.close()
print('🔧 Database repair attempted')
except Exception as e:
print(f'⚠️ Database repair failed: {e}')
" 2>/dev/null || true
# Final permission fix
chown "$target_uid:$target_gid" "$db_file"*
chmod 664 "$db_file"*
fi
else
echo "📝 No existing database - will be created with proper permissions"
# Pre-create database with correct ownership
sudo -u "#$target_uid" python3 -c "
import sqlite3
import os
db_path = '$db_file'
if not os.path.exists(db_path):
conn = sqlite3.connect(db_path)
conn.execute('CREATE TABLE IF NOT EXISTS init_test (id INTEGER)')
conn.execute('DROP TABLE init_test')
conn.commit()
conn.close()
os.chmod(db_path, 0o664)
print('📝 Database pre-created with proper permissions')
" 2>/dev/null || echo "ℹ️ Database will be created by application"
fi
# Handle WAL and SHM files
for ext in wal shm; do
local aux_file="${db_file}-${ext}"
if [ -f "$aux_file" ]; then
chown "$target_uid:$target_gid" "$aux_file"
chmod 664 "$aux_file"
echo "✅ Fixed permissions for $(basename "$aux_file")"
fi
done
else
# Running as non-root - perform thorough validation
echo "👤 Non-root mode - validating permissions for user $(id)"
if [ ! -w "/app/instance" ]; then
echo "❌ CRITICAL ERROR: /app/instance is not writable"
echo "Current permissions:"
ls -la /app/instance 2>/dev/null || ls -la /app/
echo ""
echo "🔧 SOLUTION: Run these commands on your host:"
echo " docker-compose down"
echo " sudo chown -R $(id -u):$(id -g) ./data"
echo " chmod 755 ./data"
echo " docker-compose up -d"
exit 1
fi
# Test write capability thoroughly
local test_file="/app/instance/write_test_$(date +%s)"
if ! touch "$test_file" 2>/dev/null; then
echo "❌ CRITICAL ERROR: Cannot create files in /app/instance"
exit 1
else
rm -f "$test_file"
echo "✅ Directory write test passed"
fi
# Validate database file if it exists
if [ -f "/app/instance/subscriptions.db" ]; then
if [ ! -w "/app/instance/subscriptions.db" ]; then
echo "❌ CRITICAL ERROR: Database file is not writable"
ls -la /app/instance/subscriptions.db
echo ""
echo "🔧 SOLUTION: Run these commands on your host:"
echo " sudo chown $(id -u):$(id -g) ./data/subscriptions.db"
echo " chmod 664 ./data/subscriptions.db"
exit 1
else
echo "✅ Database file is writable"
# Test database operations
python3 -c "
import sqlite3
import sys
try:
conn = sqlite3.connect('/app/instance/subscriptions.db')
conn.execute('CREATE TABLE IF NOT EXISTS permission_test (id INTEGER)')
conn.execute('DROP TABLE permission_test')
conn.commit()
conn.close()
print('✅ Database functionality test PASSED')
except Exception as e:
print(f'❌ Database functionality test FAILED: {e}')
sys.exit(1)
" || exit 1
fi
else
echo "📝 Database will be created by the application"
fi
fi
echo "✅ Database initialization completed successfully"
fi
}
# Validate database file after application starts
validate_database() {
if [[ "$1" == *"python"* ]] || [[ "$1" == *"gunicorn"* ]] || [[ "$1" == *"run.py"* ]]; then
# Give the application a moment to start and potentially create the database
sleep 3
echo "🔍 Post-startup database validation..."
if [ -f "/app/instance/subscriptions.db" ]; then
if [ -w "/app/instance/subscriptions.db" ]; then
echo "✅ Database file exists and is writable"
# Quick functionality test
if python3 -c "
import sqlite3
import sys
try:
conn = sqlite3.connect('/app/instance/subscriptions.db')
conn.execute('SELECT name FROM sqlite_master WHERE type=\"table\" LIMIT 1')
conn.close()
print('✅ Database is functional')
except Exception as e:
print(f'⚠️ Database issue: {e}')
" 2>/dev/null; then
echo "🎉 Database validation passed!"
else
echo "⚠️ Database may have issues, but continuing..."
fi
else
echo "❌ Database file exists but is not writable!"
ls -la /app/instance/subscriptions.db
echo "💡 This may cause 'read-only database' errors"
fi
else
echo "ℹ️ Database file not yet created (normal for first run)"
fi
fi
}
# Main execution
main() {
echo "🚀 Starting Subscription Tracker..."
echo "Initial user: $(id -u):$(id -g)"
# Handle PUID/GUID mapping first
setup_user_mapping
# Set up required directories and environment
ensure_writable_dirs
setup_temp_dirs
# Initialize database with proper permissions
init_database "$@"
# Determine execution method based on current state
if is_user_directive; then
echo "👤 User directive mode: Running directly as $(id -u):$(id -g)"
# Start validation in background
(sleep 5 && validate_database "$@") &
exec "$@"
elif should_drop_privileges; then
echo "🔽 Dropping privileges to ${APP_USER}:${APP_GROUP}"
# Start validation in background
(sleep 5 && validate_database "$@") &
exec gosu ${APP_USER}:${APP_GROUP} "$@"
else
echo "▶️ Running with current user privileges $(id -u):$(id -g)"
# Start validation in background
(sleep 5 && validate_database "$@") &
exec "$@"
fi
}
# Run main function
main "$@"