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
118 changes: 45 additions & 73 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,76 +1,48 @@
# PixelProbe Environment Configuration
# Copy this file to .env and customize for your deployment
# Copy this file to .env and customize the deployment values.

# REQUIRED: Secure secret key for Flask sessions
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=your-secure-random-key-here-replace-this
# Required
SECRET_KEY=replace-with-a-random-secret
POSTGRES_PASSWORD=replace-with-a-strong-password

# Database configuration (PostgreSQL REQUIRED since v2.2.0)
# When using docker-compose.yml, the POSTGRES_* variables below are used instead.
# DATABASE_URL is only needed for standalone (non-Docker) deployments.
# DATABASE_URL=postgresql://pixelprobe:changeme@localhost:5432/pixelprobe

# PostgreSQL credentials (used by docker-compose.yml)
POSTGRES_PASSWORD=changeme

# Container identity. Use your host UID:GID when mounted media or instance
# directories are owned by a non-default account.
PUID=10001
PGID=10001

# Cookie security defaults. Keep HTTPS enabled in production. Set false only
# for an explicitly plain-HTTP local or LAN deployment.
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=Lax
REMEMBER_COOKIE_DURATION_DAYS=30

# Scanning configuration
# Comma-separated list of directories to monitor (inside container)
SCAN_PATHS=/media

# Path to your media files on the host system (mounted read-only into container)
# Host directories and container identity
MEDIA_PATH=/path/to/your/media

# Performance tuning
# MAX_WORKERS: parallel file scanning threads per task (default: 10)
MAX_WORKERS=10

# Scheduler ownership (default: true). In multi-container deployments set
# false on the web container so only the Celery worker runs the scheduler.
# SCHEDULER_ENABLED=true

# Gunicorn listen address; comma-separated list allowed (default: 0.0.0.0:5000)
# For IPv6/dual-stack (e.g. rootless podman) use: GUNICORN_BIND=[::]:5000
# GUNICORN_BIND=0.0.0.0:5000
# BATCH_SIZE: files per database batch commit (default: 100)
BATCH_SIZE=100

# Celery configuration
# CELERY_CONCURRENCY: number of concurrent Celery tasks (default: 4)
CELERY_CONCURRENCY=4

# Timezone configuration
TZ=America/New_York

# Scheduling (optional)
# Periodic scan schedule (examples):
# PERIODIC_SCAN_SCHEDULE=cron:0 2 * * * # Daily at 2 AM
# PERIODIC_SCAN_SCHEDULE=interval:hours:6 # Every 6 hours
PERIODIC_SCAN_SCHEDULE=

# Cleanup schedule (examples):
# CLEANUP_SCHEDULE=cron:0 3 * * 0 # Weekly on Sunday at 3 AM
# CLEANUP_SCHEDULE=interval:days:7 # Every 7 days
CLEANUP_SCHEDULE=

# Exclusions
# Path exclusions (comma-separated)
EXCLUDED_PATHS=/media/temp,/media/cache
# Extension exclusions (comma-separated)
EXCLUDED_EXTENSIONS=.tmp,.temp,.cache

# SSRF trusted hosts (optional)
# Hostnames and/or CIDR ranges that bypass private-IP blocking for healthcheck pings.
# Comma-separated. Example: myhost.local,192.168.5.0/24
TRUSTED_INTERNAL_HOSTS=
SCAN_PATHS=/media
# PUID=10001
# PGID=10001

# Keep HTTPS cookie protection enabled. Set false only for plain HTTP.
# SESSION_COOKIE_SECURE=false

# Optional startup settings. Uncomment only when changing the Compose defaults.
# TZ=UTC
# CELERY_CONCURRENCY sets the worker process count.
# CELERY_CONCURRENCY=4
# CELERY_LOG_LEVEL=INFO
# MAX_WORKERS caps selected-file rescan threads; the API defaults to 4.
# MAX_WORKERS=10
# BATCH_SIZE sets the legacy discovery database lookup batch size.
# BATCH_SIZE=100
# MAX_OUTPUT_SIZE=10000
# OUTPUT_ROTATION_ENABLED=true
# FLASK_ENV=production

# Startup seeds this value only when no saved row exists; saved values win.
# FREEZE_DETECTION_ENABLED=true

# Legacy startup import only. Active exclusions are stored in PostgreSQL after
# the one-time import; these values do not override saved rules at runtime.
# EXCLUDED_PATHS=/media/temp,/media/cache
# EXCLUDED_EXTENSIONS=.tmp,.temp,.cache
# Legacy exclusions.json keys: paths, extensions, filename_patterns.
# Built-in filename patterns remain excluded after configured rules are cleared.

# Optional SSRF allowlist for internal hosts/CIDRs.
# TRUSTED_INTERNAL_HOSTS=healthcheck.internal.local,192.168.5.0/24

# Deprecated compatibility override read by app.py if passed to the process.
# Current Compose uses POSTGRES_* and does not forward DATABASE_URL.
# DATABASE_URL=postgresql://pixelprobe:password@localhost:5432/pixelprobe

# Optional startup schedules run alongside schedules stored in the database.
# PERIODIC_SCAN_SCHEDULE=cron:0 2 * * *
# CLEANUP_SCHEDULE=cron:0 3 * * 0
16 changes: 16 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.10.4] - 2026-10-10

### Fixed

- Mobile dialogs and the sidebar fit the visible browser height and keep background scrolling locked until the last overlay closes.

## [2.10.3] - 2026-10-10

### Changed

- Compose shares app and worker startup settings, and `.env.example` separates required values from optional overrides. The configuration guide documents six shared read-only media mounts and the one-time import and database runtime behavior for exclusions.

### Fixed

- Saved exclusions now apply to scans, scheduled scans, and selected-file rescans. Legacy JSON and environment rules import once without restoring removed database entries, and API replacement and re-add operations handle existing rows.

## [2.10.2] - 2026-10-10

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ cd PixelProbe
cp .env.example .env
```

Before startup, edit `.env`: replace `SECRET_KEY` and `POSTGRES_PASSWORD`, set `MEDIA_PATH`, and choose `PUID` and `PGID`. Create the host runtime directory and make it writable by those configured values. The Compose default is `10001:10001`.
Before startup, edit `.env`: set `SECRET_KEY`, `POSTGRES_PASSWORD`, and `MEDIA_PATH`. `MEDIA_PATH` must name an existing host directory; Compose will fail if it is missing. The `PUID` and `PGID` defaults are `10001:10001`; change them only if the host directories need a different identity. Create the host `instance` directory and make it writable by the configured identity.

```bash
mkdir -p instance
Expand Down
103 changes: 32 additions & 71 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,40 @@
# so this is the main scan-throughput knob (8-12 reasonable; chunk work
# is mostly ffmpeg/imagemagick subprocess-bound)
#
# - MAX_WORKERS: Parallel workers for selected-file rescans (default: 10)
# - MAX_WORKERS: Maximum thread count for selected-file rescans (default: 10).
# The API defaults to 4 threads; this value sets the maximum.
#
# Connection math (keep under PostgreSQL max_connections, default 100):
# web: 4 gunicorn workers x (DB_POOL_SIZE 5 + DB_MAX_OVERFLOW 10) = 60 max
# celery: CELERY_CONCURRENCY children x (5 + 10) worst-case, few in practice
# plus ~MAX_WORKERS single checker connections during selected-file rescans

x-pixelprobe-environment: &pixelprobe-environment
SECRET_KEY: ${SECRET_KEY}
TZ: ${TZ:-UTC}
DB_POOL_SIZE: ${DB_POOL_SIZE:-5}
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: pixelprobe
POSTGRES_USER: pixelprobe
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
FLASK_ENV: ${FLASK_ENV:-production}
SCAN_PATHS: ${SCAN_PATHS:-/media}
EXCLUDED_PATHS: ${EXCLUDED_PATHS:-}
EXCLUDED_EXTENSIONS: ${EXCLUDED_EXTENSIONS:-.txt,.log,.md}
MAX_WORKERS: ${MAX_WORKERS:-10}
BATCH_SIZE: ${BATCH_SIZE:-100}
MAX_OUTPUT_SIZE: ${MAX_OUTPUT_SIZE:-10000}
OUTPUT_ROTATION_ENABLED: ${OUTPUT_ROTATION_ENABLED:-true}
# Legacy migration input; stored Tunables become authoritative after seeding.
FREEZE_DETECTION_ENABLED: ${FREEZE_DETECTION_ENABLED:-true}
PILLOW_BLOCKS_MAX: 256
PERIODIC_SCAN_SCHEDULE: ${PERIODIC_SCAN_SCHEDULE:-}
CLEANUP_SCHEDULE: ${CLEANUP_SCHEDULE:-}
TRUSTED_INTERNAL_HOSTS: ${TRUSTED_INTERNAL_HOSTS:-}

services:
# PostgreSQL database
# BREAKING (v2.7.0): default moved from postgres:15 to postgres:18. An
Expand Down Expand Up @@ -71,11 +98,11 @@ services:

# PixelProbe application
pixelprobe:
image: ttlequals0/pixelprobe:${PIXELPROBE_VERSION:-2.10.0}
image: ttlequals0/pixelprobe:${PIXELPROBE_VERSION:-2.10.3}
container_name: pixelprobe-app
environment:
<<: *pixelprobe-environment
# Security
SECRET_KEY: ${SECRET_KEY}
SESSION_COOKIE_SECURE: ${SESSION_COOKIE_SECURE:-true}
SESSION_COOKIE_HTTPONLY: ${SESSION_COOKIE_HTTPONLY:-true}
SESSION_COOKIE_SAMESITE: ${SESSION_COOKIE_SAMESITE:-Lax}
Expand All @@ -84,43 +111,10 @@ services:
GUNICORN_BIND: ${GUNICORN_BIND:-0.0.0.0:5000}
GUNICORN_WORKERS: ${GUNICORN_WORKERS:-4}
GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-300}
DB_POOL_SIZE: ${DB_POOL_SIZE:-5}
TZ: ${TZ:-UTC}

# Scheduler runs in celery-worker; keep the web container out of the
# scheduler lock entirely (set to "true" only in single-container setups)
SCHEDULER_ENABLED: "false"

# PostgreSQL database configuration
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: pixelprobe
POSTGRES_USER: pixelprobe
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}

# Celery configuration
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0

# Application settings
FLASK_ENV: ${FLASK_ENV:-production}
SCAN_PATHS: ${SCAN_PATHS:-/media}
EXCLUDED_PATHS: ${EXCLUDED_PATHS:-}
EXCLUDED_EXTENSIONS: ${EXCLUDED_EXTENSIONS:-.txt,.log,.md}
MAX_WORKERS: ${MAX_WORKERS:-10}
BATCH_SIZE: ${BATCH_SIZE:-100}
MAX_OUTPUT_SIZE: ${MAX_OUTPUT_SIZE:-10000}
OUTPUT_ROTATION_ENABLED: ${OUTPUT_ROTATION_ENABLED:-true}
FREEZE_DETECTION_ENABLED: ${FREEZE_DETECTION_ENABLED:-true}
PILLOW_BLOCKS_MAX: 256


# Scheduled scans
PERIODIC_SCAN_SCHEDULE: ${PERIODIC_SCAN_SCHEDULE:-}
CLEANUP_SCHEDULE: ${CLEANUP_SCHEDULE:-}

# SSRF trusted hosts (hostnames/CIDRs that bypass private-IP blocking)
TRUSTED_INTERNAL_HOSTS: ${TRUSTED_INTERNAL_HOSTS:-}

volumes:
# Media files to scan
Expand Down Expand Up @@ -167,47 +161,14 @@ services:

# P1 Celery Worker for distributed task processing
celery-worker:
image: ttlequals0/pixelprobe:${PIXELPROBE_VERSION:-2.10.0}
image: ttlequals0/pixelprobe:${PIXELPROBE_VERSION:-2.10.3}
container_name: pixelprobe-celery-worker
command: python celery_worker.py
environment:
<<: *pixelprobe-environment
# Celery configuration
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/0
CELERY_LOG_LEVEL: ${CELERY_LOG_LEVEL:-INFO}
CELERY_CONCURRENCY: ${CELERY_CONCURRENCY:-4}
DB_POOL_SIZE: ${DB_POOL_SIZE:-5}
TZ: ${TZ:-UTC}

# Database connection (worker needs database access)
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: pixelprobe
POSTGRES_USER: pixelprobe
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}

# Worker-specific settings
SECRET_KEY: ${SECRET_KEY}
FLASK_ENV: ${FLASK_ENV:-production}
MAX_WORKERS: ${MAX_WORKERS:-10} # Parallel file scanning workers per task
BATCH_SIZE: ${BATCH_SIZE:-100}
MAX_OUTPUT_SIZE: ${MAX_OUTPUT_SIZE:-10000}
OUTPUT_ROTATION_ENABLED: ${OUTPUT_ROTATION_ENABLED:-true}
FREEZE_DETECTION_ENABLED: ${FREEZE_DETECTION_ENABLED:-true}
PILLOW_BLOCKS_MAX: 256

# Scan configuration (required for scheduler)
SCAN_PATHS: ${SCAN_PATHS:-/media}
EXCLUDED_PATHS: ${EXCLUDED_PATHS:-}
EXCLUDED_EXTENSIONS: ${EXCLUDED_EXTENSIONS:-.txt,.log,.md}

# Scheduled scans (scheduler runs in celery-worker)
PERIODIC_SCAN_SCHEDULE: ${PERIODIC_SCAN_SCHEDULE:-}
CLEANUP_SCHEDULE: ${CLEANUP_SCHEDULE:-}

# SSRF trusted hosts (hostnames/CIDRs that bypass private-IP blocking)
TRUSTED_INTERNAL_HOSTS: ${TRUSTED_INTERNAL_HOSTS:-}


volumes:
# Media files access (read-only for scanning)
Expand Down
Loading
Loading