Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build stage
FROM python:3.13-slim as builder

Check warning on line 2 in Dockerfile

View workflow job for this annotation

GitHub Actions / build

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/

WORKDIR /app

Expand Down Expand Up @@ -50,9 +50,10 @@
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 \
Expand Down
8 changes: 7 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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://'):
Expand Down
48 changes: 48 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines +119 to +145
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern matching logic is fragile and could match unintended commands. Consider using a more explicit check like case "$1" in with specific command patterns or checking the actual command name.

Suggested change
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
case "$1" in
python|python3|gunicorn|*/run.py)
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
;;
*)
# Not a main application command, do nothing
;;
esac

Copilot uses AI. Check for mistakes.
}

# Main execution
main() {
echo "Starting Subscription Tracker..."
Expand All @@ -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}"
Expand Down
107 changes: 107 additions & 0 deletions init_db.py
Original file line number Diff line number Diff line change
@@ -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')
Comment on lines +13 to +17
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded path '/app' reduces portability. Consider using relative paths or environment variables to make the script more flexible across different deployment environments.

Suggested change
sys.path.insert(0, '/app')
def check_database_permissions():
"""Check and fix database file permissions"""
instance_dir = Path('/app/instance')
APP_HOME = os.environ.get('APP_HOME', str(Path(__file__).resolve().parent))
sys.path.insert(0, APP_HOME)
def check_database_permissions():
"""Check and fix database file permissions"""
instance_dir = Path(APP_HOME) / 'instance'

Copilot uses AI. Check for mistakes.
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"))
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The execute() method is deprecated in SQLAlchemy 2.0+. Use db.session.execute() instead to avoid compatibility issues.

Suggested change
result = db.engine.execute(db.text("SELECT 1 as test"))
result = db.session.execute(db.text("SELECT 1 as test"))

Copilot uses AI. Check for mistakes.
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()
Loading