diff --git a/Dockerfile b/Dockerfile index 8d281d6..3fdab1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,9 +50,10 @@ COPY --chown=appuser:appgroup . . RUN chmod +x /app/docker-entrypoint.sh # Create writable directories for read-only filesystem compatibility -RUN mkdir -p /tmp/app-runtime /var/tmp/app \ - && chown -R appuser:appgroup /tmp/app-runtime /var/tmp/app \ - && chmod 755 /tmp/app-runtime /var/tmp/app +RUN mkdir -p /tmp/app-runtime /var/tmp/app /app/instance \ + && chown -R appuser:appgroup /tmp/app-runtime /var/tmp/app /app/instance \ + && chmod 755 /tmp/app-runtime /var/tmp/app \ + && chmod 775 /app/instance ENV FLASK_APP=run.py \ USER=appuser \ diff --git a/config.py b/config.py index 3b327c7..5787daa 100644 --- a/config.py +++ b/config.py @@ -3,7 +3,13 @@ def normalize_database_url(): """Normalize DATABASE_URL to use correct driver for psycopg3""" - database_url = os.environ.get('DATABASE_URL') or 'sqlite:///subscriptions.db' + database_url = os.environ.get('DATABASE_URL') + + # Default SQLite path - ensure it's in the writable instance directory + if not database_url: + # Use absolute path to ensure database is in the mounted volume + instance_dir = os.path.abspath('/app/instance') + database_url = f'sqlite:///{instance_dir}/subscriptions.db' # Convert postgresql:// or postgres:// to postgresql+psycopg:// for psycopg3 if database_url.startswith('postgresql://') or database_url.startswith('postgres://'): diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 32690da..63c107a 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -73,6 +73,19 @@ ensure_writable_dirs() { chown "$owner:$group" /app/instance 2>/dev/null || true fi fi + + # Ensure proper permissions on instance directory + chmod 755 /app/instance 2>/dev/null || true + + # If SQLite database exists, ensure it has proper permissions + if [ -f "/app/instance/subscriptions.db" ]; then + chmod 664 /app/instance/subscriptions.db 2>/dev/null || true + if [ "$(id -u)" = "0" ]; then + local owner="${PUID:-$APP_USER}" + local group="${GUID:-$APP_GROUP}" + chown "$owner:$group" /app/instance/subscriptions.db 2>/dev/null || true + fi + fi } # Set up temporary directories for application runtime @@ -100,6 +113,38 @@ should_drop_privileges() { [ "$(id -u)" = "0" ] } +# Initialize database with proper permissions +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..." + + # Create database directory if it doesn't exist + mkdir -p /app/instance + + # Set proper permissions for database operations + if [ "$(id -u)" = "0" ]; then + local owner="${PUID:-$APP_USER}" + local group="${GUID:-$APP_GROUP}" + chown "$owner:$group" /app/instance + chmod 755 /app/instance + + # If database file exists, fix its permissions + if [ -f "/app/instance/subscriptions.db" ]; then + chown "$owner:$group" /app/instance/subscriptions.db + chmod 664 /app/instance/subscriptions.db + fi + else + # Running as non-root, ensure we can write to the directory + if [ ! -w "/app/instance" ]; then + echo "WARNING: /app/instance is not writable by current user $(id -u):$(id -g)" + echo "Please ensure the mounted volume has proper permissions:" + echo " sudo chown -R $(id -u):$(id -g) ./data" + fi + fi + fi +} + # Main execution main() { echo "Starting Subscription Tracker..." @@ -112,6 +157,9 @@ main() { ensure_writable_dirs setup_temp_dirs + # Initialize database with proper permissions + init_database "$@" + # Drop privileges if running as root, otherwise run directly if should_drop_privileges; then echo "Dropping privileges to ${APP_USER}:${APP_GROUP}" diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..9683d3f --- /dev/null +++ b/init_db.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Database initialization script for Subscription Tracker +Ensures database is created with proper permissions and structure +""" + +import os +import sys +import stat +from pathlib import Path + +# Add the app directory to Python path +sys.path.insert(0, '/app') + +def check_database_permissions(): + """Check and fix database file permissions""" + instance_dir = Path('/app/instance') + db_file = instance_dir / 'subscriptions.db' + + print(f"Checking database permissions...") + print(f"Instance directory: {instance_dir}") + print(f"Database file: {db_file}") + + # Check if instance directory exists and is writable + if not instance_dir.exists(): + print(f"ERROR: Instance directory {instance_dir} does not exist!") + return False + + if not os.access(instance_dir, os.W_OK): + print(f"ERROR: Instance directory {instance_dir} is not writable!") + print(f"Current permissions: {oct(instance_dir.stat().st_mode)[-3:]}") + print(f"Owner: {instance_dir.stat().st_uid}:{instance_dir.stat().st_gid}") + print(f"Current user: {os.getuid()}:{os.getgid()}") + return False + + # Check database file if it exists + if db_file.exists(): + if not os.access(db_file, os.W_OK): + print(f"ERROR: Database file {db_file} is not writable!") + print(f"Current permissions: {oct(db_file.stat().st_mode)[-3:]}") + print(f"Owner: {db_file.stat().st_uid}:{db_file.stat().st_gid}") + return False + else: + print(f"Database file exists and is writable") + else: + print(f"Database file does not exist yet (will be created)") + + print("Database permissions check passed!") + return True + +def initialize_database(): + """Initialize the database with proper Flask app context""" + try: + from app import create_app, db + + # Create Flask app + app = create_app() + + with app.app_context(): + print("Creating database tables...") + + # Create all tables + db.create_all() + + # Verify tables were created + from sqlalchemy import inspect + inspector = inspect(db.engine) + tables = inspector.get_table_names() + + print(f"Created {len(tables)} tables: {', '.join(tables)}") + + # Test database write capability + print("Testing database write capability...") + result = db.engine.execute(db.text("SELECT 1 as test")) + test_result = result.fetchone() + print(f"Database connection test: {'PASSED' if test_result else 'FAILED'}") + + return True + + except Exception as e: + print(f"ERROR: Failed to initialize database: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Main initialization function""" + print("=" * 50) + print("Database Initialization Script") + print("=" * 50) + + # Check permissions first + if not check_database_permissions(): + print("Permission check failed!") + sys.exit(1) + + # Initialize database + if not initialize_database(): + print("Database initialization failed!") + sys.exit(1) + + print("=" * 50) + print("Database initialization completed successfully!") + print("=" * 50) + +if __name__ == '__main__': + main() \ No newline at end of file