From 3706e63beddeeccc1b5809fdb4677f9f2ba49ca6 Mon Sep 17 00:00:00 2001 From: Matt Grutza Date: Wed, 21 Jan 2026 11:14:28 -0600 Subject: [PATCH] feat: Add DISPATCHARR_ENV=modular support for multi-container deployments - docker/entrypoint.sh: Conditional PostgreSQL init/startup for modular mode - docker/entrypoint.celery.sh: New dedicated entrypoint for celery container - prevents race condition in modular deployment - docker/uwsgi.modular.ini: New uWSGI config without Redis/Celery daemons - docker/Dockerfile: Add line ending fixes and chmod for entrypoints - adds cross-platform support for image builds - docker/docker-compose.yml: Configure modular mode with DISPATCHARR_ENV - uses new celery entrypoint and adds depends_on to prevent race condition --- docker/Dockerfile | 4 +++ docker/docker-compose.yml | 19 +++++++----- docker/entrypoint.celery.sh | 22 ++++++++++++++ docker/entrypoint.sh | 52 ++++++++++++++++++++++---------- docker/uwsgi.modular.ini | 59 +++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 docker/entrypoint.celery.sh create mode 100644 docker/uwsgi.modular.ini diff --git a/docker/Dockerfile b/docker/Dockerfile index bfb35c11..409f1096 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,6 +30,10 @@ WORKDIR /app COPY . /app # Copy nginx configuration COPY ./docker/nginx.conf /etc/nginx/sites-enabled/default +# Verify entrypoint scripts exist, fix line endings, and make them executable +RUN ls -la /app/docker/entrypoint*.sh && \ + sed -i 's/\r$//' /app/docker/entrypoint.sh /app/docker/entrypoint.celery.sh /app/docker/entrypoint.aio.sh && \ + chmod +x /app/docker/entrypoint.sh /app/docker/entrypoint.celery.sh /app/docker/entrypoint.aio.sh # Clean out existing frontend folder RUN rm -rf /app/frontend # Copy built frontend assets diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e4093e4b..df295f90 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -10,6 +10,7 @@ services: - db - redis environment: + - DISPATCHARR_ENV=modular - POSTGRES_HOST=db - POSTGRES_DB=dispatcharr - POSTGRES_USER=dispatch @@ -25,7 +26,6 @@ services: # Lower values = higher priority. Range: -20 (highest) to 19 (lowest) # Negative values require cap_add: SYS_NICE (uncomment below) #- UWSGI_NICE_LEVEL=-5 # uWSGI/FFmpeg/Streaming (default: 0, recommended: -5 for high priority) - #- CELERY_NICE_LEVEL=5 # Celery/EPG/Background tasks (default: 5, low priority) # # Uncomment to enable high priority for streaming (required if UWSGI_NICE_LEVEL < 0) #cap_add: @@ -52,22 +52,27 @@ services: depends_on: - db - redis + - web volumes: - - ../:/app + - ./data:/data extra_hosts: - "host.docker.internal:host-gateway" environment: + - DISPATCHARR_ENV=modular - POSTGRES_HOST=db - POSTGRES_DB=dispatcharr - POSTGRES_USER=dispatch - POSTGRES_PASSWORD=secret - REDIS_HOST=redis - CELERY_BROKER_URL=redis://redis:6379/0 - command: > - bash -c " - cd /app && - nice -n 5 celery -A dispatcharr worker -l info - " + - DISPATCHARR_LOG_LEVEL=info + #- CELERY_NICE_LEVEL=5 #Celery/EPG/Background tasks (default:5, low priority; Range: -20 to 19) + - DJANGO_SETTINGS_MODULE=dispatcharr.settings + - PYTHONUNBUFFERED=1 + # Uncomment to enable high priority for Celery (required if CELERY_NICE_LEVEL < 0) + #cap_add: + # - SYS_NICE + entrypoint: ["/app/docker/entrypoint.celery.sh"] db: image: postgres:14 diff --git a/docker/entrypoint.celery.sh b/docker/entrypoint.celery.sh new file mode 100644 index 00000000..fafe2c3f --- /dev/null +++ b/docker/entrypoint.celery.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +cd /app +source /dispatcharrpy/bin/activate + +# Wait for Django secret key +echo 'Waiting for Django secret key...' +while [ ! -f /data/jwt ]; do sleep 1; done +export DJANGO_SECRET_KEY=$(cat /data/jwt) + +# Wait for migrations to complete +echo 'Waiting for migrations to complete...' +until python manage.py showmigrations 2>&1 | grep -q '\[X\]'; do + echo 'Migrations not ready yet, waiting...' + sleep 2 +done + +# Start Celery +echo 'Migrations complete, starting Celery...' +celery -A dispatcharr beat -l info & +nice -n ${CELERY_NICE_LEVEL:-5} celery -A dispatcharr worker -l info --autoscale=6,1 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index a50f2f49..36948c1f 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -150,26 +150,43 @@ fi # Run init scripts echo "Starting user setup..." . /app/docker/init/01-user-setup.sh -echo "Setting up PostgreSQL..." -. /app/docker/init/02-postgres.sh + +# Initialize PostgreSQL if NOT in modular mode (using external database) +if [[ "$DISPATCHARR_ENV" != "modular" ]]; then + echo "Setting up PostgreSQL..." + . /app/docker/init/02-postgres.sh +fi + echo "Starting init process..." . /app/docker/init/03-init-dispatcharr.sh -# Start PostgreSQL -echo "Starting Postgres..." -su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} start -w -t 300 -o '-c port=${POSTGRES_PORT}'" -# Wait for PostgreSQL to be ready -until su - postgres -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do - echo_with_timestamp "Waiting for PostgreSQL to be ready..." - sleep 1 -done -postgres_pid=$(su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p') -echo "✅ Postgres started with PID $postgres_pid" -pids+=("$postgres_pid") +# Start PostgreSQL if NOT in modular mode (using external database) +if [[ "$DISPATCHARR_ENV" != "modular" ]]; then + echo "Starting Postgres..." + su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} start -w -t 300 -o '-c port=${POSTGRES_PORT}'" + # Wait for PostgreSQL to be ready + until su - postgres -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do + echo_with_timestamp "Waiting for PostgreSQL to be ready..." + sleep 1 + done + postgres_pid=$(su - postgres -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p') + echo "✅ Postgres started with PID $postgres_pid" + pids+=("$postgres_pid") +else + echo "🔗 Modular mode: Using external PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}" + # Wait for external PostgreSQL to be ready + echo_with_timestamp "Waiting for external PostgreSQL to be ready..." + until su - postgres -c "$PG_BINDIR/pg_isready -h ${POSTGRES_HOST} -p ${POSTGRES_PORT}" >/dev/null 2>&1; do + echo_with_timestamp "Waiting for PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}..." + sleep 1 + done + echo "✅ External PostgreSQL is ready" +fi -# Ensure database encoding is UTF8 -. /app/docker/init/02-postgres.sh -ensure_utf8_encoding +# Ensure database encoding is UTF8 (only for internal database) +if [[ "$DISPATCHARR_ENV" != "modular" ]]; then + ensure_utf8_encoding +fi if [[ "$DISPATCHARR_ENV" = "dev" ]]; then . /app/docker/init/99-init-dev.sh @@ -197,6 +214,9 @@ if [ "$DISPATCHARR_ENV" = "dev" ] && [ "$DISPATCHARR_DEBUG" != "true" ]; then elif [ "$DISPATCHARR_DEBUG" = "true" ]; then echo "🚀 Starting uwsgi in debug mode..." uwsgi_file="/app/docker/uwsgi.debug.ini" +elif [ "$DISPATCHARR_ENV" = "modular" ]; then + echo "🚀 Starting uwsgi in modular mode..." + uwsgi_file="/app/docker/uwsgi.modular.ini" else echo "🚀 Starting uwsgi in production mode..." uwsgi_file="/app/docker/uwsgi.ini" diff --git a/docker/uwsgi.modular.ini b/docker/uwsgi.modular.ini new file mode 100644 index 00000000..3220a8d8 --- /dev/null +++ b/docker/uwsgi.modular.ini @@ -0,0 +1,59 @@ +[uwsgi] +; Modular deployment mode - external PostgreSQL, Redis, and Celery +; Remove file creation commands since we're not logging to files anymore +; exec-pre = mkdir -p /data/logs +; exec-pre = touch /data/logs/uwsgi.log +; exec-pre = chmod 666 /data/logs/uwsgi.log + +; First run Redis availability check script once +exec-pre = python /app/scripts/wait_for_redis.py + +; Start Daphne for WebSocket support (required for real-time features) +; Redis and Celery run in separate containers in modular mode +attach-daemon = daphne -b 0.0.0.0 -p 8001 dispatcharr.asgi:application + +# Core settings +chdir = /app +module = dispatcharr.wsgi:application +virtualenv = /dispatcharrpy +master = true +env = DJANGO_SETTINGS_MODULE=dispatcharr.settings +env = USE_NGINX_ACCEL=true +socket = /app/uwsgi.sock +chmod-socket = 777 +vacuum = true +die-on-term = true +static-map = /static=/app/static + +# Worker management +workers = 4 + +# Optimize for streaming +http = 0.0.0.0:5656 +http-keepalive = 1 +buffer-size = 65536 # Increase buffer for large payloads +post-buffering = 4096 # Reduce buffering for real-time streaming +http-timeout = 600 # Prevent disconnects from long streams +socket-timeout = 600 # Prevent write timeouts when client buffers +lazy-apps = true # Improve memory efficiency + +# Async mode (use gevent for high concurrency) +gevent = 400 # Each unused greenlet costs ~2-4KB of memory +# Higher values have minimal performance impact when idle, but provide capacity for traffic spikes +# If memory usage becomes an issue, reduce this value + +# Performance tuning +thunder-lock = true +log-4xx = true +log-5xx = true +disable-logging = false + +# Logging configuration +# Enable console logging (stdout) +log-master = true +# Enable strftime formatting for timestamps +logformat-strftime = true +log-date = %%Y-%%m-%%d %%H:%%M:%%S,000 +# Use formatted time with environment variable for log level +log-format = %(ftime) $(DISPATCHARR_LOG_LEVEL) uwsgi.requests Worker ID: %(wid) %(method) %(status) %(uri) %(msecs)ms +log-buffering = 1024 # Add buffer size limit for logging