From 3706e63beddeeccc1b5809fdb4677f9f2ba49ca6 Mon Sep 17 00:00:00 2001 From: Matt Grutza Date: Wed, 21 Jan 2026 11:14:28 -0600 Subject: [PATCH 1/4] 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 From 236b2307e1e7878ddb2a2bb67ea4ede7312479cd Mon Sep 17 00:00:00 2001 From: Matt Grutza Date: Wed, 21 Jan 2026 17:40:36 -0600 Subject: [PATCH 2/4] Add health check and fix dangerous checks - docker-compose: added service health checks - dockerfile: added if logic for sed command so missing files do not cause build fail - entrypoint.celery: change migration detection to not continue until ALL migrations are applied - entrypoint: trim django key and change external postgres check to python in case postgres binaries are missing --- docker/Dockerfile | 10 ++++++---- docker/docker-compose.yml | 25 ++++++++++++++++++++----- docker/entrypoint.celery.sh | 6 +++--- docker/entrypoint.sh | 17 ++++++++++++++--- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 409f1096..3e30a825 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,10 +30,12 @@ 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 +# Fix line endings and make entrypoint scripts executable +RUN for f in /app/docker/entrypoint*.sh; do \ + if [ -f "$f" ]; then \ + sed -i 's/\r$//' "$f" && chmod +x "$f"; \ + fi; \ + done # 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 df295f90..e9254d30 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -7,8 +7,10 @@ services: volumes: - ./data:/data depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_healthy environment: - DISPATCHARR_ENV=modular - POSTGRES_HOST=db @@ -50,9 +52,12 @@ services: image: ghcr.io/dispatcharr/dispatcharr:latest container_name: dispatcharr_celery depends_on: - - db - - redis - - web + db: + condition: service_healthy + redis: + condition: service_healthy + web: + condition: service_started volumes: - ./data:/data extra_hosts: @@ -85,10 +90,20 @@ services: - POSTGRES_PASSWORD=secret volumes: - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U dispatch -d dispatcharr"] + interval: 5s + timeout: 5s + retries: 5 redis: image: redis:latest container_name: dispatcharr_redis + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 volumes: postgres_data: diff --git a/docker/entrypoint.celery.sh b/docker/entrypoint.celery.sh index fafe2c3f..0516cc54 100644 --- a/docker/entrypoint.celery.sh +++ b/docker/entrypoint.celery.sh @@ -7,11 +7,11 @@ 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) +export DJANGO_SECRET_KEY="$(tr -d '\r\n' < /data/jwt)" -# Wait for migrations to complete +# Wait for migrations to complete (check that NO unapplied migrations remain) echo 'Waiting for migrations to complete...' -until python manage.py showmigrations 2>&1 | grep -q '\[X\]'; do +until ! python manage.py showmigrations 2>&1 | grep -q '\[ \]'; do echo 'Migrations not ready yet, waiting...' sleep 2 done diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 36948c1f..0c17b9d0 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -66,7 +66,7 @@ PY mv -f "$tmpfile" "$SECRET_FILE" || { echo "move failed"; rm -f "$tmpfile"; exit 1; } umask $old_umask fi -export DJANGO_SECRET_KEY="$(cat "$SECRET_FILE")" +export DJANGO_SECRET_KEY="$(tr -d '\r\n' < "$SECRET_FILE")" # Process priority configuration # UWSGI_NICE_LEVEL: Absolute nice value for uWSGI/streaming (default: 0 = normal priority) @@ -174,9 +174,20 @@ if [[ "$DISPATCHARR_ENV" != "modular" ]]; then pids+=("$postgres_pid") else echo "🔗 Modular mode: Using external PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}" - # Wait for external PostgreSQL to be ready + # Wait for external PostgreSQL to be ready using Python (no pg_isready needed) 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 + until python3 -c " +import socket +import sys +try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(2) + s.connect(('${POSTGRES_HOST}', ${POSTGRES_PORT})) + s.close() + sys.exit(0) +except Exception: + sys.exit(1) +" 2>/dev/null; do echo_with_timestamp "Waiting for PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}..." sleep 1 done From 7a46577d2ec89ac65f20ae7249cc4194659fde7e Mon Sep 17 00:00:00 2001 From: Matt Grutza Date: Wed, 21 Jan 2026 17:46:07 -0600 Subject: [PATCH 3/4] Remove health check conditions Not supported on compose v3+ --- docker/docker-compose.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e9254d30..6673f896 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -7,10 +7,8 @@ services: volumes: - ./data:/data depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy + - db + - redis environment: - DISPATCHARR_ENV=modular - POSTGRES_HOST=db @@ -52,12 +50,9 @@ services: image: ghcr.io/dispatcharr/dispatcharr:latest container_name: dispatcharr_celery depends_on: - db: - condition: service_healthy - redis: - condition: service_healthy - web: - condition: service_started + - db + - redis + - web volumes: - ./data:/data extra_hosts: From 8c2ffbe76a82972de35493826b3d29591e2240cf Mon Sep 17 00:00:00 2001 From: Matt Grutza Date: Wed, 21 Jan 2026 17:59:23 -0600 Subject: [PATCH 4/4] CELERY_NICE_LEVEL warning Changed the celery entrypoint to include a warning if CELERY_NICE_LEVEL is negative AND SYS_NICE is not configured (SYS_NICE is required for negative levels) --- docker/entrypoint.celery.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker/entrypoint.celery.sh b/docker/entrypoint.celery.sh index 0516cc54..81cece86 100644 --- a/docker/entrypoint.celery.sh +++ b/docker/entrypoint.celery.sh @@ -19,4 +19,11 @@ 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 + +# Default to nice level 5 (lower priority) - safe for unprivileged containers +# Negative values require SYS_NICE capability +NICE_LEVEL="${CELERY_NICE_LEVEL:-5}" +if [ "$NICE_LEVEL" -lt 0 ] 2>/dev/null; then + echo "Warning: CELERY_NICE_LEVEL=$NICE_LEVEL is negative, requires SYS_NICE capability" +fi +nice -n "$NICE_LEVEL" celery -A dispatcharr worker -l info --autoscale=6,1