#!/bin/bash set -e echo "Starting DigiServer v2..." # --------------------------------------------------------------------------- # Pin the database explicitly. # # The migration scripts and the application must target the SAME SQLite file. # Without this, they do not: the config classes default to different files # (dev.db for DevelopmentConfig, dashboard.db for ProductionConfig), and most # migration scripts call create_app() without an argument, which selects the # *development* config. The app itself runs as create_app('production'). # Exporting DATABASE_URL makes every code path below resolve to the same # database, regardless of which config gets loaded. # --------------------------------------------------------------------------- export DATABASE_URL="${DATABASE_URL:-sqlite:////app/instance/dashboard.db}" export FLASK_ENV="${FLASK_ENV:-production}" echo "Database: ${DATABASE_URL}" # Create necessary directories mkdir -p /app/instance mkdir -p /app/app/static/uploads # Staged player source (bind-mounted from ./data/player). Created here so the # container works even if the volume was not pre-created on the host. mkdir -p /app/data/player # --------------------------------------------------------------------------- # Ensure the schema exists and bootstrap the admin user. # # Both operations are idempotent, so this runs on every container start: # db.create_all() only creates missing tables, and the admin block creates the # user if absent or otherwise refreshes its password from the environment. # --------------------------------------------------------------------------- echo "Ensuring database schema and admin user..." python -c " from app.app import create_app from app.extensions import db, bcrypt from app.models import User import os app = create_app('production') with app.app_context(): db.create_all() admin_username = os.getenv('ADMIN_USERNAME', 'admin') admin_password = os.getenv('ADMIN_PASSWORD', 'admin123') admin = User.query.filter_by(username=admin_username).first() hashed = bcrypt.generate_password_hash(admin_password).decode('utf-8') if not admin: admin = User(username=admin_username, password=hashed, role='admin') db.session.add(admin) print(f'✅ Admin user created ({admin_username})') else: # Keep the stored password in sync with the environment. admin.password = hashed print(f'✅ Admin user password updated ({admin_username})') db.session.commit() " # --------------------------------------------------------------------------- # Apply schema migrations. # # Every migration script is idempotent (it guards against duplicate columns and # already-migrated tables), so this is safe to run on every start and upgrades # databases created by older releases. # # ORDER MATTERS: migrations that create/repair a table must run before the ones # that alter that table (e.g. add_player_user_table.py before # migrate_player_user_global.py, add_https_config_table.py before # add_email_to_https_config.py). # # Migrations are deliberately NON-FATAL: a failure is logged and startup # continues, so one bad migration cannot strand the container in a restart # loop. Look for the WARNING lines in the logs if something looks wrong. # --------------------------------------------------------------------------- MIGRATIONS=( "add_https_config_table.py" "add_player_user_table.py" "add_email_to_https_config.py" "migrate_player_user_global.py" "add_url_to_content.py" "add_original_filename_to_content.py" "add_deployment_fields_to_player.py" ) echo "Running database migrations..." FAILED_MIGRATIONS=() for migration in "${MIGRATIONS[@]}"; do migration_path="/app/migrations/${migration}" if [ ! -f "$migration_path" ]; then echo "⚠️ Skipping missing migration: ${migration}" continue fi echo " • ${migration}" if ! python "$migration_path"; then echo "⚠️ WARNING: migration failed: ${migration}" FAILED_MIGRATIONS+=("${migration}") fi done if [ ${#FAILED_MIGRATIONS[@]} -gt 0 ]; then echo "⚠️ WARNING: ${#FAILED_MIGRATIONS[@]} migration(s) failed: ${FAILED_MIGRATIONS[*]}" echo "⚠️ Starting anyway — check the errors above." else echo "✅ All database migrations applied." fi # --------------------------------------------------------------------------- # Bootstrap HTTPS from environment variables. # # HOSTNAME_INTERNAL + HOST_IP (compose → env) configure Caddy for HTTPS at # startup, so a fresh deployment is reachable over TLS without a manual step. # When either is unset this is a deliberate NO-OP: the app stays on plain HTTP # and HTTPS can be enabled later from Admin → HTTPS Configuration, which # reloads Caddy live. # # ORDERING MATTERS: gunicorn is started FIRST (below) and only then is HTTPS # configured. The bootstrap probes https://…/api/health to decide whether the # certificate really works; if it ran before the app was listening, Caddy would # return 502 and the probe would wrongly conclude HTTPS was broken. # # Non-fatal: if this fails the app still runs, and the admin page remains the # fallback path for configuring HTTPS. # --------------------------------------------------------------------------- bootstrap_https() { echo "Checking HTTPS bootstrap..." if ! python /app/https_manager.py bootstrap; then echo "⚠️ WARNING: HTTPS bootstrap failed — continuing; configure HTTPS from the admin UI." fi } # Start the application # --timeout is a safety net for any remaining synchronous long operation. The # player build no longer blocks a worker (it runs in a background thread), but # a generous margin avoids surprise worker kills during large uploads or # dependency installs triggered from the admin UI. echo "Starting Gunicorn..." gunicorn \ --bind 0.0.0.0:5000 \ --workers 4 \ --timeout 300 \ --access-logfile - \ --error-logfile - \ "app.app:create_app('production')" & GUNICORN_PID=$! # Wait for the app to answer before configuring HTTPS, then run the bootstrap. for _ in $(seq 1 30); do if python -c " import sys, urllib.request try: urllib.request.urlopen('http://127.0.0.1:5000/health', timeout=2) except Exception: sys.exit(1) " 2>/dev/null; then break fi sleep 1 done bootstrap_https # Keep the container attached to gunicorn so signals and healthchecks behave. wait $GUNICORN_PID