Proyecto LCX Dispatcharr multicuenta
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
FROM lscr.io/linuxserver/ffmpeg:latest
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV UV_PROJECT_ENVIRONMENT=/dispatcharrpy
|
||||
ENV VIRTUAL_ENV=/dispatcharrpy
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# --- Install Python 3.13 and build dependencies ---
|
||||
# Note: Hardware acceleration (VA-API, VDPAU, NVENC) already included in base ffmpeg image
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
ca-certificates software-properties-common gnupg2 curl wget \
|
||||
&& add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
python3.13 python3.13-dev python3.13-venv libpython3.13 \
|
||||
libpcre3 libpcre3-dev libpq-dev procps pciutils \
|
||||
nginx comskip \
|
||||
vlc-bin vlc-plugin-base \
|
||||
build-essential gcc g++ gfortran libopenblas-dev libopenblas0 ninja-build
|
||||
|
||||
# --- Install UV ---
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# --- Create Python virtual environment and install dependencies ---
|
||||
WORKDIR /tmp/build
|
||||
COPY pyproject.toml /tmp/build/
|
||||
COPY version.py /tmp/build/
|
||||
COPY README.md /tmp/build/
|
||||
RUN uv sync --python 3.13 --no-cache --no-install-project --no-dev && \
|
||||
rm -rf /tmp/build
|
||||
WORKDIR /
|
||||
|
||||
# --- Build legacy NumPy wheel for old hardware (store for runtime switching) ---
|
||||
RUN uv pip install --python $UV_PROJECT_ENVIRONMENT/bin/python --no-cache build pip && \
|
||||
cd /tmp && \
|
||||
$UV_PROJECT_ENVIRONMENT/bin/python -m pip download --no-binary numpy --no-deps numpy && \
|
||||
tar -xzf numpy-*.tar.gz && \
|
||||
cd numpy-*/ && \
|
||||
$UV_PROJECT_ENVIRONMENT/bin/python -m build --wheel -Csetup-args=-Dcpu-baseline="none" -Csetup-args=-Dcpu-dispatch="none" && \
|
||||
mv dist/*.whl /opt/ && \
|
||||
cd / && rm -rf /tmp/numpy-* /tmp/*.tar.gz && \
|
||||
uv pip uninstall --python $UV_PROJECT_ENVIRONMENT/bin/python build pip
|
||||
|
||||
# --- Clean up build dependencies to reduce image size ---
|
||||
RUN apt-get remove -y build-essential gcc g++ gfortran libopenblas-dev libpcre3-dev python3.13-dev ninja-build && \
|
||||
apt-get autoremove -y --purge && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.cache /tmp/*
|
||||
|
||||
# --- Set up Redis 7.x ---
|
||||
RUN curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | \
|
||||
tee /etc/apt/sources.list.d/redis.list && \
|
||||
apt-get update && apt-get install -y redis-server && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# --- Set up PostgreSQL 17.x ---
|
||||
RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" | \
|
||||
tee /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && apt-get install -y postgresql-17 postgresql-contrib-17 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create render group for hardware acceleration support with GID 109
|
||||
RUN groupadd -r -g 109 render || true
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Define base image build arguments (must be before any FROM)
|
||||
ARG REPO_OWNER=dispatcharr
|
||||
ARG REPO_NAME=dispatcharr
|
||||
ARG BASE_TAG=base
|
||||
|
||||
# --- Build frontend ---
|
||||
|
||||
FROM node:24 AS frontend-builder
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY ./frontend /app/frontend
|
||||
# remove any node_modules that may have been copied from the host (x86)
|
||||
RUN rm -rf node_modules || true; \
|
||||
npm install --no-audit --progress=false;
|
||||
RUN npm run build && \
|
||||
rm -rf node_modules .cache
|
||||
|
||||
# --- Redeclare build arguments for the next stage ---
|
||||
ARG REPO_OWNER
|
||||
ARG REPO_NAME
|
||||
ARG BASE_TAG
|
||||
|
||||
# --- Final image based on the dynamic base ---
|
||||
FROM ghcr.io/${REPO_OWNER}/${REPO_NAME}:${BASE_TAG} AS final
|
||||
ENV VIRTUAL_ENV=/dispatcharrpy
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
WORKDIR /app
|
||||
|
||||
# Copy application code
|
||||
COPY . /app
|
||||
# Copy nginx configuration
|
||||
COPY ./docker/nginx.conf /etc/nginx/sites-enabled/default
|
||||
# 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
|
||||
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
||||
|
||||
# Add timestamp argument
|
||||
ARG TIMESTAMP
|
||||
|
||||
# Update version.py with build timestamp if provided
|
||||
RUN if [ -n "$TIMESTAMP" ]; then \
|
||||
echo "Updating timestamp to ${TIMESTAMP} in version.py" && \
|
||||
sed -i "s|__timestamp__ = None.*|__timestamp__ = '${TIMESTAMP}' # Set during CI/CD build process|" /app/version.py && \
|
||||
cat /app/version.py; \
|
||||
fi
|
||||
|
||||
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Default values
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
VERSION=$(python3 -c "import sys; sys.path.append('${ROOT_DIR}'); import version; print(version.__version__)")
|
||||
REGISTRY="dispatcharr" # Registry or private repo to push to
|
||||
IMAGE="dispatcharr" # Image that we're building
|
||||
BRANCH="dev"
|
||||
ARCH="" # Architectures to build for, e.g. linux/amd64,linux/arm64
|
||||
PUSH=false
|
||||
|
||||
usage() {
|
||||
cat <<-EOF
|
||||
To test locally:
|
||||
./build-dev.sh
|
||||
|
||||
To build and push to registry:
|
||||
./build-dev.sh -p
|
||||
|
||||
To build and push to a private registry:
|
||||
./build-dev.sh -p -r myregistry:5000
|
||||
|
||||
To build for -both- x86_64 and arm_64:
|
||||
./build-dev.sh -p -a linux/amd64,linux/arm64
|
||||
|
||||
Do it all:
|
||||
./build-dev.sh -p -r myregistry:5000 -a linux/amd64,linux/arm64
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse options
|
||||
while getopts "pr:a:b:i:h" opt; do
|
||||
case $opt in
|
||||
r) REGISTRY="$OPTARG" ;;
|
||||
a) ARCH="$OPTARG" ;;
|
||||
b) BRANCH="$OPTARG" ;;
|
||||
i) IMAGE="$OPTARG" ;;
|
||||
p) PUSH=true ;;
|
||||
h) usage ;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BUILD_ARGS="BRANCH=$BRANCH"
|
||||
ARCH_ARGS=()
|
||||
if [ -n "$ARCH" ]; then
|
||||
ARCH_ARGS=(--platform "$ARCH")
|
||||
fi
|
||||
|
||||
echo docker build --build-arg "$BUILD_ARGS" "${ARCH_ARGS[@]}" -t "$IMAGE"
|
||||
docker build -f "${SCRIPT_DIR}/Dockerfile" --build-arg "$BUILD_ARGS" "${ARCH_ARGS[@]}" -t "$IMAGE" "$ROOT_DIR"
|
||||
docker tag "$IMAGE" "$IMAGE":"$BRANCH"
|
||||
docker tag "$IMAGE" "$IMAGE":"$VERSION"
|
||||
|
||||
if [ "$PUSH" = "true" ]; then
|
||||
for TAG in latest "$VERSION" "$BRANCH"; do
|
||||
docker tag "$IMAGE" "$REGISTRY/$IMAGE:$TAG"
|
||||
docker push -q "$REGISTRY/$IMAGE:$TAG"
|
||||
done
|
||||
echo "Images pushed successfully."
|
||||
else
|
||||
echo "Please run 'docker push $IMAGE:$BRANCH' and 'docker push $IMAGE:${VERSION}' when ready"
|
||||
fi
|
||||
@@ -0,0 +1,6 @@
|
||||
; Minimal default comskip config
|
||||
edl_out=1
|
||||
output_edl=1
|
||||
verbose=0
|
||||
thread_count=0
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
dispatcharr:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
image: ghcr.io/dispatcharr/dispatcharr:latest
|
||||
restart: unless-stopped
|
||||
container_name: dispatcharr
|
||||
ports:
|
||||
- 9191:9191
|
||||
volumes:
|
||||
- dispatcharr_data:/data
|
||||
environment:
|
||||
- DISPATCHARR_ENV=aio
|
||||
- REDIS_HOST=localhost
|
||||
- CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
- DISPATCHARR_LOG_LEVEL=info
|
||||
# Legacy CPU Support (Optional)
|
||||
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
|
||||
# that lack support for newer baseline CPU features
|
||||
#- USE_LEGACY_NUMPY=true
|
||||
# Process Priority Configuration (Optional)
|
||||
# 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:
|
||||
# - SYS_NICE
|
||||
# Optional for hardware acceleration
|
||||
#devices:
|
||||
# - /dev/dri:/dev/dri # For Intel/AMD GPU acceleration (VA-API)
|
||||
# Uncomment the following lines for NVIDIA GPU support
|
||||
# NVidia GPU support (requires NVIDIA Container Toolkit)
|
||||
#deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
|
||||
|
||||
volumes:
|
||||
dispatcharr_data:
|
||||
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
dispatcharr:
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: docker/Dockerfile.dev
|
||||
image: ghcr.io/dispatcharr/dispatcharr:base
|
||||
container_name: dispatcharr_debug
|
||||
ports:
|
||||
- 5656:5656 # API port
|
||||
- 9193:9191 # Web UI port
|
||||
- 8001:8001 # Socket port
|
||||
- 5678:5678 # Debugging port
|
||||
volumes:
|
||||
- ../:/app
|
||||
environment:
|
||||
- DISPATCHARR_ENV=dev
|
||||
- DISPATCHARR_DEBUG=true
|
||||
- REDIS_HOST=localhost
|
||||
- CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
- DISPATCHARR_LOG_LEVEL=trace
|
||||
# Legacy CPU Support (Optional)
|
||||
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
|
||||
# that lack support for newer baseline CPU features
|
||||
#- USE_LEGACY_NUMPY=true
|
||||
# Process Priority Configuration (Optional)
|
||||
# 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:
|
||||
# - SYS_NICE
|
||||
@@ -0,0 +1,57 @@
|
||||
services:
|
||||
dispatcharr:
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: docker/Dockerfile.dev
|
||||
image: ghcr.io/dispatcharr/dispatcharr:base
|
||||
container_name: dispatcharr_dev
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5656:5656
|
||||
- 9191:9191
|
||||
- 8001:8001
|
||||
volumes:
|
||||
- ../:/app
|
||||
- ./data:/data
|
||||
environment:
|
||||
- DISPATCHARR_ENV=dev
|
||||
- REDIS_HOST=localhost
|
||||
- CELERY_BROKER_URL=redis://localhost:6379/0
|
||||
- DISPATCHARR_LOG_LEVEL=debug
|
||||
# Legacy CPU Support (Optional)
|
||||
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
|
||||
# that lack support for newer baseline CPU features
|
||||
#- USE_LEGACY_NUMPY=true
|
||||
# Process Priority Configuration (Optional)
|
||||
# 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:
|
||||
# - SYS_NICE
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: admin@admin.com
|
||||
PGADMIN_DEFAULT_PASSWORD: admin
|
||||
volumes:
|
||||
- dispatcharr_dev_pgadmin:/var/lib/pgadmin
|
||||
ports:
|
||||
- 8082:80
|
||||
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- REDIS_HOSTS=dispatcharr:dispatcharr:6379:0
|
||||
- TRUST_PROXY=true
|
||||
- ADDRESS=0.0.0.0
|
||||
ports:
|
||||
- 8081:8081
|
||||
|
||||
volumes:
|
||||
dispatcharr_dev_pgadmin:
|
||||
@@ -0,0 +1,229 @@
|
||||
# Dispatcharr - Modular Deployment Configuration
|
||||
# This compose file runs Dispatcharr in modular mode with separate containers
|
||||
# for web, celery workers, PostgreSQL database, and Redis cache.
|
||||
|
||||
services:
|
||||
# ============================================================================
|
||||
# Web Service
|
||||
# ============================================================================
|
||||
web:
|
||||
image: ghcr.io/dispatcharr/dispatcharr:latest
|
||||
container_name: dispatcharr_web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 9191:9191
|
||||
volumes:
|
||||
- ./data:/data
|
||||
#- ./certs:/certs:ro # TLS certificates (optional)
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
# --- Environment Configuration ---
|
||||
environment:
|
||||
# Deployment Mode
|
||||
- DISPATCHARR_ENV=modular
|
||||
|
||||
# PostgreSQL Connection
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=dispatcharr
|
||||
- POSTGRES_USER=dispatch
|
||||
- POSTGRES_PASSWORD=secret
|
||||
# PostgreSQL TLS (optional) — mount certs via the volume above
|
||||
#- POSTGRES_SSL=true # required to enable TLS
|
||||
#- POSTGRES_SSL_MODE=verify-full # optional: verify-full (default) | verify-ca | require
|
||||
#- POSTGRES_SSL_CA_CERT=/certs/postgres/ca.crt # optional: CA cert to verify the server
|
||||
#- POSTGRES_SSL_CERT=/certs/postgres/client.crt # optional: client cert (only if server requires client auth)
|
||||
#- POSTGRES_SSL_KEY=/certs/postgres/client.key # optional: client key (only if server requires client auth)
|
||||
|
||||
# Redis Connection
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
# Redis Authentication (Optional)
|
||||
# Uncomment and set if your Redis requires authentication:
|
||||
#- REDIS_PASSWORD=your_strong_redis_password
|
||||
#- REDIS_USER=your_redis_username # For Redis 6+ ACL - see Redis service below
|
||||
# Redis TLS (optional) — mount certs via the volume above
|
||||
#- REDIS_SSL=true # required to enable TLS
|
||||
#- REDIS_SSL_VERIFY=true # optional: set false for self-signed certs without a CA
|
||||
#- REDIS_SSL_CA_CERT=/certs/redis/ca.crt # optional: CA cert to verify the server
|
||||
#- REDIS_SSL_CERT=/certs/redis/client.crt # optional: client cert (only if server requires client auth)
|
||||
#- REDIS_SSL_KEY=/certs/redis/client.key # optional: client key (only if server requires client auth)
|
||||
|
||||
# Logging
|
||||
- DISPATCHARR_LOG_LEVEL=info
|
||||
|
||||
# Legacy CPU Support (Optional)
|
||||
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
|
||||
# that lack support for newer baseline CPU features:
|
||||
#- USE_LEGACY_NUMPY=true
|
||||
|
||||
# Process Priority Configuration (Optional)
|
||||
# Lower values = higher priority. Range: -20 (highest) to 19 (lowest)
|
||||
# Negative values require cap_add: SYS_NICE (see below)
|
||||
#- UWSGI_NICE_LEVEL=-5 # uWSGI/FFmpeg/Streaming (default: 0, recommended: -5 for high priority)
|
||||
|
||||
# --- Advanced Configuration ---
|
||||
# Uncomment to enable high priority for streaming (required if UWSGI_NICE_LEVEL < 0)
|
||||
#cap_add:
|
||||
# - SYS_NICE
|
||||
|
||||
# --- Hardware Acceleration (Optional) ---
|
||||
# Uncomment for GPU access (transcoding acceleration)
|
||||
#group_add:
|
||||
# - video
|
||||
# #- render # Uncomment if your GPU requires it
|
||||
#devices:
|
||||
# - /dev/dri:/dev/dri # For Intel/AMD GPU acceleration (VA-API)
|
||||
|
||||
# NVIDIA GPU Support (requires NVIDIA Container Toolkit)
|
||||
# Uncomment the following lines for NVIDIA GPU support
|
||||
#deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
|
||||
# ============================================================================
|
||||
# Celery Service - Background task worker
|
||||
# ============================================================================
|
||||
celery:
|
||||
image: ghcr.io/dispatcharr/dispatcharr:latest
|
||||
container_name: dispatcharr_celery
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
web:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- ./data:/data
|
||||
#- ./certs:/certs:ro # TLS certificates (optional)
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
entrypoint: ["/app/docker/entrypoint.celery.sh"]
|
||||
|
||||
# --- Environment Configuration ---
|
||||
environment:
|
||||
# Deployment Mode
|
||||
- DISPATCHARR_ENV=modular
|
||||
|
||||
# Internal Service Communication
|
||||
# Must match the web service port for DVR recording and internal API calls
|
||||
- DISPATCHARR_PORT=9191
|
||||
|
||||
# PostgreSQL — must match web service settings
|
||||
- POSTGRES_HOST=db
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_DB=dispatcharr
|
||||
- POSTGRES_USER=dispatch
|
||||
- POSTGRES_PASSWORD=secret
|
||||
# PostgreSQL TLS — must match web service
|
||||
#- POSTGRES_SSL=true
|
||||
#- POSTGRES_SSL_MODE=verify-full
|
||||
#- POSTGRES_SSL_CA_CERT=/certs/postgres/ca.crt
|
||||
#- POSTGRES_SSL_CERT=/certs/postgres/client.crt
|
||||
#- POSTGRES_SSL_KEY=/certs/postgres/client.key
|
||||
|
||||
# Redis — must match web service settings
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
#- REDIS_PASSWORD=your_strong_redis_password
|
||||
#- REDIS_USER=your_redis_username
|
||||
# Redis TLS — must match web service
|
||||
#- REDIS_SSL=true
|
||||
#- REDIS_SSL_VERIFY=true
|
||||
#- REDIS_SSL_CA_CERT=/certs/redis/ca.crt
|
||||
#- REDIS_SSL_CERT=/certs/redis/client.crt
|
||||
#- REDIS_SSL_KEY=/certs/redis/client.key
|
||||
|
||||
# Logging
|
||||
- DISPATCHARR_LOG_LEVEL=info
|
||||
|
||||
# Process Priority Configuration (Optional)
|
||||
#- CELERY_NICE_LEVEL=5 # Celery/EPG/Background tasks (default: 5, low priority; Range: -20 to 19)
|
||||
|
||||
# Legacy CPU Support (Optional)
|
||||
# Uncomment to enable legacy NumPy build for older CPUs (circa 2009)
|
||||
# that lack support for newer baseline CPU features:
|
||||
#- USE_LEGACY_NUMPY=true
|
||||
|
||||
# Django Configuration
|
||||
- DJANGO_SETTINGS_MODULE=dispatcharr.settings
|
||||
- PYTHONUNBUFFERED=1
|
||||
|
||||
# --- Advanced Configuration ---
|
||||
# Uncomment to enable high priority for Celery (required if CELERY_NICE_LEVEL < 0)
|
||||
#cap_add:
|
||||
# - SYS_NICE
|
||||
|
||||
# ============================================================================
|
||||
# PostgreSQL
|
||||
# ============================================================================
|
||||
db:
|
||||
image: postgres:17
|
||||
container_name: dispatcharr_db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5436:5432"
|
||||
environment:
|
||||
- POSTGRES_DB=dispatcharr
|
||||
- POSTGRES_USER=dispatch
|
||||
- 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
|
||||
# ============================================================================
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: dispatcharr_redis
|
||||
restart: unless-stopped
|
||||
|
||||
# --- Authentication Configuration (Optional) ---
|
||||
# By default, Redis runs without authentication.
|
||||
# Choose ONE of the following options if authentication is required:
|
||||
|
||||
# Option 1: Password-only authentication (Redis <6 or default user)
|
||||
#command: ["redis-server", "--requirepass", "your_strong_redis_password"]
|
||||
|
||||
# Option 2: Redis 6+ ACL with username + password (requires custom config file - see Redis documentation for configuration)
|
||||
#command: ["redis-server", "/etc/redis/redis.conf"]
|
||||
#volumes:
|
||||
# - ./redis.conf:/etc/redis/redis.conf:ro
|
||||
|
||||
# --- Health Check Configuration ---
|
||||
healthcheck:
|
||||
# Default: No authentication
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
|
||||
# If using Option 1 (password-only), uncomment this instead:
|
||||
#test: ["CMD", "redis-cli", "-a", "your_strong_redis_password", "ping"]
|
||||
|
||||
# If using Option 2 (Redis 6+ ACL), uncomment this instead:
|
||||
#test: ["CMD", "redis-cli", "--user", "your_redis_username", "-a", "your_strong_redis_password", "ping"]
|
||||
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# ==============================================================================
|
||||
# Volumes
|
||||
# ==============================================================================
|
||||
volumes:
|
||||
postgres_data:
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run Django migrations and collect static files
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py migrate --noinput
|
||||
|
||||
# Function to clean up only running processes
|
||||
cleanup() {
|
||||
echo "🔥 Cleanup triggered! Stopping services..."
|
||||
for pid in "${pids[@]}"; do
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo "⛔ Stopping process (PID: $pid)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
else
|
||||
echo "✅ Process (PID: $pid) already stopped."
|
||||
fi
|
||||
done
|
||||
wait
|
||||
}
|
||||
|
||||
# Catch termination signals (CTRL+C, Docker Stop, etc.)
|
||||
trap cleanup TERM INT
|
||||
|
||||
# Initialize an array to store PIDs
|
||||
pids=()
|
||||
|
||||
GUNICORN_PORT=9191
|
||||
|
||||
# If running in development mode, install and start frontend
|
||||
if [ "$DISPATCHARR_ENV" = "dev" ]; then
|
||||
echo "🚀 Development Mode - Setting up Frontend..."
|
||||
GUNICORN_PORT=5656
|
||||
|
||||
# Install Node.js
|
||||
apt-get update && apt-get install -y nodejs
|
||||
|
||||
# Install frontend dependencies
|
||||
cd /app/frontend && npm install
|
||||
cd /app
|
||||
|
||||
# Start React development server
|
||||
echo "🚀 Starting React Dev Server..."
|
||||
cd /app/frontend
|
||||
PORT=9191 ./node_modules/pm2/bin/pm2 --name test start npm -- start
|
||||
./node_modules/pm2/bin/pm2 logs &
|
||||
react_pid=$!
|
||||
echo "✅ React started with PID $react_pid"
|
||||
pids+=("$react_pid")
|
||||
cd /app
|
||||
fi
|
||||
|
||||
# If running in `dev` or `aio`, start Redis and Celery
|
||||
if [ "$DISPATCHARR_ENV" = "dev" ] || [ "$DISPATCHARR_ENV" = "aio" ]; then
|
||||
echo "🚀 Running Redis and Celery for '$DISPATCHARR_ENV'..."
|
||||
|
||||
# Start Redis
|
||||
echo "🚀 Starting Redis..."
|
||||
redis-server --daemonize no &
|
||||
sleep 1 # Give Redis time to start
|
||||
redis_pid=$(pgrep -x redis-server)
|
||||
if [ -n "$redis_pid" ]; then
|
||||
echo "✅ Redis started with PID $redis_pid"
|
||||
pids+=("$redis_pid")
|
||||
else
|
||||
echo "❌ Redis failed to start!"
|
||||
fi
|
||||
|
||||
# Start Celery
|
||||
echo "🚀 Starting Celery..."
|
||||
celery -A dispatcharr worker -l info &
|
||||
celery_pid=$!
|
||||
echo "✅ Celery started with PID $celery_pid"
|
||||
pids+=("$celery_pid")
|
||||
fi
|
||||
|
||||
# Always start Gunicorn
|
||||
echo "🚀 Starting Gunicorn..."
|
||||
gunicorn --workers=4 --worker-class=gevent --timeout=300 --bind 0.0.0.0:${GUNICORN_PORT} dispatcharr.wsgi:application &
|
||||
gunicorn_pid=$!
|
||||
echo "✅ Gunicorn started with PID $gunicorn_pid"
|
||||
pids+=("$gunicorn_pid")
|
||||
|
||||
# Log PIDs
|
||||
echo "📝 Process PIDs: ${pids[*]}"
|
||||
|
||||
# Wait for at least one process to exit and log the process that exited first
|
||||
if [ ${#pids[@]} -gt 0 ]; then
|
||||
echo "⏳ Waiting for processes to exit..."
|
||||
ps -aux | grep -E 'redis-server|celery|gunicorn|npm'
|
||||
wait -n "${pids[@]}"
|
||||
echo "🚨 One of the processes exited! Checking which one..."
|
||||
|
||||
for pid in "${pids[@]}"; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
process_name=$(ps -p "$pid" -o comm=)
|
||||
echo "❌ Process $process_name (PID: $pid) has exited!"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "❌ No processes started. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup and stop remaining processes
|
||||
cleanup
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cd /app
|
||||
source /dispatcharrpy/bin/activate
|
||||
|
||||
# Function to echo with timestamp
|
||||
echo_with_timestamp() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
# Wait for Django secret key (generated by the web container on startup)
|
||||
JWT_TIMEOUT=120
|
||||
JWT_WAITED=0
|
||||
echo 'Waiting for Django secret key...'
|
||||
while [ ! -f /data/jwt ]; do
|
||||
if [ $JWT_WAITED -ge $JWT_TIMEOUT ]; then
|
||||
echo "❌ ERROR: Timed out waiting for /data/jwt after ${JWT_TIMEOUT}s."
|
||||
echo " Is the web container running? Does it have the /data volume mounted?"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
JWT_WAITED=$((JWT_WAITED + 1))
|
||||
done
|
||||
export DJANGO_SECRET_KEY="$(tr -d '\r\n' < /data/jwt)"
|
||||
|
||||
# --- NumPy version switching for legacy hardware ---
|
||||
if [ "$USE_LEGACY_NUMPY" = "true" ]; then
|
||||
# Check if NumPy was compiled with baseline support
|
||||
if "$VIRTUAL_ENV/bin/python" -c "import numpy; numpy.show_config()" 2>&1 | grep -qi "baseline" || [ $? -ne 0 ]; then
|
||||
echo_with_timestamp "🔧 Switching to legacy NumPy (no CPU baseline)..."
|
||||
uv pip install --python "$VIRTUAL_ENV/bin/python" --no-cache --force-reinstall --no-deps /opt/numpy-*.whl
|
||||
echo_with_timestamp "✅ Legacy NumPy installed"
|
||||
else
|
||||
echo_with_timestamp "✅ Legacy NumPy (no baseline) already installed, skipping reinstallation"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fix TLS client key permissions/ownership for PostgreSQL.
|
||||
FIXED_KEY_PATH="/data/.pg-client-celery.key"
|
||||
. /app/docker/init/00-fix-pg-ssl-key.sh
|
||||
|
||||
# Wait for migrations to complete
|
||||
# Uses 'migrate --check' which exits 0 only when all migrations are applied,
|
||||
# and exits 1 on unapplied migrations OR connection errors (safe either way)
|
||||
MIG_TIMEOUT=300
|
||||
MIG_WAITED=0
|
||||
echo 'Waiting for migrations to complete...'
|
||||
until python manage.py migrate --check >/dev/null 2>&1; do
|
||||
if [ $MIG_WAITED -ge $MIG_TIMEOUT ]; then
|
||||
echo "❌ ERROR: Timed out waiting for migrations after ${MIG_TIMEOUT}s."
|
||||
echo " Check web container logs for migration errors."
|
||||
exit 1
|
||||
fi
|
||||
echo_with_timestamp 'Migrations not ready yet, waiting...'
|
||||
sleep 2
|
||||
MIG_WAITED=$((MIG_WAITED + 2))
|
||||
done
|
||||
|
||||
# Start Celery
|
||||
echo 'Migrations complete, starting Celery...'
|
||||
celery -A dispatcharr beat -l info &
|
||||
|
||||
# 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
|
||||
Executable
+396
@@ -0,0 +1,396 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
# Guard flag to prevent cleanup running twice (trap + explicit call)
|
||||
_cleanup_done=false
|
||||
|
||||
# Function to clean up only running processes
|
||||
cleanup() {
|
||||
if $_cleanup_done; then return; fi
|
||||
_cleanup_done=true
|
||||
set +e # Disable exit-on-error so cleanup always runs fully
|
||||
echo "🔥 Cleanup triggered! Stopping services..."
|
||||
|
||||
# Explicitly stop uwsgi workers - children of 'su' wrapper, not tracked in pids[]
|
||||
echo "⛔ Stopping uwsgi workers..."
|
||||
pkill -TERM -f uwsgi 2>/dev/null || true
|
||||
|
||||
# Stop celery, daphne, redis - also not tracked in pids[]
|
||||
echo "⛔ Stopping celery, daphne, redis..."
|
||||
pkill -TERM -f "celery" 2>/dev/null || true
|
||||
pkill -TERM -f "daphne" 2>/dev/null || true
|
||||
pkill -TERM -f "redis-server" 2>/dev/null || true
|
||||
|
||||
# Stop tracked processes (postgres, nginx, su/uwsgi wrapper)
|
||||
for pid in "${pids[@]}"; do
|
||||
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||
echo "⛔ Stopping process (PID: $pid)..."
|
||||
kill -TERM "$pid" 2>/dev/null
|
||||
else
|
||||
echo "✅ Process (PID: $pid) already stopped."
|
||||
fi
|
||||
done
|
||||
|
||||
# Wait up to 8 s for graceful shutdown, exit early once all are gone
|
||||
# (leaves headroom within Docker's default 10 s stop_grace_period)
|
||||
_shutdown_timeout=8
|
||||
_shutdown_elapsed=0
|
||||
while [ "$_shutdown_elapsed" -lt "$_shutdown_timeout" ]; do
|
||||
pgrep -f "uwsgi|celery|daphne|redis-server|postgres" >/dev/null 2>&1 || break
|
||||
sleep 1
|
||||
_shutdown_elapsed=$((_shutdown_elapsed + 1))
|
||||
done
|
||||
|
||||
# Force kill anything still lingering
|
||||
pkill -KILL -f uwsgi 2>/dev/null || true
|
||||
pkill -KILL -f "celery" 2>/dev/null || true
|
||||
pkill -KILL -f "daphne" 2>/dev/null || true
|
||||
pkill -KILL -f "redis-server" 2>/dev/null || true
|
||||
# Use pg_ctl immediate stop rather than SIGKILL. Avoids data corruption
|
||||
# while still forcing a fast exit (crash recovery runs on next startup)
|
||||
if pgrep -f "postgres" >/dev/null 2>&1; then
|
||||
su - "$POSTGRES_USER" -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} stop -m immediate" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
wait
|
||||
echo "✅ All processes stopped cleanly."
|
||||
}
|
||||
|
||||
# Catch termination signals (CTRL+C, Docker Stop, etc.)
|
||||
trap cleanup TERM INT
|
||||
|
||||
# Initialize an array to store PIDs and a map of PID->name
|
||||
pids=()
|
||||
declare -A pid_names
|
||||
|
||||
# Function to echo with timestamp
|
||||
echo_with_timestamp() {
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
# Set PostgreSQL environment variables
|
||||
export POSTGRES_DB=${POSTGRES_DB:-dispatcharr}
|
||||
export POSTGRES_USER=${POSTGRES_USER:-dispatch}
|
||||
# AIO mode: default to 'secret' for internal DB.
|
||||
# Modular mode + TLS: no default — cert-only auth (mTLS) uses no password.
|
||||
# Modular mode + no TLS: preserve 'secret' default for backward compatibility.
|
||||
if [[ "${DISPATCHARR_ENV:-}" == "modular" && "${POSTGRES_SSL:-}" == "true" ]]; then
|
||||
export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}"
|
||||
else
|
||||
export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-secret}"
|
||||
fi
|
||||
export DISPATCHARR_ENV=${DISPATCHARR_ENV:-aio}
|
||||
if [[ "$DISPATCHARR_ENV" == "aio" ]]; then
|
||||
# Use Unix socket for loopback values (unset, localhost, 127.0.0.1)
|
||||
if [[ -z "$POSTGRES_HOST" || "$POSTGRES_HOST" == "localhost" || "$POSTGRES_HOST" == "127.0.0.1" ]]; then
|
||||
export POSTGRES_HOST=/var/run/postgresql
|
||||
fi
|
||||
else
|
||||
export POSTGRES_HOST=${POSTGRES_HOST:-localhost}
|
||||
fi
|
||||
export POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
export PG_VERSION=$(ls /usr/lib/postgresql/ | sort -V | tail -n 1)
|
||||
export PG_BINDIR="/usr/lib/postgresql/${PG_VERSION}/bin"
|
||||
export REDIS_HOST=${REDIS_HOST:-localhost}
|
||||
export REDIS_PORT=${REDIS_PORT:-6379}
|
||||
export REDIS_DB=${REDIS_DB:-0}
|
||||
export REDIS_PASSWORD=${REDIS_PASSWORD:-}
|
||||
export REDIS_USER=${REDIS_USER:-}
|
||||
export DISPATCHARR_PORT=${DISPATCHARR_PORT:-9191}
|
||||
export LIBVA_DRIVERS_PATH='/usr/local/lib/x86_64-linux-gnu/dri'
|
||||
export LD_LIBRARY_PATH='/usr/local/lib'
|
||||
export SECRET_FILE="/data/jwt"
|
||||
# Ensure Django secret key exists or generate a new one
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
echo "Generating new Django secret key..."
|
||||
old_umask=$(umask)
|
||||
umask 077
|
||||
tmpfile="$(mktemp "${SECRET_FILE}.XXXXXX")" || { echo "mktemp failed"; exit 1; }
|
||||
python3 - <<'PY' >"$tmpfile" || { echo "secret generation failed"; rm -f "$tmpfile"; exit 1; }
|
||||
import secrets
|
||||
print(secrets.token_urlsafe(64))
|
||||
PY
|
||||
mv -f "$tmpfile" "$SECRET_FILE" || { echo "move failed"; rm -f "$tmpfile"; exit 1; }
|
||||
umask $old_umask
|
||||
fi
|
||||
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)
|
||||
# CELERY_NICE_LEVEL: Absolute nice value for Celery/background tasks (default: 5 = low priority)
|
||||
# Note: The script will automatically calculate the relative offset for Celery since it's spawned by uWSGI
|
||||
export UWSGI_NICE_LEVEL=${UWSGI_NICE_LEVEL:-0}
|
||||
CELERY_NICE_ABSOLUTE=${CELERY_NICE_LEVEL:-5}
|
||||
|
||||
# Calculate relative nice value for Celery (since nice is relative to parent process)
|
||||
# Celery is spawned by uWSGI, so we need to add the offset to reach the desired absolute value
|
||||
export CELERY_NICE_LEVEL=$((CELERY_NICE_ABSOLUTE - UWSGI_NICE_LEVEL))
|
||||
|
||||
# Set LIBVA_DRIVER_NAME if user has specified it
|
||||
if [ -v LIBVA_DRIVER_NAME ]; then
|
||||
export LIBVA_DRIVER_NAME
|
||||
fi
|
||||
# Extract version information from version.py
|
||||
export DISPATCHARR_VERSION=$(python -c "import sys; sys.path.append('/app'); import version; print(version.__version__)")
|
||||
export DISPATCHARR_TIMESTAMP=$(python -c "import sys; sys.path.append('/app'); import version; print(version.__timestamp__ or '')")
|
||||
|
||||
# Display version information with timestamp if available
|
||||
if [ -n "$DISPATCHARR_TIMESTAMP" ]; then
|
||||
echo "📦 Dispatcharr version: ${DISPATCHARR_VERSION} (build: ${DISPATCHARR_TIMESTAMP})"
|
||||
else
|
||||
echo "📦 Dispatcharr version: ${DISPATCHARR_VERSION}"
|
||||
fi
|
||||
export DISPATCHARR_LOG_LEVEL
|
||||
# Set log level with default if not provided
|
||||
DISPATCHARR_LOG_LEVEL=${DISPATCHARR_LOG_LEVEL:-INFO}
|
||||
# Convert to uppercase
|
||||
DISPATCHARR_LOG_LEVEL=${DISPATCHARR_LOG_LEVEL^^}
|
||||
|
||||
|
||||
echo "Environment DISPATCHARR_LOG_LEVEL set to: '${DISPATCHARR_LOG_LEVEL}'"
|
||||
|
||||
# Also make the log level available in /etc/environment for all login shells
|
||||
#grep -q "DISPATCHARR_LOG_LEVEL" /etc/environment || echo "DISPATCHARR_LOG_LEVEL=${DISPATCHARR_LOG_LEVEL}" >> /etc/environment
|
||||
|
||||
# Translate Dispatcharr POSTGRES_SSL_* env vars into libpq-recognized PGSSL*
|
||||
# env vars. Called once before any external PostgreSQL connection; all child
|
||||
# processes (psql, pg_dump, pg_isready, createdb, dropdb) inherit these
|
||||
# automatically. No-op when POSTGRES_SSL is not "true".
|
||||
setup_pg_ssl_env() {
|
||||
if [ "${POSTGRES_SSL:-false}" != "true" ]; then
|
||||
return 0
|
||||
fi
|
||||
export PGSSLMODE="${POSTGRES_SSL_MODE:-verify-full}"
|
||||
if [ -n "${POSTGRES_SSL_CA_CERT:-}" ]; then export PGSSLROOTCERT="$POSTGRES_SSL_CA_CERT"; fi
|
||||
if [ -n "${POSTGRES_SSL_CERT:-}" ]; then export PGSSLCERT="$POSTGRES_SSL_CERT"; fi
|
||||
if [ -n "${POSTGRES_SSL_KEY:-}" ]; then export PGSSLKEY="$POSTGRES_SSL_KEY"; fi
|
||||
}
|
||||
|
||||
# READ-ONLY - don't let users change these
|
||||
export POSTGRES_DIR=/data/db
|
||||
|
||||
# Global variables, stored so other users inherit them.
|
||||
# Rewritten every startup so that container restarts with changed env vars
|
||||
# pick up the new values (not stale ones from a previous run).
|
||||
# Define all variables to process
|
||||
variables=(
|
||||
PATH VIRTUAL_ENV DJANGO_SETTINGS_MODULE PYTHONUNBUFFERED PYTHONDONTWRITEBYTECODE
|
||||
POSTGRES_DB POSTGRES_USER POSTGRES_PASSWORD POSTGRES_HOST POSTGRES_PORT
|
||||
DISPATCHARR_ENV DISPATCHARR_DEBUG DISPATCHARR_LOG_LEVEL
|
||||
REDIS_HOST REDIS_PORT REDIS_DB REDIS_PASSWORD REDIS_USER POSTGRES_DIR DISPATCHARR_PORT
|
||||
DISPATCHARR_VERSION DISPATCHARR_TIMESTAMP LIBVA_DRIVERS_PATH LIBVA_DRIVER_NAME LD_LIBRARY_PATH
|
||||
CELERY_NICE_LEVEL UWSGI_NICE_LEVEL DJANGO_SECRET_KEY
|
||||
)
|
||||
|
||||
# TLS variables are optional — only propagate when set to avoid noisy warnings
|
||||
for _tls_var in POSTGRES_SSL POSTGRES_SSL_MODE POSTGRES_SSL_CA_CERT POSTGRES_SSL_CERT POSTGRES_SSL_KEY \
|
||||
REDIS_SSL REDIS_SSL_VERIFY REDIS_SSL_CA_CERT REDIS_SSL_CERT REDIS_SSL_KEY; do
|
||||
if [ -n "${!_tls_var+x}" ]; then
|
||||
variables+=("$_tls_var")
|
||||
fi
|
||||
done
|
||||
|
||||
# Truncate files before rewriting
|
||||
> /etc/profile.d/dispatcharr.sh
|
||||
|
||||
# Process each variable for both profile.d and environment
|
||||
for var in "${variables[@]}"; do
|
||||
# Check if the variable is set in the environment
|
||||
if [ -n "${!var+x}" ]; then
|
||||
# Add to profile.d (quoted to handle special characters in values)
|
||||
echo "export ${var}='${!var}'" >> /etc/profile.d/dispatcharr.sh
|
||||
# Add/update in /etc/environment
|
||||
sed -i "/^${var}=/d" /etc/environment
|
||||
echo "${var}='${!var}'" >> /etc/environment
|
||||
else
|
||||
echo "Warning: Environment variable $var is not set"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod +x /etc/profile.d/dispatcharr.sh
|
||||
|
||||
# Ensure root's .bashrc sources the profile.d scripts for interactive non-login shells
|
||||
if ! grep -q "profile.d/dispatcharr.sh" /root/.bashrc 2>/dev/null; then
|
||||
cat >> /root/.bashrc << 'EOF'
|
||||
|
||||
# Source Dispatcharr environment variables
|
||||
if [ -f /etc/profile.d/dispatcharr.sh ]; then
|
||||
. /etc/profile.d/dispatcharr.sh
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Run init scripts
|
||||
echo "Starting user setup..."
|
||||
. /app/docker/init/01-user-setup.sh
|
||||
|
||||
# Fix TLS client key permissions/ownership BEFORE any external PG connections.
|
||||
# Must run after 01-user-setup.sh (user exists for chown) and before
|
||||
# 02-postgres.sh / pg_isready (which make the first external PG connections).
|
||||
FIXED_KEY_PATH="/data/.pg-client.key"
|
||||
. /app/docker/init/00-fix-pg-ssl-key.sh
|
||||
# Propagate the fixed path to login shells (su - strips env vars)
|
||||
if [ "${POSTGRES_SSL_KEY:-}" = "$FIXED_KEY_PATH" ]; then
|
||||
sed -i "/^POSTGRES_SSL_KEY=/d" /etc/environment
|
||||
echo "POSTGRES_SSL_KEY='$FIXED_KEY_PATH'" >> /etc/environment
|
||||
sed -i "s|export POSTGRES_SSL_KEY=.*|export POSTGRES_SSL_KEY='$FIXED_KEY_PATH'|" /etc/profile.d/dispatcharr.sh
|
||||
fi
|
||||
|
||||
# Export libpq TLS env vars so all subsequent psql/pg_dump/pg_isready calls
|
||||
# (in 02-postgres.sh, modular-mode checks, etc.) use TLS automatically.
|
||||
setup_pg_ssl_env
|
||||
|
||||
# Initialize PostgreSQL (script handles modular vs internal mode internally)
|
||||
echo "Setting up PostgreSQL..."
|
||||
. /app/docker/init/02-postgres.sh
|
||||
|
||||
echo "Starting init process..."
|
||||
. /app/docker/init/03-init-dispatcharr.sh
|
||||
|
||||
# Start PostgreSQL if NOT in modular mode (using external database)
|
||||
if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
||||
echo "Starting Postgres..."
|
||||
prepare_pg_socket_dir
|
||||
su - "$POSTGRES_USER" -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_USER" -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_USER" -c "$PG_BINDIR/pg_ctl -D ${POSTGRES_DIR} status" | sed -n 's/.*PID: \([0-9]\+\).*/\1/p')
|
||||
echo "✅ Postgres started with PID $postgres_pid"
|
||||
if [ -n "$postgres_pid" ]; then pids+=("$postgres_pid"); pid_names[$postgres_pid]="postgres"; fi
|
||||
|
||||
# Unconditional startup guarantees — run on every AIO startup.
|
||||
# Each is idempotent and handles all scenarios (fresh, upgrade, restart).
|
||||
promote_app_role
|
||||
ensure_app_database
|
||||
else
|
||||
echo "🔗 Modular mode: Using external PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}"
|
||||
# Wait for external PostgreSQL to be ready using pg_isready (checks actual protocol readiness)
|
||||
echo_with_timestamp "Waiting for external PostgreSQL to be ready..."
|
||||
until $PG_BINDIR/pg_isready -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -q >/dev/null 2>&1; do
|
||||
echo_with_timestamp "Waiting for PostgreSQL at ${POSTGRES_HOST}:${POSTGRES_PORT}..."
|
||||
sleep 1
|
||||
done
|
||||
echo "✅ External PostgreSQL is ready"
|
||||
|
||||
# Check PostgreSQL version compatibility
|
||||
check_external_postgres_version || exit 1
|
||||
fi
|
||||
|
||||
# Wait for Redis to be ready and flush stale state.
|
||||
# In modular mode Redis is external — call wait_for_redis.py here
|
||||
# because uWSGI's exec-pre runs under 'su -' which strips env vars
|
||||
# (DISPATCHARR_ENV, REDIS_HOST, etc.).
|
||||
# In AIO mode Redis is started by uWSGI (attach-daemon), so the
|
||||
# exec-pre in uwsgi.ini handles the wait + flush there instead.
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
echo "🔗 Modular mode: Using external Redis at ${REDIS_HOST}:${REDIS_PORT}"
|
||||
echo_with_timestamp "Waiting for Redis to be ready..."
|
||||
python3 /app/scripts/wait_for_redis.py
|
||||
echo "✅ Redis is ready"
|
||||
fi
|
||||
|
||||
# Ensure database encoding is UTF8 (handles both internal and external databases)
|
||||
ensure_utf8_encoding
|
||||
|
||||
if [[ "$DISPATCHARR_ENV" = "dev" ]]; then
|
||||
. /app/docker/init/99-init-dev.sh
|
||||
echo "Starting frontend dev environment"
|
||||
su - "$POSTGRES_USER" -c "cd /app/frontend && npm run dev &"
|
||||
npm_pid=$(pgrep vite | sort | head -n1)
|
||||
echo "✅ vite started with PID $npm_pid"
|
||||
if [ -n "$npm_pid" ]; then pids+=("$npm_pid"); pid_names[$npm_pid]="vite"; fi
|
||||
else
|
||||
echo "🚀 Starting nginx..."
|
||||
nginx
|
||||
nginx_pid=$(pgrep nginx | sort | head -n1)
|
||||
echo "✅ nginx started with PID $nginx_pid"
|
||||
if [ -n "$nginx_pid" ]; then pids+=("$nginx_pid"); pid_names[$nginx_pid]="nginx"; fi
|
||||
fi
|
||||
|
||||
|
||||
# --- NumPy version switching for legacy hardware ---
|
||||
if [ "$USE_LEGACY_NUMPY" = "true" ]; then
|
||||
# Check if NumPy was compiled with baseline support
|
||||
if "$VIRTUAL_ENV/bin/python" -c "import numpy; numpy.show_config()" 2>&1 | grep -qi "baseline" || [ $? -ne 0 ]; then
|
||||
echo_with_timestamp "🔧 Switching to legacy NumPy (no CPU baseline)..."
|
||||
uv pip install --python "$VIRTUAL_ENV/bin/python" --no-cache --force-reinstall --no-deps /opt/numpy-*.whl
|
||||
echo_with_timestamp "✅ Legacy NumPy installed"
|
||||
else
|
||||
echo_with_timestamp "✅ Legacy NumPy (no baseline) already installed, skipping reinstallation"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run Django commands as non-root user to prevent permission issues
|
||||
su - "$POSTGRES_USER" -c "cd /app && python manage.py migrate --noinput"
|
||||
su - "$POSTGRES_USER" -c "cd /app && python manage.py collectstatic --noinput"
|
||||
|
||||
# Select proper uwsgi config based on environment
|
||||
if [ "$DISPATCHARR_ENV" = "dev" ] && [ "$DISPATCHARR_DEBUG" != "true" ]; then
|
||||
echo "🚀 Starting uwsgi in dev mode..."
|
||||
uwsgi_file="/app/docker/uwsgi.dev.ini"
|
||||
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"
|
||||
fi
|
||||
|
||||
# Set base uwsgi args
|
||||
uwsgi_args="--ini $uwsgi_file"
|
||||
|
||||
# Conditionally disable logging if not in debug mode
|
||||
if [ "$DISPATCHARR_DEBUG" != "true" ]; then
|
||||
uwsgi_args+=" --disable-logging"
|
||||
fi
|
||||
|
||||
# Launch uwsgi with configurable nice level (default: 0 for normal priority)
|
||||
# Users can override via UWSGI_NICE_LEVEL environment variable in docker-compose
|
||||
# Start with nice as root, then use setpriv to drop privileges to dispatch user
|
||||
# This preserves both the nice value and environment variables
|
||||
nice -n "$UWSGI_NICE_LEVEL" su - "$POSTGRES_USER" -c "cd /app && exec $VIRTUAL_ENV/bin/uwsgi $uwsgi_args" & uwsgi_pid=$!
|
||||
echo "✅ uwsgi started with PID $uwsgi_pid (nice $UWSGI_NICE_LEVEL)"
|
||||
pids+=("$uwsgi_pid"); pid_names[$uwsgi_pid]="uwsgi"
|
||||
|
||||
# Wait for services to fully initialize before checking hardware
|
||||
echo "⏳ Waiting for services to fully initialize before hardware check..."
|
||||
sleep 5
|
||||
|
||||
# Run hardware check
|
||||
echo "🔍 Running hardware acceleration check..."
|
||||
. /app/docker/init/04-check-hwaccel.sh
|
||||
|
||||
# Wait for at least one process to exit and log the process that exited first
|
||||
if [ ${#pids[@]} -gt 0 ]; then
|
||||
echo "⏳ Dispatcharr is running. Monitoring processes..."
|
||||
set +e
|
||||
while kill -0 "${pids[@]}" 2>/dev/null; do
|
||||
sleep 1 # Wait for a second before checking again
|
||||
done
|
||||
|
||||
# Only report unexpected exits — skip if cleanup was already triggered by
|
||||
# the trap (i.e. docker stop sent SIGTERM and we shut down intentionally)
|
||||
if ! $_cleanup_done; then
|
||||
echo "🚨 One of the processes exited unexpectedly! Checking which one..."
|
||||
|
||||
for pid in "${pids[@]}"; do
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
process_name=${pid_names[$pid]:-unknown}
|
||||
echo "❌ Process $process_name (PID: $pid) has exited!"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "❌ No processes started. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup and stop remaining processes
|
||||
cleanup
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Fix TLS client key permissions and ownership for PostgreSQL.
|
||||
# libpq requires the client key to be 0600 or stricter.
|
||||
#
|
||||
# Triggers on:
|
||||
# - Permissions too open (Docker Desktop mounts files as 0777)
|
||||
# - Wrong ownership (Kubernetes secrets / Docker volumes mount as root;
|
||||
# the application user can't read a root-owned 0600 key)
|
||||
# - Read-only source (volume mounted :ro — can't chmod in place)
|
||||
#
|
||||
# Usage: source this script with FIXED_KEY_PATH set to the destination.
|
||||
# FIXED_KEY_PATH="/data/.pg-client.key"
|
||||
# . /app/docker/init/00-fix-pg-ssl-key.sh
|
||||
#
|
||||
# After sourcing, POSTGRES_SSL_KEY is updated to the fixed path if a copy
|
||||
# was needed. The caller is responsible for propagating the new value to
|
||||
# /etc/environment or profile.d if required.
|
||||
|
||||
: "${FIXED_KEY_PATH:?FIXED_KEY_PATH must be set before sourcing fix-pg-ssl-key.sh}"
|
||||
|
||||
if [ -n "${POSTGRES_SSL_KEY:-}" ] && [ -f "$POSTGRES_SSL_KEY" ]; then
|
||||
_key_perms=$(stat -c '%a' "$POSTGRES_SSL_KEY" 2>/dev/null)
|
||||
_key_owner=$(stat -c '%u' "$POSTGRES_SSL_KEY" 2>/dev/null)
|
||||
_needs_fix=false
|
||||
|
||||
if [ "$_key_perms" != "600" ] && [ "$_key_perms" != "640" ]; then
|
||||
_needs_fix=true
|
||||
elif [ "$(id -u)" = "0" ] && [ -n "${PUID:-}" ] && [ "$_key_owner" != "$PUID" ]; then
|
||||
_needs_fix=true
|
||||
fi
|
||||
|
||||
if [ "$_needs_fix" = true ]; then
|
||||
cp "$POSTGRES_SSL_KEY" "$FIXED_KEY_PATH"
|
||||
chmod 600 "$FIXED_KEY_PATH"
|
||||
if [ "$(id -u)" = "0" ] && [ -n "${PUID:-}" ]; then
|
||||
chown "${PUID}:${PGID:-$PUID}" "$FIXED_KEY_PATH"
|
||||
fi
|
||||
export POSTGRES_SSL_KEY="$FIXED_KEY_PATH"
|
||||
echo "Fixed PostgreSQL client key (perms: ${_key_perms}, owner: ${_key_owner} → ${PUID:-root}:600)"
|
||||
fi
|
||||
|
||||
unset _key_perms _key_owner _needs_fix
|
||||
fi
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOTE: PUID/PGID values matching internal system UIDs (e.g. 102 for the
|
||||
# postgres package user) will cause that OS user/group to be renamed to
|
||||
# $POSTGRES_USER inside the container. This is cosmetic and does not affect
|
||||
# runtime behavior since all postgres operations run as $POSTGRES_USER
|
||||
# rather than the postgres system user.
|
||||
|
||||
# Default PUID/PGID to 1000 when not explicitly set.
|
||||
# The old image ran Django as UID 1000 and PostgreSQL as UID 102. Since
|
||||
# DATA_DIRS and host-side files are owned by 1000, defaulting to 1000
|
||||
# preserves access for upgrading users without requiring configuration.
|
||||
# The DB ownership migration (102 → 1000) is handled by 02-postgres.sh.
|
||||
export PUID=${PUID:-1000}
|
||||
export PGID=${PGID:-1000}
|
||||
|
||||
# Validate PUID/PGID are positive integers before any user/group operations.
|
||||
# Non-numeric values would cause useradd/groupadd to fail with confusing errors.
|
||||
if ! [[ "$PUID" =~ ^[0-9]+$ ]] || ! [[ "$PGID" =~ ^[0-9]+$ ]]; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: PUID and PGID must be positive integers."
|
||||
echo " PUID=$PUID PGID=$PGID"
|
||||
echo " Please set valid numeric values (default: 1000)."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# PostgreSQL refuses to run as root (UID 0). Block early — before any
|
||||
# user/group manipulation — to prevent renaming the root user/group,
|
||||
# which would break the container.
|
||||
if [ "$PUID" = "0" ] || [ "$PGID" = "0" ]; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: PUID=0 or PGID=0 is not supported."
|
||||
echo " PostgreSQL cannot run as root (UID 0)."
|
||||
echo " Please set PUID and PGID to a non-zero value (default: 1000)."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if group with PGID exists
|
||||
if getent group "$PGID" >/dev/null 2>&1; then
|
||||
# Group exists, check if it's named correctly (should match POSTGRES_USER)
|
||||
existing_group=$(getent group "$PGID" | cut -d: -f1)
|
||||
if [ "$existing_group" != "$POSTGRES_USER" ]; then
|
||||
# Rename the existing group to match POSTGRES_USER
|
||||
groupmod -n "$POSTGRES_USER" "$existing_group"
|
||||
echo "Group $existing_group with GID $PGID renamed to $POSTGRES_USER"
|
||||
fi
|
||||
else
|
||||
# Group doesn't exist, create it with same name as POSTGRES_USER
|
||||
groupadd -g "$PGID" "$POSTGRES_USER"
|
||||
echo "Group $POSTGRES_USER with GID $PGID created"
|
||||
fi
|
||||
|
||||
# Create user if it doesn't exist
|
||||
if ! getent passwd "$PUID" > /dev/null 2>&1; then
|
||||
useradd -u "$PUID" -g "$PGID" -m "$POSTGRES_USER"
|
||||
else
|
||||
existing_user=$(getent passwd "$PUID" | cut -d: -f1)
|
||||
if [ "$existing_user" != "$POSTGRES_USER" ]; then
|
||||
usermod -l "$POSTGRES_USER" -g "$PGID" "$existing_user"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get the GID of /dev/dri/renderD128 on the host (must be mounted into container)
|
||||
if [ -e "/dev/dri/renderD128" ]; then
|
||||
HOST_RENDER_GID=$(stat -c '%g' /dev/dri/renderD128)
|
||||
|
||||
# Check if this GID belongs to the video group
|
||||
VIDEO_GID=$(getent group video 2>/dev/null | cut -d: -f3)
|
||||
|
||||
if [ "$HOST_RENDER_GID" = "$VIDEO_GID" ]; then
|
||||
echo "RenderD128 GID ($HOST_RENDER_GID) matches video group GID. Using video group for GPU access."
|
||||
# Make sure POSTGRES_USER is in video group
|
||||
if ! id -nG "$POSTGRES_USER" | grep -qw "video"; then
|
||||
usermod -a -G video "$POSTGRES_USER"
|
||||
echo "Added user $POSTGRES_USER to video group for GPU access"
|
||||
fi
|
||||
else
|
||||
# We need to ensure render group exists with correct GID
|
||||
if getent group render >/dev/null; then
|
||||
CURRENT_RENDER_GID=$(getent group render | cut -d: -f3)
|
||||
if [ "$CURRENT_RENDER_GID" != "$HOST_RENDER_GID" ]; then
|
||||
# Check if another group already has the target GID
|
||||
if getent group "$HOST_RENDER_GID" >/dev/null 2>&1; then
|
||||
EXISTING_GROUP=$(getent group "$HOST_RENDER_GID" | cut -d: -f1)
|
||||
echo "Warning: Cannot change render group GID to $HOST_RENDER_GID as it's already used by group '$EXISTING_GROUP'"
|
||||
# Add user to the existing group with the target GID to ensure device access
|
||||
if ! id -nG "$POSTGRES_USER" | grep -qw "$EXISTING_GROUP"; then
|
||||
usermod -a -G "$EXISTING_GROUP" "$POSTGRES_USER" || echo "Warning: Failed to add user to $EXISTING_GROUP group"
|
||||
echo "Added user $POSTGRES_USER to $EXISTING_GROUP group for GPU access"
|
||||
fi
|
||||
else
|
||||
echo "Changing render group GID from $CURRENT_RENDER_GID to $HOST_RENDER_GID"
|
||||
groupmod -g "$HOST_RENDER_GID" render || echo "Warning: Failed to change render group GID. Continuing anyway..."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Creating render group with GID $HOST_RENDER_GID"
|
||||
groupadd -g "$HOST_RENDER_GID" render
|
||||
fi
|
||||
|
||||
# Make sure POSTGRES_USER is in render group
|
||||
if ! id -nG "$POSTGRES_USER" | grep -qw "render"; then
|
||||
usermod -a -G render "$POSTGRES_USER"
|
||||
echo "Added user $POSTGRES_USER to render group for GPU access"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Warning: /dev/dri/renderD128 not found. GPU acceleration may not be available."
|
||||
fi
|
||||
|
||||
# Always add user to video group for hardware acceleration if it exists
|
||||
# (some systems use video group for general GPU access)
|
||||
if getent group video >/dev/null 2>&1; then
|
||||
if ! id -nG "$POSTGRES_USER" | grep -qw "video"; then
|
||||
usermod -a -G video "$POSTGRES_USER"
|
||||
echo "Added user $POSTGRES_USER to video group for hardware acceleration access"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run nginx as specified user (replace any existing user directive on line 1)
|
||||
sed -i "1s/^user .*/user $POSTGRES_USER;/" /etc/nginx/nginx.conf
|
||||
@@ -0,0 +1,507 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Skip internal PostgreSQL setup in modular mode (using external database)
|
||||
if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
||||
|
||||
# Record PUID:PGID in a sentinel file so subsequent startups can skip
|
||||
# the expensive recursive chown when ownership is already correct.
|
||||
write_ownership_sentinel() {
|
||||
echo "$PUID:$PGID" > "${POSTGRES_DIR}/.owner_puid"
|
||||
chown "$PUID:$PGID" "${POSTGRES_DIR}/.owner_puid"
|
||||
}
|
||||
|
||||
# Ensure the PostgreSQL socket directory exists, is owned by PUID:PGID,
|
||||
# and has no stale lock/socket files from an unclean previous shutdown.
|
||||
# Called immediately before every pg_ctl start so it runs after any apt
|
||||
# post-remove scripts that might reset the directory's ownership.
|
||||
prepare_pg_socket_dir() {
|
||||
mkdir -p /var/run/postgresql
|
||||
chown "$PUID:$PGID" /var/run/postgresql
|
||||
chmod 755 /var/run/postgresql
|
||||
rm -f "/var/run/postgresql/.s.PGSQL.${POSTGRES_PORT}" \
|
||||
"/var/run/postgresql/.s.PGSQL.${POSTGRES_PORT}.lock" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Write standard pg_hba.conf and enable network listening.
|
||||
# Local (Unix socket): trust — safe for single-app containers where only
|
||||
# authorized processes connect. Network: password required via md5.
|
||||
# Idempotent: safe to call on every startup.
|
||||
configure_pg_network() {
|
||||
local datadir="$1"
|
||||
cat > "${datadir}/pg_hba.conf" <<HBAEOF
|
||||
local all all trust
|
||||
host all all 0.0.0.0/0 md5
|
||||
host all all ::1/128 md5
|
||||
HBAEOF
|
||||
chown "$PUID:$PGID" "${datadir}/pg_hba.conf"
|
||||
# Remove any active listen_addresses setting, then append the canonical
|
||||
# value. Avoids duplicate accumulation across restarts. Only targets
|
||||
# uncommented lines; leaves initdb's default comment intact.
|
||||
sed -Ei '/^[[:space:]]*listen_addresses[[:space:]]*=/d' "${datadir}/postgresql.conf"
|
||||
echo "listen_addresses='*'" >> "${datadir}/postgresql.conf"
|
||||
}
|
||||
|
||||
# Legacy migration: move data from /data root into $POSTGRES_DIR.
|
||||
# Safe to remove once all deployments have upgraded past this layout.
|
||||
if [ -e "/data/postgresql.conf" ]; then
|
||||
echo "Migrating PostgreSQL data from /data to $POSTGRES_DIR..."
|
||||
|
||||
# Create a temporary directory outside of /data
|
||||
mkdir -p /tmp/postgres_migration
|
||||
|
||||
# Move the PostgreSQL files to the temporary directory
|
||||
mv /data/* /tmp/postgres_migration/
|
||||
|
||||
# Create the target directory
|
||||
mkdir -p "$POSTGRES_DIR"
|
||||
|
||||
# Move the files from temporary directory to the final location
|
||||
mv /tmp/postgres_migration/* "$POSTGRES_DIR/"
|
||||
|
||||
# Clean up the temporary directory
|
||||
rmdir /tmp/postgres_migration
|
||||
|
||||
# Set proper ownership and permissions for PostgreSQL data directory
|
||||
chown -R "$PUID:$PGID" "$POSTGRES_DIR"
|
||||
chmod 700 "$POSTGRES_DIR"
|
||||
|
||||
echo "Migration completed successfully."
|
||||
fi
|
||||
|
||||
PG_VERSION_FILE="${POSTGRES_DIR}/PG_VERSION"
|
||||
|
||||
# Detect current version from data directory, if present
|
||||
if [ -f "$PG_VERSION_FILE" ]; then
|
||||
CURRENT_VERSION=$(cat "$PG_VERSION_FILE")
|
||||
else
|
||||
CURRENT_VERSION=""
|
||||
fi
|
||||
|
||||
# =========================================================================
|
||||
# Existing data: ensure ownership, auth, and permissions are correct.
|
||||
# These guarantees run on EVERY startup with existing data — not just
|
||||
# upgrades. This eliminates conditional edge cases and ensures the
|
||||
# container always reaches a known-good state regardless of how the
|
||||
# data was originally created.
|
||||
# =========================================================================
|
||||
if [ -n "$CURRENT_VERSION" ] && [ -d "$POSTGRES_DIR" ]; then
|
||||
|
||||
# --- 1. Ownership reconciliation (conditional — only when needed) ---
|
||||
# Two triggers cause a recursive chown:
|
||||
# a) PG_VERSION owner doesn't match PUID (obvious mismatch)
|
||||
# b) Sentinel file (.owner_puid) missing or stale — catches partial
|
||||
# chown from a previous interrupted startup where early files
|
||||
# (including PG_VERSION) got the new owner but deeper files didn't.
|
||||
# After a successful chown, the sentinel records PUID:PGID so
|
||||
# subsequent startups skip the expensive recursive operation.
|
||||
OWNERSHIP_SENTINEL="${POSTGRES_DIR}/.owner_puid"
|
||||
CURRENT_OWNER=$(stat -c '%u' "$PG_VERSION_FILE")
|
||||
_needs_chown=false
|
||||
if [ "$CURRENT_OWNER" != "$PUID" ]; then
|
||||
_needs_chown=true
|
||||
elif [ ! -f "$OWNERSHIP_SENTINEL" ] || [ "$(cat "$OWNERSHIP_SENTINEL" 2>/dev/null)" != "$PUID:$PGID" ]; then
|
||||
# Sentinel missing or stale. Could be:
|
||||
# a) First startup with sentinel code (pre-existing data) — benign
|
||||
# b) Interrupted chown from a previous startup — needs re-chown
|
||||
# Spot-check a deeper directory to distinguish: if base/ also
|
||||
# matches PUID:PGID, ownership is likely consistent (case a).
|
||||
_deeper_check=$(stat -c '%u:%g' "${POSTGRES_DIR}/base" 2>/dev/null)
|
||||
if [ "$_deeper_check" != "$PUID:$PGID" ]; then
|
||||
_needs_chown=true
|
||||
else
|
||||
# Spot-check passed — ownership is consistent, record sentinel
|
||||
# so future startups skip the spot-check entirely.
|
||||
write_ownership_sentinel
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$_needs_chown" = true ]; then
|
||||
echo "Migrating PostgreSQL data ownership from UID $CURRENT_OWNER to $PUID:$PGID..."
|
||||
echo " This may take several minutes for large databases. Do not stop the container."
|
||||
if ! chown -R "$PUID:$PGID" "$POSTGRES_DIR" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "ERROR: Cannot update ownership of $POSTGRES_DIR"
|
||||
echo " Current owner: UID $CURRENT_OWNER"
|
||||
echo " Target owner: UID $PUID (GID $PGID)"
|
||||
echo ""
|
||||
echo " This typically occurs with rootless Docker or restricted"
|
||||
echo " filesystems (NFS with root_squash, CIFS/SMB)."
|
||||
echo ""
|
||||
echo " To fix:"
|
||||
echo " - Local/NFS: sudo chown -R $PUID:$PGID <host_path_to_data>/db"
|
||||
echo " - CIFS/SMB: set the mount uid=$PUID,gid=$PGID option instead"
|
||||
echo " Then restart the container."
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
chmod 700 "$POSTGRES_DIR"
|
||||
# Write sentinel LAST — if chown was interrupted, the sentinel
|
||||
# won't exist and next startup will re-run the full chown.
|
||||
write_ownership_sentinel
|
||||
echo "Ownership migration complete."
|
||||
fi
|
||||
|
||||
# --- 2. Authentication guarantee (unconditional) ---
|
||||
# Always rewrite pg_hba.conf to the known-good state. This replaces
|
||||
# any auth method (peer, ident, md5, scram) left by previous images
|
||||
# or initdb defaults. Eliminates the class of bugs where the OS user
|
||||
# name doesn't match any PG role under peer/ident auth.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
fi
|
||||
|
||||
# Only run upgrade if current version is set and not the target
|
||||
if [ -n "$CURRENT_VERSION" ] && [ "$CURRENT_VERSION" != "$PG_VERSION" ]; then
|
||||
echo "Detected PostgreSQL data directory version $CURRENT_VERSION, upgrading to $PG_VERSION..."
|
||||
# Set binary paths for upgrade if needed
|
||||
OLD_BINDIR="/usr/lib/postgresql/${CURRENT_VERSION}/bin"
|
||||
NEW_BINDIR="/usr/lib/postgresql/${PG_VERSION}/bin"
|
||||
PG_INSTALLED_BY_SCRIPT=0
|
||||
if [ ! -d "$OLD_BINDIR" ]; then
|
||||
echo "PostgreSQL binaries for version $CURRENT_VERSION not found. Installing..."
|
||||
apt update && apt install -y postgresql-$CURRENT_VERSION postgresql-contrib-$CURRENT_VERSION
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install PostgreSQL version $CURRENT_VERSION. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
PG_INSTALLED_BY_SCRIPT=1
|
||||
fi
|
||||
|
||||
# Prepare the old cluster for pg_upgrade:
|
||||
# 1. Promote $POSTGRES_USER to superuser (needed for post-upgrade ops)
|
||||
# 2. Detect the bootstrap superuser (install user) — pg_upgrade
|
||||
# requires -U to match this role exactly.
|
||||
# The old cluster's install user is "postgres" (pre-PUID images)
|
||||
# or $POSTGRES_USER (post-PUID images, future upgrades).
|
||||
echo "Preparing old cluster for upgrade..."
|
||||
prepare_pg_socket_dir
|
||||
su - "$POSTGRES_USER" -c "$OLD_BINDIR/pg_ctl -D $POSTGRES_DIR start -w -o '-c port=${POSTGRES_PORT}'"
|
||||
_promoted=false
|
||||
for _role in "postgres" "$POSTGRES_USER"; do
|
||||
if su - "$POSTGRES_USER" -c "psql -U $_role -d template1 -p ${POSTGRES_PORT} -tAc 'SELECT 1;'" 2>/dev/null | grep -q 1; then
|
||||
if su - "$POSTGRES_USER" -c "psql -U $_role -d template1 -p ${POSTGRES_PORT} -v ON_ERROR_STOP=1" <<UPGEOF
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN;
|
||||
ELSE
|
||||
ALTER ROLE $POSTGRES_USER WITH SUPERUSER;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
UPGEOF
|
||||
then
|
||||
_promoted=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Detect the bootstrap superuser (OID 10 = the role that ran initdb).
|
||||
_install_user=$(su - "$POSTGRES_USER" -c "psql -d template1 -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT rolname FROM pg_authid WHERE oid = 10;\"" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -z "$_install_user" ]; then
|
||||
_install_user="postgres"
|
||||
fi
|
||||
|
||||
su - "$POSTGRES_USER" -c "$OLD_BINDIR/pg_ctl -D $POSTGRES_DIR stop -w"
|
||||
if [ "$_promoted" != true ]; then
|
||||
echo "❌ Failed to prepare old cluster for upgrade."
|
||||
echo " Could not promote '$POSTGRES_USER' to superuser in PG $CURRENT_VERSION."
|
||||
exit 1
|
||||
fi
|
||||
echo "Old cluster install user: $_install_user"
|
||||
|
||||
# Prepare new data directory
|
||||
NEW_POSTGRES_DIR="${POSTGRES_DIR}_$PG_VERSION"
|
||||
|
||||
# Remove new data directory if it already exists (from a failed/partial upgrade)
|
||||
if [ -d "$NEW_POSTGRES_DIR" ]; then
|
||||
echo "Warning: $NEW_POSTGRES_DIR already exists. Removing it to avoid upgrade issues."
|
||||
rm -rf "$NEW_POSTGRES_DIR"
|
||||
fi
|
||||
|
||||
mkdir -p "$NEW_POSTGRES_DIR"
|
||||
chown -R "$PUID:$PGID" "$NEW_POSTGRES_DIR"
|
||||
chmod 700 "$NEW_POSTGRES_DIR"
|
||||
|
||||
# Initialize new data directory with the same install user as the old
|
||||
# cluster. pg_upgrade requires the -U user to match both clusters.
|
||||
echo "Initializing new PostgreSQL data directory at $NEW_POSTGRES_DIR..."
|
||||
su - "$POSTGRES_USER" -c "$NEW_BINDIR/initdb -U $_install_user -D $NEW_POSTGRES_DIR"
|
||||
echo "Running pg_upgrade from $OLD_BINDIR to $NEW_BINDIR..."
|
||||
su - "$POSTGRES_USER" -c "$NEW_BINDIR/pg_upgrade -U $_install_user -b $OLD_BINDIR -B $NEW_BINDIR -d $POSTGRES_DIR -D $NEW_POSTGRES_DIR"
|
||||
|
||||
# Move old data directory for backup, move new into place
|
||||
mv "$POSTGRES_DIR" "${POSTGRES_DIR}_backup_${CURRENT_VERSION}_$(date +%s)"
|
||||
mv "$NEW_POSTGRES_DIR" "$POSTGRES_DIR"
|
||||
|
||||
# Apply standard connection configuration to the upgraded data directory.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
|
||||
# Record ownership sentinel for the newly upgraded data directory.
|
||||
write_ownership_sentinel
|
||||
|
||||
echo "Upgrade complete. Old data directory backed up."
|
||||
|
||||
# Uninstall PostgreSQL if we installed it just for upgrade
|
||||
if [ "$PG_INSTALLED_BY_SCRIPT" -eq 1 ]; then
|
||||
echo "Uninstalling temporary PostgreSQL $CURRENT_VERSION packages..."
|
||||
apt remove -y postgresql-$CURRENT_VERSION postgresql-contrib-$CURRENT_VERSION
|
||||
apt autoremove -y
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize PostgreSQL data directory (fresh install only).
|
||||
# Only runs initdb + configure_pg_network here. Database creation,
|
||||
# role setup, and password configuration are handled by the
|
||||
# unconditional guarantees (promote_app_role, ensure_app_database)
|
||||
# after PostgreSQL starts in entrypoint.sh.
|
||||
if [ -z "$(ls -A "$POSTGRES_DIR")" ]; then
|
||||
echo "Initializing PostgreSQL database..."
|
||||
mkdir -p "$POSTGRES_DIR"
|
||||
chown -R "$PUID:$PGID" "$POSTGRES_DIR"
|
||||
chmod 700 "$POSTGRES_DIR"
|
||||
|
||||
# Initialize PostgreSQL as the application user.
|
||||
# The superuser role is automatically named $POSTGRES_USER.
|
||||
su - "$POSTGRES_USER" -c "$PG_BINDIR/initdb -D ${POSTGRES_DIR}"
|
||||
|
||||
# Configure authentication and network access.
|
||||
configure_pg_network "${POSTGRES_DIR}"
|
||||
|
||||
# Record ownership sentinel for the freshly initialized data directory.
|
||||
write_ownership_sentinel
|
||||
fi
|
||||
|
||||
fi # End of DISPATCHARR_ENV != modular check
|
||||
|
||||
# =========================================================================
|
||||
# 3. Role guarantee (unconditional — runs after PostgreSQL starts)
|
||||
#
|
||||
# Ensures the application role ($POSTGRES_USER) exists with superuser
|
||||
# privileges and the correct password. Called from entrypoint.sh after
|
||||
# PostgreSQL starts on every AIO startup.
|
||||
#
|
||||
# Idempotent: checks before altering. Handles all scenarios:
|
||||
# - Fresh install: role exists from initdb, just verifies
|
||||
# - Upgrade from postgres-user: creates dispatch role, promotes to superuser
|
||||
# - PUID change: verifies existing role, updates password
|
||||
# - Normal restart: no-op (role already correct)
|
||||
#
|
||||
# Tries multiple database/role combinations to handle incomplete data
|
||||
# (e.g., interrupted initialization from a previous image version).
|
||||
# =========================================================================
|
||||
promote_app_role() {
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Ensuring application role is configured..."
|
||||
|
||||
# Find a connectable superuser role. Try multiple databases in case
|
||||
# the default 'postgres' database doesn't exist (e.g., incomplete
|
||||
# initialization from a crashed previous container).
|
||||
# Single query per candidate: if connection fails, output is empty;
|
||||
# if connected but not superuser, output is 'f'. Only 't' passes.
|
||||
local CONNECT_ROLE=""
|
||||
local CONNECT_DB=""
|
||||
for try_db in "postgres" "template1"; do
|
||||
for try_role in "postgres" "$POSTGRES_USER"; do
|
||||
local _super
|
||||
_super=$(su - "$POSTGRES_USER" -c "psql -U $try_role -d $try_db -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT rolsuper FROM pg_roles WHERE rolname='$try_role';\"" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ "$_super" = "t" ]; then
|
||||
CONNECT_ROLE="$try_role"
|
||||
CONNECT_DB="$try_db"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ -z "$CONNECT_ROLE" ]; then
|
||||
echo "❌ Role setup failed: no connectable superuser role found."
|
||||
echo " To recover manually:"
|
||||
echo " su - "$POSTGRES_USER" -c \"psql -d template1 -p $POSTGRES_PORT\""
|
||||
echo " CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '<your_password>';"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Escape single quotes for safe SQL interpolation
|
||||
local _sql_pw="${POSTGRES_PASSWORD//\'/\'\'}"
|
||||
|
||||
if ! su - "$POSTGRES_USER" -c "psql -U $CONNECT_ROLE -d $CONNECT_DB -p ${POSTGRES_PORT} -v ON_ERROR_STOP=1" <<EOSQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
-- Ensure the application role exists with superuser and login.
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
CREATE ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '${_sql_pw}';
|
||||
ELSE
|
||||
-- Only alter if not already superuser (idempotent).
|
||||
IF NOT (SELECT rolsuper FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN
|
||||
ALTER ROLE $POSTGRES_USER WITH SUPERUSER LOGIN;
|
||||
END IF;
|
||||
-- Ensure password is current regardless.
|
||||
ALTER ROLE $POSTGRES_USER WITH PASSWORD '${_sql_pw}';
|
||||
END IF;
|
||||
|
||||
-- Rollback compatibility: preserve the postgres role as superuser so
|
||||
-- older images (which connect as the postgres DB role) continue to work.
|
||||
-- This block can be removed once rollback to pre-PUID images is no
|
||||
-- longer expected.
|
||||
IF EXISTS (SELECT FROM pg_roles WHERE rolname = 'postgres') THEN
|
||||
IF NOT (SELECT rolsuper FROM pg_roles WHERE rolname = 'postgres') THEN
|
||||
ALTER ROLE postgres WITH SUPERUSER;
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOSQL
|
||||
then
|
||||
echo "❌ Role setup failed. The application may not be able to connect."
|
||||
echo " Check PostgreSQL logs for details."
|
||||
echo " To recover manually:"
|
||||
echo " su - "$POSTGRES_USER" -c \"psql -d template1 -p $POSTGRES_PORT\""
|
||||
echo " ALTER ROLE $POSTGRES_USER WITH SUPERUSER LOGIN PASSWORD '<your_password>';"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Application role configured."
|
||||
}
|
||||
|
||||
# =========================================================================
|
||||
# 4. Database guarantee (unconditional — runs after role setup)
|
||||
#
|
||||
# Ensures the application database ($POSTGRES_DB) exists. Handles
|
||||
# incomplete data from interrupted previous initializations where
|
||||
# PG_VERSION exists but the application database was never created.
|
||||
# =========================================================================
|
||||
ensure_app_database() {
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Connect to template1 (always exists) to check pg_database catalog.
|
||||
if su - "$POSTGRES_USER" -c "psql -d template1 -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB';\"" 2>/dev/null | grep -q 1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Application database '$POSTGRES_DB' not found — creating..."
|
||||
if ! su - "$POSTGRES_USER" -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 ${POSTGRES_DB}" 2>/dev/null; then
|
||||
# Might already exist if the check failed for a transient reason.
|
||||
if su - "$POSTGRES_USER" -c "psql -d template1 -p ${POSTGRES_PORT} -tAc \
|
||||
\"SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB';\"" 2>/dev/null | grep -q 1; then
|
||||
return 0
|
||||
fi
|
||||
echo "❌ Failed to create database '$POSTGRES_DB'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Database '$POSTGRES_DB' created."
|
||||
}
|
||||
|
||||
ensure_utf8_encoding() {
|
||||
# Check encoding of existing database
|
||||
# Supports both internal (Unix socket) and external (TCP) PostgreSQL
|
||||
echo "Checking database encoding..."
|
||||
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
# External database: use TCP connection with password
|
||||
CURRENT_ENCODING=$(PGPASSWORD="$POSTGRES_PASSWORD" psql -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database();" 2>/dev/null | tr -d ' ')
|
||||
else
|
||||
# Internal database: use Unix socket as application user
|
||||
CURRENT_ENCODING=$(su - "$POSTGRES_USER" -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB} -tAc \"SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database();\"" | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ "$CURRENT_ENCODING" != "UTF8" ]; then
|
||||
echo "Database $POSTGRES_DB encoding is $CURRENT_ENCODING, converting to UTF8..."
|
||||
DUMP_FILE="/tmp/${POSTGRES_DB}_utf8_dump_$(date +%s).sql"
|
||||
|
||||
if [[ "$DISPATCHARR_ENV" == "modular" ]]; then
|
||||
# External database: use TCP connection with password
|
||||
# Dump database (include permissions and ownership)
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" pg_dump -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" "$POSTGRES_DB" > "$DUMP_FILE" || { echo "Dump failed"; return 1; }
|
||||
# Drop and recreate database with UTF8 encoding using template0
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" dropdb -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" "$POSTGRES_DB" || { echo "Drop failed"; return 1; }
|
||||
# Recreate database with UTF8 encoding
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" createdb -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" --encoding=UTF8 --template=template0 "$POSTGRES_DB" || { echo "Create failed"; return 1; }
|
||||
# Restore data
|
||||
PGPASSWORD="$POSTGRES_PASSWORD" psql -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" < "$DUMP_FILE" || { echo "Restore failed"; return 1; }
|
||||
else
|
||||
# Internal database: use Unix socket as application user
|
||||
# Dump database (include permissions and ownership)
|
||||
su - "$POSTGRES_USER" -c "pg_dump -p ${POSTGRES_PORT} ${POSTGRES_DB}" > "$DUMP_FILE" || { echo "Dump failed"; return 1; }
|
||||
# Drop and recreate database with UTF8 encoding using template0
|
||||
su - "$POSTGRES_USER" -c "dropdb -p ${POSTGRES_PORT} ${POSTGRES_DB}" || { echo "Drop failed"; return 1; }
|
||||
# Recreate database with UTF8 encoding and correct owner
|
||||
su - "$POSTGRES_USER" -c "createdb -p ${POSTGRES_PORT} --encoding=UTF8 --template=template0 --owner=${POSTGRES_USER} ${POSTGRES_DB}" || { echo "Create failed"; return 1; }
|
||||
# Restore data
|
||||
cat "$DUMP_FILE" | su - "$POSTGRES_USER" -c "psql -p ${POSTGRES_PORT} -d ${POSTGRES_DB}" || { echo "Restore failed"; return 1; }
|
||||
fi
|
||||
|
||||
rm -f "$DUMP_FILE"
|
||||
echo "✅ Database $POSTGRES_DB converted to UTF8."
|
||||
else
|
||||
echo "✅ Database encoding is UTF8"
|
||||
fi
|
||||
}
|
||||
|
||||
check_external_postgres_version() {
|
||||
# Only check for modular deployments
|
||||
if [[ "$DISPATCHARR_ENV" != "modular" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "🔍 Checking external PostgreSQL version compatibility..."
|
||||
|
||||
# Get minimum required version from base image (set in entrypoint.sh)
|
||||
# PG_VERSION is from DispatcharrBase
|
||||
MIN_REQUIRED_VERSION=$PG_VERSION
|
||||
|
||||
# Query external PostgreSQL version
|
||||
# Use $POSTGRES_DB — restricted users may not have access to the default 'postgres' database
|
||||
PG_VERSION_ERR=$(mktemp)
|
||||
EXTERNAL_VERSION=$(PGPASSWORD="$POSTGRES_PASSWORD" psql -w -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SHOW server_version;" 2>"$PG_VERSION_ERR" | grep -oE '^[0-9]+')
|
||||
|
||||
if [ -z "$EXTERNAL_VERSION" ]; then
|
||||
echo "❌ ERROR: Unable to determine external PostgreSQL version"
|
||||
echo " Could not connect to database '$POSTGRES_DB' at ${POSTGRES_HOST}:${POSTGRES_PORT} as user '$POSTGRES_USER'"
|
||||
echo " Error: $(cat "$PG_VERSION_ERR")"
|
||||
echo " Please verify your database connection settings."
|
||||
rm -f "$PG_VERSION_ERR"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$PG_VERSION_ERR"
|
||||
|
||||
# Compare versions
|
||||
if [[ "$EXTERNAL_VERSION" -lt "$MIN_REQUIRED_VERSION" ]]; then
|
||||
# FAIL: Version too old
|
||||
echo ""
|
||||
echo "❌ ERROR: PostgreSQL version mismatch"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " External Database: PostgreSQL $EXTERNAL_VERSION"
|
||||
echo " Required Version: PostgreSQL $MIN_REQUIRED_VERSION or higher"
|
||||
echo ""
|
||||
echo " Your external PostgreSQL database is too old for Dispatcharr."
|
||||
echo " Please upgrade to PostgreSQL $MIN_REQUIRED_VERSION or higher."
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
return 1
|
||||
|
||||
elif [[ "$EXTERNAL_VERSION" -eq "$MIN_REQUIRED_VERSION" ]]; then
|
||||
# MATCH: Exact version match
|
||||
echo "✅ PostgreSQL version check passed"
|
||||
echo " External Database: PostgreSQL $EXTERNAL_VERSION (matches target version)"
|
||||
|
||||
else
|
||||
# HIGHER: Newer version
|
||||
echo "✅ PostgreSQL version check passed"
|
||||
echo " External Database: PostgreSQL $EXTERNAL_VERSION"
|
||||
echo " Target Version: PostgreSQL $MIN_REQUIRED_VERSION"
|
||||
echo " ℹ️ Your database is newer than the target version."
|
||||
echo " PostgreSQL version should be compatible with Dispatcharr."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define directories that need to exist and be owned by PUID:PGID.
|
||||
# DATA_DIRS may reside on external mounts (NFS, SMB/CIFS, FUSE) where
|
||||
# mkdir and chown can fail. Failures are collected and reported as a
|
||||
# single consolidated warning so the container still starts.
|
||||
DATA_DIRS=(
|
||||
"/data/backups"
|
||||
"/data/logos"
|
||||
"/data/recordings"
|
||||
"/data/uploads/m3us"
|
||||
"/data/uploads/epgs"
|
||||
"/data/m3us"
|
||||
"/data/epgs"
|
||||
"/data/plugins"
|
||||
"/data/models"
|
||||
"/data/scripts"
|
||||
)
|
||||
|
||||
# APP_DIRS live on the image layer and are always locally writable.
|
||||
APP_DIRS=(
|
||||
"/app/logo_cache"
|
||||
"/app/media"
|
||||
"/app/static"
|
||||
)
|
||||
|
||||
# Create app directories (image layer — always writable)
|
||||
for dir in "${APP_DIRS[@]}"; do
|
||||
mkdir -p "$dir"
|
||||
done
|
||||
|
||||
# Create data directories, tolerating failures on external mounts
|
||||
_failed_mkdir=()
|
||||
_failed_chown=()
|
||||
for dir in "${DATA_DIRS[@]}"; do
|
||||
_mkdir_err=$(mkdir -p "$dir" 2>&1) || _failed_mkdir+=("$dir ($_mkdir_err)")
|
||||
done
|
||||
|
||||
# Ensure /app itself is owned by PUID:PGID (needed for uwsgi socket creation)
|
||||
if [ "$(id -u)" = "0" ] && [ -d "/app" ]; then
|
||||
if [ "$(stat -c '%u:%g' /app)" != "$PUID:$PGID" ]; then
|
||||
echo "Fixing ownership for /app (non-recursive)"
|
||||
chown "$PUID:$PGID" /app
|
||||
fi
|
||||
fi
|
||||
# Configure nginx port
|
||||
if ! [[ "$DISPATCHARR_PORT" =~ ^[0-9]+$ ]]; then
|
||||
echo "⚠️ Warning: DISPATCHARR_PORT is not a valid integer, using default port 9191"
|
||||
DISPATCHARR_PORT=9191
|
||||
fi
|
||||
sed -i "s/NGINX_PORT/${DISPATCHARR_PORT}/g" /etc/nginx/sites-enabled/default
|
||||
|
||||
# Configure nginx based on IPv6 availability
|
||||
if ip -6 addr show | grep -q "inet6"; then
|
||||
echo "✅ IPv6 is available, enabling IPv6 in nginx"
|
||||
else
|
||||
echo "⚠️ IPv6 not available, disabling IPv6 in nginx"
|
||||
sed -i '/listen \[::\]:/d' /etc/nginx/sites-enabled/default
|
||||
fi
|
||||
|
||||
# NOTE: mac doesn't run as root, so only manage permissions
|
||||
# if this script is running as root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
# Fix data directories (non-recursive to avoid touching user files).
|
||||
# Failures are collected rather than fatal — directories may be on
|
||||
# external mounts (NFS, SMB/CIFS, FUSE) that reject chown.
|
||||
for dir in "${DATA_DIRS[@]}"; do
|
||||
if [ -d "$dir" ] && [ "$(stat -c '%u:%g' "$dir" 2>/dev/null)" != "$PUID:$PGID" ]; then
|
||||
_chown_err=$(chown "$PUID:$PGID" "$dir" 2>&1) || {
|
||||
_current_owner=$(stat -c '%u:%g' "$dir" 2>/dev/null || echo "unknown")
|
||||
_failed_chown+=("$dir (current: $_current_owner, error: $_chown_err)")
|
||||
}
|
||||
fi
|
||||
done
|
||||
|
||||
# Fix app directories (recursive since they're managed by the app)
|
||||
for dir in "${APP_DIRS[@]}"; do
|
||||
if [ -d "$dir" ] && [ "$(stat -c '%u:%g' "$dir")" != "$PUID:$PGID" ]; then
|
||||
echo "Fixing ownership for $dir (recursive)"
|
||||
chown -R "$PUID:$PGID" "$dir"
|
||||
fi
|
||||
done
|
||||
|
||||
# /data/db ownership is handled by 02-postgres.sh (sentinel-based reconciliation).
|
||||
# No secondary check needed here — duplicating it could chown without updating
|
||||
# the sentinel, creating inconsistent state.
|
||||
|
||||
# Fix /data directory ownership (non-recursive).
|
||||
# Tolerates failure for the same external-mount reasons as DATA_DIRS.
|
||||
if [ -d "/data" ] && [ "$(stat -c '%u:%g' /data 2>/dev/null)" != "$PUID:$PGID" ]; then
|
||||
_chown_err=$(chown "$PUID:$PGID" /data 2>&1) || {
|
||||
_current_owner=$(stat -c '%u:%g' /data 2>/dev/null || echo "unknown")
|
||||
_failed_chown+=("/data (current: $_current_owner, error: $_chown_err)")
|
||||
}
|
||||
fi
|
||||
|
||||
chmod +x /data 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Consolidated warning for all mkdir/chown failures.
|
||||
# Emitted outside the root guard so non-root mkdir failures are also reported.
|
||||
if [ ${#_failed_mkdir[@]} -gt 0 ] || [ ${#_failed_chown[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "================================================================"
|
||||
echo "WARNING: Some data directories could not be created or updated."
|
||||
echo " This typically occurs with NFS, SMB/CIFS, or other external"
|
||||
echo " mounts that restrict ownership changes."
|
||||
echo ""
|
||||
if [ ${#_failed_mkdir[@]} -gt 0 ]; then
|
||||
echo " Could not create:"
|
||||
for entry in "${_failed_mkdir[@]}"; do
|
||||
echo " - $entry"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
if [ ${#_failed_chown[@]} -gt 0 ]; then
|
||||
echo " Could not set ownership to $PUID:$PGID:"
|
||||
for entry in "${_failed_chown[@]}"; do
|
||||
echo " - $entry"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
echo " To fix, either:"
|
||||
echo " 1. Set PUID/PGID to match your mount's owner"
|
||||
echo " 2. Fix ownership on the host/NAS:"
|
||||
echo " sudo chown $PUID:$PGID <path>"
|
||||
echo " 3. For SMB/CIFS: set uid=$PUID,gid=$PGID in mount options"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
fi
|
||||
@@ -0,0 +1,715 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔍 Checking for GPU acceleration devices..."
|
||||
|
||||
# Helper function for device access checks
|
||||
check_dev() {
|
||||
local dev=$1
|
||||
if [ -e "$dev" ]; then
|
||||
if [ -r "$dev" ] && [ -w "$dev" ]; then
|
||||
echo "✅ Device $dev is accessible."
|
||||
else
|
||||
echo "⚠️ Device $dev exists but is not accessible. Check permissions or container runtime options."
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Device $dev does not exist."
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize device detection flags
|
||||
ANY_GPU_DEVICES_FOUND=false
|
||||
DRI_DEVICES_FOUND=false
|
||||
NVIDIA_FOUND=false
|
||||
NVIDIA_GPU_IN_LSPCI=false
|
||||
INTEL_GPU_IN_LSPCI=false
|
||||
AMD_GPU_IN_LSPCI=false
|
||||
|
||||
# Check for all GPU types in hardware via lspci
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
# Check for NVIDIA GPUs
|
||||
if lspci | grep -i "NVIDIA" | grep -i "VGA\|3D\|Display" >/dev/null; then
|
||||
NVIDIA_GPU_IN_LSPCI=true
|
||||
NVIDIA_MODEL=$(lspci | grep -i "NVIDIA" | grep -i "VGA\|3D\|Display" | head -1 | sed -E 's/.*: (.*) \[.*/\1/' | sed 's/Corporation //')
|
||||
fi
|
||||
|
||||
# Check for Intel GPUs - making sure it's not already detected as NVIDIA
|
||||
if lspci | grep -i "Intel" | grep -v "NVIDIA" | grep -i "VGA\|3D\|Display" >/dev/null; then
|
||||
INTEL_GPU_IN_LSPCI=true
|
||||
INTEL_MODEL=$(lspci | grep -i "Intel" | grep -v "NVIDIA" | grep -i "VGA\|3D\|Display" | head -1 | sed -E 's/.*: (.*) \[.*/\1/' | sed 's/Corporation //')
|
||||
fi
|
||||
|
||||
# Check for AMD GPUs - making sure it's not already detected as NVIDIA or Intel
|
||||
if lspci | grep -i "AMD\|ATI\|Advanced Micro Devices" | grep -v "NVIDIA\|Intel" | grep -i "VGA\|3D\|Display" >/dev/null; then
|
||||
AMD_GPU_IN_LSPCI=true
|
||||
AMD_MODEL=$(lspci | grep -i "AMD\|ATI\|Advanced Micro Devices" | grep -v "NVIDIA\|Intel" | grep -i "VGA\|3D\|Display" | head -1 | sed -E 's/.*: (.*) \[.*/\1/' | sed 's/Corporation //' | sed 's/Technologies //')
|
||||
fi
|
||||
|
||||
# Display detected GPU hardware
|
||||
if [ "$NVIDIA_GPU_IN_LSPCI" = true ]; then
|
||||
echo "🔍 Hardware detection: NVIDIA GPU ($NVIDIA_MODEL)"
|
||||
fi
|
||||
if [ "$INTEL_GPU_IN_LSPCI" = true ]; then
|
||||
echo "🔍 Hardware detection: Intel GPU ($INTEL_MODEL)"
|
||||
fi
|
||||
if [ "$AMD_GPU_IN_LSPCI" = true ]; then
|
||||
echo "🔍 Hardware detection: AMD GPU ($AMD_MODEL)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Silently check for any GPU devices first
|
||||
for dev in /dev/dri/renderD* /dev/dri/card* /dev/nvidia*; do
|
||||
if [ -e "$dev" ]; then
|
||||
ANY_GPU_DEVICES_FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Only if devices might exist, show detailed checks
|
||||
if [ "$ANY_GPU_DEVICES_FOUND" = true ]; then
|
||||
# Check Intel/AMD VAAPI devices
|
||||
echo "🔍 Checking for VAAPI device nodes (Intel/AMD)..."
|
||||
for dev in /dev/dri/renderD* /dev/dri/card*; do
|
||||
if [ -e "$dev" ]; then
|
||||
DRI_DEVICES_FOUND=true
|
||||
check_dev "$dev"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check NVIDIA device nodes
|
||||
echo "🔍 Checking for NVIDIA device nodes..."
|
||||
for dev in /dev/nvidia*; do
|
||||
if [ -e "$dev" ]; then
|
||||
NVIDIA_FOUND=true
|
||||
check_dev "$dev"
|
||||
fi
|
||||
done
|
||||
|
||||
# Show GPU device availability messages
|
||||
if [ "$NVIDIA_FOUND" = false ] && [ "$NVIDIA_GPU_IN_LSPCI" = true ]; then
|
||||
echo "⚠️ No NVIDIA device nodes available despite hardware detection."
|
||||
echo " You may be able to use VAAPI for hardware acceleration, but NVENC/CUDA won't be available."
|
||||
echo " For optimal performance, configure proper NVIDIA container runtime."
|
||||
elif [ "$NVIDIA_FOUND" = false ]; then
|
||||
echo "ℹ️ No NVIDIA device nodes found under /dev."
|
||||
fi
|
||||
|
||||
# Check for Intel/AMD GPUs that might not be fully accessible
|
||||
if [ "$DRI_DEVICES_FOUND" = false ] && [ "$INTEL_GPU_IN_LSPCI" = true ]; then
|
||||
echo "⚠️ Intel GPU detected in hardware but no DRI devices found."
|
||||
echo " Hardware acceleration will not be available."
|
||||
echo " Make sure /dev/dri/ devices are properly mapped to the container."
|
||||
elif [ "$DRI_DEVICES_FOUND" = false ] && [ "$AMD_GPU_IN_LSPCI" = true ]; then
|
||||
echo "⚠️ AMD GPU detected in hardware but no DRI devices found."
|
||||
echo " Hardware acceleration will not be available."
|
||||
echo " Make sure /dev/dri/ devices are properly mapped to the container."
|
||||
fi
|
||||
else
|
||||
# No GPU devices found, skip the detailed checks
|
||||
echo "❌ No GPU acceleration devices detected in this container."
|
||||
echo "ℹ️ Checking for potential configuration issues..."
|
||||
|
||||
# Check if the host might have GPUs that aren't passed to the container
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
if lspci | grep -i "VGA\|3D\|Display" | grep -i "NVIDIA\|Intel\|AMD" >/dev/null; then
|
||||
echo "⚠️ Host system appears to have GPU hardware, but no devices are accessible to the container."
|
||||
echo " - For NVIDIA GPUs: Ensure NVIDIA Container Runtime is configured properly"
|
||||
echo " - For Intel/AMD GPUs: Verify that /dev/dri/ devices are passed to the container"
|
||||
echo " - Check your Docker run command or docker-compose.yml for proper device mapping"
|
||||
else
|
||||
echo "ℹ️ No GPU hardware detected on the host system. CPU-only transcoding will be used."
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Unable to check host GPU hardware (lspci not available). CPU-only transcoding will be used."
|
||||
fi
|
||||
|
||||
echo "📋 =================================================="
|
||||
echo "✅ GPU detection script complete. No GPUs available for hardware acceleration."
|
||||
# Don't exit the container - just return from this script
|
||||
return 0 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Check group membership for GPU access - context-aware based on hardware
|
||||
echo "🔍 Checking user group memberships and device access..."
|
||||
VIDEO_GID=$(getent group video | cut -d: -f3)
|
||||
RENDER_GID=$(getent group render | cut -d: -f3)
|
||||
NVIDIA_CONTAINER_TOOLKIT_FOUND=false
|
||||
NVIDIA_ENV_MISMATCH=false
|
||||
|
||||
# Improved device access check function
|
||||
check_user_device_access() {
|
||||
local device=$1
|
||||
local user=$2
|
||||
if [ -e "$device" ];then
|
||||
if su -c "test -r '$device' && test -w '$device'" - "$user" 2>/dev/null; then
|
||||
echo "✅ User $user has full access to $device"
|
||||
return 0
|
||||
else
|
||||
echo "⚠️ User $user cannot access $device (permission denied)"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Device doesn't exist, no need to report here
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
# Direct device access verification for DRI (Intel/AMD)
|
||||
echo "🔍 Verifying if $POSTGRES_USER has direct access to GPU devices..."
|
||||
HAS_DRI_ACCESS=false
|
||||
DRI_ACCESS_COUNT=0
|
||||
DRI_DEVICE_COUNT=0
|
||||
|
||||
for dev in /dev/dri/renderD* /dev/dri/card*; do
|
||||
if [ -e "$dev" ]; then
|
||||
DRI_DEVICE_COUNT=$((DRI_DEVICE_COUNT + 1))
|
||||
if check_user_device_access "$dev" "$POSTGRES_USER"; then
|
||||
DRI_ACCESS_COUNT=$((DRI_ACCESS_COUNT + 1))
|
||||
HAS_DRI_ACCESS=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Direct device access verification for NVIDIA
|
||||
HAS_NVIDIA_ACCESS=false
|
||||
NVIDIA_ACCESS_COUNT=0
|
||||
NVIDIA_DEVICE_COUNT=0
|
||||
|
||||
for dev in /dev/nvidia*; do
|
||||
if [ -e "$dev" ]; then
|
||||
NVIDIA_DEVICE_COUNT=$((NVIDIA_DEVICE_COUNT + 1))
|
||||
if check_user_device_access "$dev" "$POSTGRES_USER"; then
|
||||
NVIDIA_ACCESS_COUNT=$((NVIDIA_ACCESS_COUNT + 1))
|
||||
HAS_NVIDIA_ACCESS=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Summary of device access
|
||||
if [ $DRI_DEVICE_COUNT -gt 0 ]; then
|
||||
if [ $DRI_ACCESS_COUNT -eq $DRI_DEVICE_COUNT ]; then
|
||||
echo "✅ User $POSTGRES_USER has access to all DRI devices ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI hardware acceleration should work properly."
|
||||
else
|
||||
echo "⚠️ User $POSTGRES_USER has limited access to DRI devices ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI hardware acceleration may not work properly."
|
||||
echo " Consider adding $POSTGRES_USER to the 'video' and/or 'render' groups."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $NVIDIA_DEVICE_COUNT -gt 0 ]; then
|
||||
if [ $NVIDIA_ACCESS_COUNT -eq $NVIDIA_DEVICE_COUNT ]; then
|
||||
echo "✅ User $POSTGRES_USER has access to all NVIDIA devices ($NVIDIA_ACCESS_COUNT/$NVIDIA_DEVICE_COUNT)"
|
||||
echo " NVIDIA hardware acceleration should work properly."
|
||||
else
|
||||
echo "⚠️ User $POSTGRES_USER has limited access to NVIDIA devices ($NVIDIA_ACCESS_COUNT/$NVIDIA_DEVICE_COUNT)"
|
||||
echo " NVIDIA hardware acceleration may not work properly."
|
||||
if [ "$NVIDIA_CONTAINER_TOOLKIT_FOUND" = false ]; then
|
||||
echo " Consider adding $POSTGRES_USER to the 'video' group or use NVIDIA Container Toolkit."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for traditional group memberships (as additional information)
|
||||
USER_IN_VIDEO_GROUP=false
|
||||
USER_IN_RENDER_GROUP=false
|
||||
|
||||
if [ -n "$VIDEO_GID" ]; then
|
||||
if id -nG "$POSTGRES_USER" 2>/dev/null | grep -qw "video"; then
|
||||
USER_IN_VIDEO_GROUP=true
|
||||
echo "ℹ️ User $POSTGRES_USER is in the 'video' group (GID $VIDEO_GID)."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$RENDER_GID" ]; then
|
||||
if id -nG "$POSTGRES_USER" 2>/dev/null | grep -qw "render"; then
|
||||
USER_IN_RENDER_GROUP=true
|
||||
echo "ℹ️ User $POSTGRES_USER is in the 'render' group (GID $RENDER_GID)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if NVIDIA Container Toolkit is present through environment or CLI tool
|
||||
# IMPORTANT: Only mark as found if both env vars AND actual NVIDIA devices exist
|
||||
if [ "$NVIDIA_FOUND" = true ] && command -v nvidia-container-cli >/dev/null 2>&1; then
|
||||
NVIDIA_CONTAINER_TOOLKIT_FOUND=true
|
||||
# Check for environment variables set by NVIDIA Container Runtime, but only if NVIDIA hardware exists
|
||||
elif [ "$NVIDIA_FOUND" = true ] && [ -n "$NVIDIA_VISIBLE_DEVICES" ] && [ -n "$NVIDIA_DRIVER_CAPABILITIES" ]; then
|
||||
NVIDIA_CONTAINER_TOOLKIT_FOUND=true
|
||||
echo "✅ NVIDIA Container Toolkit detected (via environment variables)."
|
||||
echo " The container is properly configured with Docker Compose's 'driver: nvidia' syntax."
|
||||
elif [ -n "$NVIDIA_VISIBLE_DEVICES" ] && [ -n "$NVIDIA_DRIVER_CAPABILITIES" ] && [ "$NVIDIA_FOUND" = false ]; then
|
||||
NVIDIA_ENV_MISMATCH=true
|
||||
fi
|
||||
|
||||
# Removed duplicate video group checks here - consolidated into the earlier checks that include GID
|
||||
|
||||
# Check NVIDIA Container Toolkit support
|
||||
echo "🔍 Checking NVIDIA container runtime support..."
|
||||
|
||||
# More reliable detection of NVIDIA Container Runtime
|
||||
NVIDIA_RUNTIME_ACTIVE=false
|
||||
|
||||
# Method 1: Check for nvidia-container-cli tool
|
||||
if command -v nvidia-container-cli >/dev/null 2>&1; then
|
||||
NVIDIA_RUNTIME_ACTIVE=true
|
||||
echo "✅ NVIDIA Container Runtime detected (nvidia-container-cli found)."
|
||||
|
||||
if nvidia-container-cli info >/dev/null 2>&1; then
|
||||
echo "✅ NVIDIA container runtime is functional."
|
||||
else
|
||||
echo "⚠️ nvidia-container-cli found, but 'info' command failed. Runtime may be misconfigured."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 2: Check for NVIDIA Container Runtime specific files
|
||||
if [ -e "/dev/.nv" ] || [ -e "/.nv" ] || [ -e "/.nvidia-container-runtime" ]; then
|
||||
NVIDIA_RUNTIME_ACTIVE=true
|
||||
echo "✅ NVIDIA Container Runtime files detected."
|
||||
fi
|
||||
|
||||
# Method 3: Check cgroup information for NVIDIA
|
||||
if grep -q "nvidia" /proc/self/cgroup 2>/dev/null; then
|
||||
NVIDIA_RUNTIME_ACTIVE=true
|
||||
echo "✅ NVIDIA Container Runtime cgroups detected."
|
||||
fi
|
||||
|
||||
# Final verdict based on hardware AND runtime with improved messaging
|
||||
if [ "$NVIDIA_FOUND" = true ] && ([ "$NVIDIA_RUNTIME_ACTIVE" = true ] || [ "$NVIDIA_CONTAINER_TOOLKIT_FOUND" = true ]); then
|
||||
echo "✅ NVIDIA Container Runtime is properly configured with hardware access."
|
||||
elif [ "$NVIDIA_FOUND" = true ] && [ "$NVIDIA_RUNTIME_ACTIVE" = false ] && [ "$NVIDIA_CONTAINER_TOOLKIT_FOUND" = false ]; then
|
||||
echo "ℹ️ NVIDIA devices accessible via direct passthrough instead of Container Runtime."
|
||||
echo " This works but consider using the 'deploy: resources: reservations: devices:' method in docker-compose."
|
||||
elif [ "$NVIDIA_FOUND" = false ] && [ "$NVIDIA_RUNTIME_ACTIVE" = true ]; then
|
||||
echo "⚠️ NVIDIA Container Runtime appears to be configured, but no NVIDIA devices found."
|
||||
echo " Check that your host has NVIDIA drivers installed and GPUs are properly passed to the container."
|
||||
elif [ "$DRI_DEVICES_FOUND" = true ] && [ "$NVIDIA_GPU_IN_LSPCI" = true ]; then
|
||||
echo "ℹ️ Limited GPU access: Only DRI devices available for NVIDIA hardware."
|
||||
echo " VAAPI acceleration may work but NVENC/CUDA won't be available."
|
||||
echo " For full NVIDIA capabilities, configure the NVIDIA Container Runtime."
|
||||
elif [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
echo "ℹ️ Using Intel/AMD GPU hardware for acceleration via VAAPI."
|
||||
else
|
||||
echo "⚠️ No GPU acceleration devices detected. CPU-only transcoding will be used."
|
||||
fi
|
||||
|
||||
# Run nvidia-smi if available
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
echo "🔍 Running nvidia-smi to verify GPU visibility..."
|
||||
if nvidia-smi >/dev/null 2>&1; then
|
||||
echo "✅ nvidia-smi successful - GPU is accessible to container!"
|
||||
echo " This confirms hardware acceleration should be available to FFmpeg."
|
||||
else
|
||||
echo "⚠️ nvidia-smi command failed. GPU may not be properly mapped into container."
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ nvidia-smi not installed or not in PATH."
|
||||
fi
|
||||
|
||||
# Show relevant environment variables with contextual suggestions
|
||||
echo "🔍 Checking GPU-related environment variables..."
|
||||
|
||||
# Set flags based on device detection
|
||||
DRI_DEVICES_FOUND=false
|
||||
for dev in /dev/dri/renderD* /dev/dri/card*; do
|
||||
if [ -e "$dev" ];then
|
||||
DRI_DEVICES_FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Give contextual suggestions based on detected hardware
|
||||
if [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
# Detect Intel/AMD GPU model - skip this if we already detected GPUs earlier
|
||||
if [ "$NVIDIA_GPU_IN_LSPCI" = false ] && [ "$INTEL_GPU_IN_LSPCI" = false ] && [ "$AMD_GPU_IN_LSPCI" = false ] && command -v lspci >/dev/null 2>&1; then
|
||||
GPU_INFO=$(lspci -nn | grep -i "VGA\|Display" | head -1)
|
||||
if [ -n "$GPU_INFO" ]; then
|
||||
echo "🔍 Detected GPU: $GPU_INFO"
|
||||
# Extract model for cleaner display in summary
|
||||
GPU_MODEL=$(echo "$GPU_INFO" | sed -E 's/.*: (.*) \[.*/\1/' | sed 's/Corporation //' | sed 's/Technologies //')
|
||||
fi
|
||||
else
|
||||
# Use already detected GPU model info
|
||||
if [ "$NVIDIA_GPU_IN_LSPCI" = true ]; then
|
||||
GPU_MODEL=$NVIDIA_MODEL
|
||||
elif [ "$INTEL_GPU_IN_LSPCI" = true ]; then
|
||||
GPU_MODEL=$INTEL_MODEL
|
||||
elif [ "$AMD_GPU_IN_LSPCI" = true ]; then
|
||||
GPU_MODEL=$AMD_MODEL
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$GPU_MODEL" ]; then
|
||||
echo "🔍 GPU model: $GPU_MODEL"
|
||||
fi
|
||||
# Check for LIBVA_DRIVER_NAME environment variable
|
||||
if [ -n "$LIBVA_DRIVER_NAME" ]; then
|
||||
echo "ℹ️ LIBVA_DRIVER_NAME is set to '$LIBVA_DRIVER_NAME'"
|
||||
echo " Note: If you experience issues with hardware acceleration, try removing this"
|
||||
echo " environment variable to let the system auto-detect the appropriate driver."
|
||||
else
|
||||
# Check if we can detect the GPU type
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
echo "ℹ️ VAAPI driver auto-detection is usually reliable. Settings below only needed if you experience issues."
|
||||
|
||||
# Create variables to store recommended driver and supported methods
|
||||
INTEL_RECOMMENDED_DRIVER=""
|
||||
INTEL_SUPPORTS_QSV=false
|
||||
|
||||
# Use the Intel model information we already captured
|
||||
if [ "$INTEL_GPU_IN_LSPCI" = true ] && [ -n "$INTEL_MODEL" ]; then
|
||||
# Check for newer Intel generations that use iHD
|
||||
if echo "$INTEL_MODEL" | grep -q -E "Arc|Xe|Alchemist|Tiger|Alder|Raptor|Meteor|Gen1[2-9]"; then
|
||||
echo "💡 Detected Intel GPU that supports iHD (e.g. Gen12+/Arc/Xe)"
|
||||
echo " Recommended: LIBVA_DRIVER_NAME=iHD"
|
||||
echo " Note: Only set this environment variable if hardware acceleration doesn't work by default"
|
||||
INTEL_RECOMMENDED_DRIVER="iHD"
|
||||
INTEL_SUPPORTS_QSV=true
|
||||
elif echo "$INTEL_MODEL" | grep -q -E "Coffee|Whiskey|Comet|Gen11"; then
|
||||
echo "💡 Detected Intel GPU that supports both i965 and iHD (e.g. Gen9.5/Gen11)"
|
||||
echo " Preferred: LIBVA_DRIVER_NAME=iHD"
|
||||
echo " Recommended: Try i965 only if iHD has compatibility issues"
|
||||
echo " Note: Only set this environment variable if hardware acceleration doesn't work by default"
|
||||
INTEL_RECOMMENDED_DRIVER="iHD"
|
||||
INTEL_SUPPORTS_QSV=true
|
||||
elif echo "$INTEL_MODEL" | grep -q -E "Haswell|Broadwell|Skylake|Kaby"; then
|
||||
echo "💡 Detected Intel GPU that supports i965 (e.g. Gen9 and below)"
|
||||
echo " Recommended: Set LIBVA_DRIVER_NAME=i965"
|
||||
echo " Note: Only set this environment variable if hardware acceleration doesn't work by default"
|
||||
INTEL_RECOMMENDED_DRIVER="i965"
|
||||
# Older Intel GPUs support QSV through i965 driver but with more limitations
|
||||
INTEL_SUPPORTS_QSV=false
|
||||
else
|
||||
# Generic Intel case - we're not fully confident in our recommendation
|
||||
echo "💡 Unable to definitively identify Intel GPU generation"
|
||||
echo " Try auto-detection first (no environment variable)"
|
||||
echo " If issues occur: Try LIBVA_DRIVER_NAME=iHD first (newer GPUs)"
|
||||
echo " If that fails: Try LIBVA_DRIVER_NAME=i965 (older GPUs)"
|
||||
INTEL_RECOMMENDED_DRIVER="unknown" # Mark as unknown rather than assuming
|
||||
INTEL_SUPPORTS_QSV="maybe" # Mark as maybe instead of assuming true
|
||||
fi
|
||||
elif [ "$AMD_GPU_IN_LSPCI" = true ]; then
|
||||
echo "💡 If auto-detection fails: Set LIBVA_DRIVER_NAME=radeonsi for AMD GPUs"
|
||||
echo " Note: Only set this environment variable if hardware acceleration doesn't work by default"
|
||||
else
|
||||
echo "ℹ️ Common VAAPI driver options if auto-detection fails:"
|
||||
echo " - For modern Intel GPUs (Gen12+/Arc/Xe): LIBVA_DRIVER_NAME=iHD"
|
||||
echo " - For older Intel GPUs: LIBVA_DRIVER_NAME=i965"
|
||||
echo " - For AMD GPUs: LIBVA_DRIVER_NAME=radeonsi"
|
||||
echo " Note: Only set these environment variables if hardware acceleration doesn't work by default"
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Intel/AMD GPU detected. Auto-detection should work in most cases."
|
||||
echo " If VAAPI doesn't work, you may need to set LIBVA_DRIVER_NAME manually."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check FFmpeg hardware acceleration support
|
||||
echo "🔍 Checking FFmpeg hardware acceleration capabilities..."
|
||||
if command -v ffmpeg >/dev/null 2>&1; then
|
||||
HWACCEL=$(ffmpeg -hide_banner -hwaccels 2>/dev/null | grep -v "Hardware acceleration methods:" || echo "None found")
|
||||
|
||||
# Initialize variables to store compatible and missing methods
|
||||
COMPATIBLE_METHODS=""
|
||||
MISSING_METHODS=""
|
||||
|
||||
# Format the list of hardware acceleration methods in a more readable way
|
||||
echo "🔍 Available FFmpeg hardware acceleration methods:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Process the list into a more readable format with relevance indicators
|
||||
if [ -n "$HWACCEL" ] && [ "$HWACCEL" != "None found" ]; then
|
||||
# First, show methods compatible with detected hardware
|
||||
echo " 📌 Compatible with your hardware:"
|
||||
COMPATIBLE_FOUND=false
|
||||
|
||||
for method in $HWACCEL; do
|
||||
# Skip if it's just the header line or empty
|
||||
if [ "$method" = "Hardware" ] || [ -z "$method" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if this method is relevant to detected hardware
|
||||
IS_COMPATIBLE=false
|
||||
DESCRIPTION=""
|
||||
|
||||
if [ "$NVIDIA_FOUND" = true ] && [[ "$method" =~ ^(cuda|cuvid|nvenc|nvdec)$ ]]; then
|
||||
IS_COMPATIBLE=true
|
||||
DESCRIPTION="NVIDIA GPU acceleration"
|
||||
elif [ "$INTEL_GPU_IN_LSPCI" = true ] && [ "$method" = "qsv" ] && [ "$INTEL_SUPPORTS_QSV" = true ]; then
|
||||
IS_COMPATIBLE=true
|
||||
DESCRIPTION="Intel QuickSync acceleration"
|
||||
elif [ "$method" = "vaapi" ] && (([ "$INTEL_GPU_IN_LSPCI" = true ] || [ "$AMD_GPU_IN_LSPCI" = true ]) && [ "$DRI_DEVICES_FOUND" = true ]); then
|
||||
IS_COMPATIBLE=true
|
||||
if [ "$INTEL_GPU_IN_LSPCI" = true ]; then
|
||||
DESCRIPTION="Intel VAAPI acceleration"
|
||||
else
|
||||
DESCRIPTION="AMD VAAPI acceleration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display compatible methods and store for summary
|
||||
if [ "$IS_COMPATIBLE" = true ]; then
|
||||
COMPATIBLE_FOUND=true
|
||||
COMPATIBLE_METHODS="$COMPATIBLE_METHODS $method"
|
||||
echo " ✅ $method - $DESCRIPTION"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$COMPATIBLE_FOUND" = false ]; then
|
||||
echo " ❌ No compatible acceleration methods found for your hardware"
|
||||
fi
|
||||
|
||||
# Then show all other available methods
|
||||
echo " 📌 Other available methods (not compatible with detected hardware):"
|
||||
OTHER_FOUND=false
|
||||
|
||||
for method in $HWACCEL; do
|
||||
# Skip if it's just the header line or empty
|
||||
if [ "$method" = "Hardware" ] || [ -z "$method" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if this method is relevant to detected hardware
|
||||
IS_COMPATIBLE=false
|
||||
|
||||
if [ "$NVIDIA_FOUND" = true ] && [[ "$method" =~ ^(cuda|cuvid|nvenc|nvdec)$ ]]; then
|
||||
IS_COMPATIBLE=true
|
||||
elif [ "$INTEL_GPU_IN_LSPCI" = true ] && [ "$method" = "qsv" ] && [ "$INTEL_SUPPORTS_QSV" = true ]; then
|
||||
IS_COMPATIBLE=true
|
||||
elif [ "$method" = "vaapi" ] && (([ "$INTEL_GPU_IN_LSPCI" = true ] || [ "$AMD_GPU_IN_LSPCI" = true ]) && [ "$DRI_DEVICES_FOUND" = true ]); then
|
||||
IS_COMPATIBLE=true
|
||||
fi
|
||||
|
||||
# Display other methods that aren't compatible
|
||||
if [ "$IS_COMPATIBLE" = false ]; then
|
||||
OTHER_FOUND=true
|
||||
echo " ℹ️ $method"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$OTHER_FOUND" = false ]; then
|
||||
echo " None"
|
||||
fi
|
||||
|
||||
# Show expected methods that are missing
|
||||
echo " 📌 Missing methods that should be available for your hardware:"
|
||||
MISSING_FOUND=false
|
||||
|
||||
# Check for NVIDIA methods if NVIDIA GPU is detected
|
||||
if [ "$NVIDIA_FOUND" = true ]; then
|
||||
EXPECTED_NVIDIA="cuda" # cuvid nvenc nvdec" keeping these in case future support is added
|
||||
for method in $EXPECTED_NVIDIA; do
|
||||
if ! echo "$HWACCEL" | grep -q "$method"; then
|
||||
MISSING_FOUND=true
|
||||
MISSING_METHODS="$MISSING_METHODS $method"
|
||||
echo " ⚠️ $method - NVIDIA acceleration (missing but should be available)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Check for Intel methods if Intel GPU is detected
|
||||
if [ "$INTEL_GPU_IN_LSPCI" = true ] && [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
if [ "$INTEL_SUPPORTS_QSV" = true ] && ! echo "$HWACCEL" | grep -q "qsv"; then
|
||||
MISSING_FOUND=true
|
||||
MISSING_METHODS="$MISSING_METHODS qsv"
|
||||
echo " ⚠️ qsv - Intel QuickSync acceleration (missing but should be available)"
|
||||
fi
|
||||
|
||||
if ! echo "$HWACCEL" | grep -q "vaapi"; then
|
||||
MISSING_FOUND=true
|
||||
MISSING_METHODS="$MISSING_METHODS vaapi"
|
||||
echo " ⚠️ vaapi - Intel VAAPI acceleration (missing but should be available)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for AMD methods if AMD GPU is detected
|
||||
if [ "$AMD_GPU_IN_LSPCI" = true ] && [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
if ! echo "$HWACCEL" | grep -q "vaapi"; then
|
||||
MISSING_FOUND=true
|
||||
MISSING_METHODS="$MISSING_METHODS vaapi"
|
||||
echo " ⚠️ vaapi - AMD VAAPI acceleration (missing but should be available)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MISSING_FOUND" = false ]; then
|
||||
echo " None - All expected methods are available"
|
||||
fi
|
||||
else
|
||||
echo " ❌ No hardware acceleration methods found"
|
||||
fi
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Show hardware-appropriate method summary using the already gathered information
|
||||
if [ -n "$COMPATIBLE_METHODS" ]; then
|
||||
echo "✅ Hardware-appropriate acceleration methods available:$COMPATIBLE_METHODS"
|
||||
fi
|
||||
|
||||
# Show missing expected methods
|
||||
if [ -n "$MISSING_METHODS" ]; then
|
||||
echo "⚠️ Expected acceleration methods not found:$MISSING_METHODS"
|
||||
echo " This might indicate missing libraries or improper driver configuration."
|
||||
fi
|
||||
|
||||
# Display specific cases of interest (simplify using previously captured information)
|
||||
if [ "$NVIDIA_FOUND" = true ] && ! echo "$COMPATIBLE_METHODS" | grep -q "cuda\|nvenc\|cuvid"; then
|
||||
echo "⚠️ NVIDIA GPU detected but no NVIDIA acceleration methods available."
|
||||
echo " Ensure ffmpeg is built with NVIDIA support and required libraries are installed."
|
||||
fi
|
||||
|
||||
if (([ "$INTEL_GPU_IN_LSPCI" = true ] || [ "$AMD_GPU_IN_LSPCI" = true ]) &&
|
||||
[ "$DRI_DEVICES_FOUND" = true ] && ! echo "$COMPATIBLE_METHODS" | grep -q "vaapi"); then
|
||||
echo "⚠️ Intel/AMD GPU detected but VAAPI acceleration not available."
|
||||
echo " Ensure ffmpeg is built with VAAPI support and proper drivers are installed."
|
||||
fi
|
||||
else
|
||||
echo "⚠️ FFmpeg not found in PATH."
|
||||
fi
|
||||
|
||||
# Provide a final summary of the hardware acceleration setup
|
||||
echo "📋 ===================== SUMMARY ====================="
|
||||
|
||||
# Identify which GPU type is active and working
|
||||
if [ "$NVIDIA_FOUND" = true ] && (nvidia-smi >/dev/null 2>&1 || [ -n "$NVIDIA_VISIBLE_DEVICES" ]); then
|
||||
if [ -n "$NVIDIA_MODEL" ]; then
|
||||
echo "🔰 NVIDIA GPU: $NVIDIA_MODEL"
|
||||
else
|
||||
echo "🔰 NVIDIA GPU: ACTIVE (model detection unavailable)"
|
||||
echo "ℹ️ Note: GPU model information couldn't be retrieved, but devices are present."
|
||||
echo " This may be due to missing nvidia-smi tool or container limitations."
|
||||
fi
|
||||
|
||||
if [ "$NVIDIA_CONTAINER_TOOLKIT_FOUND" = true ]; then
|
||||
echo "✅ NVIDIA Container Toolkit: CONFIGURED CORRECTLY"
|
||||
elif [ -n "$NVIDIA_VISIBLE_DEVICES" ] && [ -n "$NVIDIA_DRIVER_CAPABILITIES" ]; then
|
||||
echo "✅ NVIDIA Docker configuration: USING MODERN DEPLOYMENT"
|
||||
else
|
||||
echo "⚠️ NVIDIA setup method: DIRECT DEVICE MAPPING (functional but not optimal)"
|
||||
fi
|
||||
|
||||
# Add device accessibility status
|
||||
if [ $NVIDIA_DEVICE_COUNT -gt 0 ]; then
|
||||
if [ $NVIDIA_ACCESS_COUNT -eq $NVIDIA_DEVICE_COUNT ]; then
|
||||
echo "✅ Device access: ALL NVIDIA DEVICES ACCESSIBLE ($NVIDIA_ACCESS_COUNT/$NVIDIA_DEVICE_COUNT)"
|
||||
else
|
||||
echo "⚠️ Device access: LIMITED NVIDIA DEVICE ACCESS ($NVIDIA_ACCESS_COUNT/$NVIDIA_DEVICE_COUNT)"
|
||||
echo " Some hardware acceleration features may not work properly."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display FFmpeg NVIDIA acceleration methods in more detail
|
||||
if echo "$COMPATIBLE_METHODS" | grep -q "cuda\|nvenc\|cuvid"; then
|
||||
echo "✅ FFmpeg NVIDIA acceleration: AVAILABLE"
|
||||
|
||||
# Show detailed breakdown of available NVIDIA methods
|
||||
NVIDIA_METHODS=$(echo "$COMPATIBLE_METHODS" | grep -o '\(cuda\|cuvid\|nvenc\|nvdec\)')
|
||||
echo " Available NVIDIA methods: $NVIDIA_METHODS"
|
||||
echo " Recommended for: Video transcoding with NVIDIA GPUs"
|
||||
else
|
||||
echo "⚠️ FFmpeg NVIDIA acceleration: NOT DETECTED"
|
||||
if [ -n "$MISSING_METHODS" ]; then
|
||||
echo " Missing methods that should be available: $MISSING_METHODS"
|
||||
fi
|
||||
fi
|
||||
elif [ "$NVIDIA_GPU_IN_LSPCI" = true ] && [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
# NVIDIA through DRI only (suboptimal but possible)
|
||||
if [ -n "$NVIDIA_MODEL" ]; then
|
||||
echo "🔰 NVIDIA GPU: $NVIDIA_MODEL (SUBOPTIMALLY CONFIGURED)"
|
||||
else
|
||||
echo "🔰 NVIDIA GPU: DETECTED BUT SUBOPTIMALLY CONFIGURED"
|
||||
fi
|
||||
echo "⚠️ Your NVIDIA GPU is only accessible through DRI devices"
|
||||
echo " - VAAPI acceleration may work for some tasks"
|
||||
echo " - NVENC/CUDA acceleration is NOT available"
|
||||
|
||||
# Add device accessibility status
|
||||
if [ $DRI_DEVICE_COUNT -gt 0 ]; then
|
||||
if [ $DRI_ACCESS_COUNT -eq $DRI_DEVICE_COUNT ]; then
|
||||
echo "✅ Device access: ALL DRI DEVICES ACCESSIBLE ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI acceleration should work properly."
|
||||
else
|
||||
echo "⚠️ Device access: LIMITED DRI DEVICE ACCESS ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI acceleration may not work properly."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "💡 RECOMMENDATION: Use the proper NVIDIA container configuration:"
|
||||
echo " deploy:"
|
||||
echo " resources:"
|
||||
echo " reservations:"
|
||||
echo " devices:"
|
||||
echo " - driver: nvidia"
|
||||
echo " count: all"
|
||||
echo " capabilities: [gpu]"
|
||||
|
||||
if echo "$COMPATIBLE_METHODS" | grep -q "vaapi"; then
|
||||
echo "✅ FFmpeg VAAPI acceleration: AVAILABLE (limited without NVENC)"
|
||||
echo " VAAPI can be used for transcoding, but NVENC/CUDA would be more efficient"
|
||||
else
|
||||
echo "⚠️ FFmpeg VAAPI acceleration: NOT DETECTED"
|
||||
fi
|
||||
elif [ "$DRI_DEVICES_FOUND" = true ]; then
|
||||
# Intel/AMD detection with model if available
|
||||
if [ -n "$GPU_MODEL" ]; then
|
||||
echo "🔰 GPU: $GPU_MODEL"
|
||||
elif [ -n "$LIBVA_DRIVER_NAME" ]; then
|
||||
echo "🔰 ${LIBVA_DRIVER_NAME^^} GPU: ACTIVE"
|
||||
else
|
||||
echo "🔰 INTEL/AMD GPU: ACTIVE (model detection unavailable)"
|
||||
echo "ℹ️ Note: Basic GPU drivers appear to be loaded (device nodes exist), but"
|
||||
echo " couldn't identify specific model. This doesn't necessarily indicate a problem."
|
||||
fi
|
||||
|
||||
# Add device accessibility status
|
||||
if [ $DRI_DEVICE_COUNT -gt 0 ]; then
|
||||
if [ $DRI_ACCESS_COUNT -eq $DRI_DEVICE_COUNT ]; then
|
||||
echo "✅ Device access: ALL DRI DEVICES ACCESSIBLE ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI hardware acceleration should work properly."
|
||||
else
|
||||
echo "⚠️ Device access: LIMITED DRI DEVICE ACCESS ($DRI_ACCESS_COUNT/$DRI_DEVICE_COUNT)"
|
||||
echo " VAAPI hardware acceleration may not work properly."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display FFmpeg VAAPI acceleration method with more details
|
||||
if echo "$COMPATIBLE_METHODS" | grep -q "vaapi"; then
|
||||
echo "✅ FFmpeg VAAPI acceleration: AVAILABLE"
|
||||
|
||||
# Add recommended usage information
|
||||
echo " Recommended for: General video transcoding with Intel/AMD GPUs"
|
||||
|
||||
# Add recommended driver information for Intel GPUs
|
||||
if [ "$INTEL_GPU_IN_LSPCI" = true ] && [ -n "$INTEL_RECOMMENDED_DRIVER" ]; then
|
||||
if [ "$INTEL_RECOMMENDED_DRIVER" = "unknown" ]; then
|
||||
echo "ℹ️ Uncertain about recommended VAAPI driver for this Intel GPU"
|
||||
echo " Auto-detection should work, but if issues occur try iHD or i965"
|
||||
else
|
||||
echo "ℹ️ Recommended VAAPI driver for this Intel GPU: $INTEL_RECOMMENDED_DRIVER"
|
||||
fi
|
||||
|
||||
if [ "$INTEL_SUPPORTS_QSV" = true ] && echo "$COMPATIBLE_METHODS" | grep -q "qsv"; then
|
||||
echo "✅ QSV acceleration: AVAILABLE"
|
||||
echo " Recommended for: Intel-specific optimized transcoding"
|
||||
echo " Works best with: $INTEL_RECOMMENDED_DRIVER driver"
|
||||
elif [ "$INTEL_SUPPORTS_QSV" = true ]; then
|
||||
echo "ℹ️ QSV acceleration: NOT DETECTED (may be available with proper configuration)"
|
||||
echo " Your Intel GPU supports QSV but it's not available in FFmpeg"
|
||||
echo " Check if FFmpeg is built with QSV support"
|
||||
elif [ "$INTEL_SUPPORTS_QSV" = "maybe" ]; then
|
||||
echo "ℹ️ QSV acceleration: MAY BE AVAILABLE (depends on exact GPU model)"
|
||||
fi
|
||||
elif [ "$AMD_GPU_IN_LSPCI" = true ]; then
|
||||
echo "ℹ️ Recommended VAAPI driver for AMD GPUs: radeonsi"
|
||||
fi
|
||||
else
|
||||
echo "⚠️ FFmpeg VAAPI acceleration: NOT DETECTED"
|
||||
if [ -n "$MISSING_METHODS" ]; then
|
||||
echo " Missing methods that should be available: $MISSING_METHODS"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "❌ NO GPU ACCELERATION DETECTED"
|
||||
echo "⚠️ Hardware acceleration is unavailable or misconfigured"
|
||||
fi
|
||||
|
||||
echo "📋 =================================================="
|
||||
echo "✅ GPU detection script complete."
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -e "/tmp/init" ]; then
|
||||
echo "🚀 Development Mode - Setting up Frontend..."
|
||||
|
||||
# Install Node.js
|
||||
if ! command -v node 2>&1 >/dev/null
|
||||
then
|
||||
echo "=== setting up nodejs ==="
|
||||
curl -sL https://deb.nodesource.com/setup_23.x -o /tmp/nodesource_setup.sh
|
||||
bash /tmp/nodesource_setup.sh
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
nodejs
|
||||
fi
|
||||
|
||||
# Install frontend dependencies
|
||||
cd /app/frontend && npm install
|
||||
# Install Python dependencies using UV
|
||||
cd /app && uv sync --python $UV_PROJECT_ENVIRONMENT/bin/python --no-install-project --no-dev
|
||||
|
||||
# Install debugpy for remote debugging
|
||||
if [ "$DISPATCHARR_DEBUG" = "true" ]; then
|
||||
echo "=== setting up debugpy ==="
|
||||
uv pip install --python $UV_PROJECT_ENVIRONMENT/bin/python debugpy
|
||||
fi
|
||||
|
||||
if [[ "$DISPATCHARR_ENV" = "dev" ]]; then
|
||||
touch /tmp/init
|
||||
fi
|
||||
else
|
||||
echo "Development mode initialization already done. Skipping dev setup."
|
||||
fi
|
||||
@@ -0,0 +1,99 @@
|
||||
proxy_cache_path /app/logo_cache levels=1:2 keys_zone=logo_cache:10m
|
||||
inactive=24h use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen NGINX_PORT;
|
||||
listen [::]:NGINX_PORT;
|
||||
|
||||
proxy_connect_timeout 75;
|
||||
proxy_send_timeout 300;
|
||||
proxy_read_timeout 300;
|
||||
client_max_body_size 0;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
# Serve Django via uWSGI
|
||||
location / {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/app/uwsgi.sock;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
root /app/static;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
root /app;
|
||||
}
|
||||
|
||||
location /logos/ {
|
||||
root /data;
|
||||
}
|
||||
|
||||
# Internal location for X-Accel-Redirect backup downloads
|
||||
# Django handles auth, nginx serves the file directly
|
||||
location /protected-backups/ {
|
||||
internal;
|
||||
alias /data/backups/;
|
||||
}
|
||||
|
||||
location ~ ^/api/channels/logos/(?<logo_id>\d+)/cache/ {
|
||||
proxy_pass http://127.0.0.1:5656;
|
||||
proxy_cache logo_cache;
|
||||
proxy_cache_key "$scheme$request_uri"; # Cache per logo URL
|
||||
proxy_cache_valid 200 24h; # Cache for 24 hours
|
||||
proxy_cache_use_stale error timeout updating; # Serve stale if Django is slow
|
||||
}
|
||||
|
||||
location ~ ^/api/vod/vodlogos/(?<logo_id>\d+)/cache/ {
|
||||
proxy_pass http://127.0.0.1:5656;
|
||||
proxy_cache logo_cache;
|
||||
proxy_cache_key "$scheme$request_uri"; # Cache per logo URL
|
||||
proxy_cache_valid 200 24h; # Cache for 24 hours
|
||||
proxy_cache_use_stale error timeout updating; # Serve stale if Django is slow
|
||||
}
|
||||
|
||||
# admin disabled when not in dev mode
|
||||
location ~ ^/admin/?$ {
|
||||
return 301 /login;
|
||||
}
|
||||
|
||||
# Route HDHR request to Django
|
||||
location /hdhr {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/app/uwsgi.sock;
|
||||
}
|
||||
|
||||
# Serve FFmpeg streams efficiently
|
||||
location /output/stream/ {
|
||||
proxy_pass http://127.0.0.1:5656;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Connection keep-alive;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSockets for real-time communication
|
||||
location /ws/ {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
}
|
||||
|
||||
# Route TS proxy requests to the dedicated instance
|
||||
location /proxy/ {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass unix:/app/uwsgi.sock;
|
||||
uwsgi_buffering off;
|
||||
uwsgi_read_timeout 300s;
|
||||
uwsgi_send_timeout 300s;
|
||||
client_max_body_size 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,892 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Integration test suite for TLS/mTLS in modular mode.
|
||||
# Validates that Dispatcharr connects correctly to external PostgreSQL and
|
||||
# Redis services using various TLS configurations.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Docker Desktop (or Docker Engine) running
|
||||
# - Internet access (pulls postgres:17, redis:latest)
|
||||
# - ~10-15 minutes for a full run
|
||||
#
|
||||
# Usage:
|
||||
# cd <repo_root>
|
||||
# bash docker/tests/test-tls-postgres.sh [--skip-build] [--keep-on-fail] [scenario_name]
|
||||
#
|
||||
# Options:
|
||||
# --skip-build Skip Docker image build (use existing dispatcharr:tls-test image)
|
||||
# --keep-on-fail Don't clean up containers/volumes on failure (for debugging)
|
||||
# scenario_name Run only the named scenario
|
||||
#
|
||||
# Scenarios:
|
||||
# modular_mtls_no_password PG mTLS cert-only auth, no password
|
||||
# modular_mtls_with_password PG mTLS + password auth combined
|
||||
# modular_tls_server_only PG server-side TLS only (no client cert)
|
||||
# modular_tls_key_permission PG mTLS with 0777 client key (Docker Desktop scenario)
|
||||
# modular_no_tls_regression Non-TLS modular mode still works
|
||||
# modular_pg_verify_full PG mTLS with verify-full (CN must match hostname)
|
||||
# modular_redis_tls Redis with TLS (server-side verification)
|
||||
# modular_full_tls_celery PG mTLS + Redis TLS with separate Celery container
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 All tests passed
|
||||
# 1 One or more tests failed (or build failed)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Prevent Git Bash (MINGW) from converting Unix paths
|
||||
export MSYS_NO_PATHCONV=1
|
||||
|
||||
###############################################################################
|
||||
# Configuration
|
||||
###############################################################################
|
||||
IMAGE_NAME="dispatcharr:tls-test"
|
||||
TEST_PREFIX="tls_test"
|
||||
STARTUP_TIMEOUT=120
|
||||
SKIP_BUILD=false
|
||||
KEEP_ON_FAIL=false
|
||||
SINGLE_SCENARIO=""
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
ERRORS=()
|
||||
CERT_DIR=""
|
||||
|
||||
# Colors (disabled if not a terminal)
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
else
|
||||
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; NC=''
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
# Parse arguments
|
||||
###############################################################################
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-build) SKIP_BUILD=true ;;
|
||||
--keep-on-fail) KEEP_ON_FAIL=true ;;
|
||||
-*) echo "Unknown option: $arg"; exit 1 ;;
|
||||
*) SINGLE_SCENARIO="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
###############################################################################
|
||||
# Helpers
|
||||
###############################################################################
|
||||
CURRENT_SCENARIO=""
|
||||
CLEANUP_ITEMS=()
|
||||
|
||||
log_pass() { echo -e " ${GREEN}✅ $1${NC}"; PASS=$((PASS + 1)); }
|
||||
log_fail() { echo -e " ${RED}❌ $1${NC}"; FAIL=$((FAIL + 1)); ERRORS+=("[$CURRENT_SCENARIO] $1"); }
|
||||
log_skip() { echo -e " ${YELLOW}⏭️ $1${NC}"; SKIP=$((SKIP + 1)); }
|
||||
log_info() { echo -e " ${CYAN}ℹ️ $1${NC}"; }
|
||||
section() { echo -e "\n${BOLD}━━━ $1 ━━━${NC}"; SCENARIO_FAIL_BEFORE=$FAIL; }
|
||||
|
||||
track_container() { CLEANUP_ITEMS+=("container:$1"); }
|
||||
track_volume() { CLEANUP_ITEMS+=("volume:$1"); }
|
||||
track_network() { CLEANUP_ITEMS+=("network:$1"); }
|
||||
|
||||
fresh_volume() {
|
||||
local vol="$1"
|
||||
docker rm -f $(docker ps -aq --filter "volume=${vol}") 2>/dev/null || true
|
||||
docker volume rm "$vol" 2>/dev/null || true
|
||||
docker volume create "$vol" >/dev/null
|
||||
track_volume "$vol"
|
||||
}
|
||||
|
||||
cleanup_scenario() {
|
||||
if [ "$KEEP_ON_FAIL" = true ] && [ "$FAIL" -gt "${SCENARIO_FAIL_BEFORE:-0}" ]; then
|
||||
log_info "Keeping resources for debugging (--keep-on-fail)"
|
||||
CLEANUP_ITEMS=()
|
||||
return
|
||||
fi
|
||||
for item in "${CLEANUP_ITEMS[@]}"; do
|
||||
local type="${item%%:*}"
|
||||
local name="${item#*:}"
|
||||
case "$type" in
|
||||
container) docker stop "$name" 2>/dev/null; docker rm -f "$name" 2>/dev/null ;;
|
||||
volume) docker volume rm "$name" 2>/dev/null ;;
|
||||
network) docker network rm "$name" 2>/dev/null ;;
|
||||
esac
|
||||
done
|
||||
CLEANUP_ITEMS=()
|
||||
}
|
||||
|
||||
trap 'cleanup_scenario; [ -n "$CERT_DIR" ] && rm -rf "$CERT_DIR"' EXIT
|
||||
|
||||
wait_for_ready() {
|
||||
local name="$1"
|
||||
local timeout="${2:-$STARTUP_TIMEOUT}"
|
||||
local elapsed=0
|
||||
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if ! docker ps -q -f "name=^${name}$" 2>/dev/null | grep -q .; then
|
||||
echo " Container $name exited unexpectedly"
|
||||
return 1
|
||||
fi
|
||||
if docker logs "$name" 2>&1 | grep -q "uwsgi started with PID"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
((elapsed+=3))
|
||||
done
|
||||
echo " Timeout (${timeout}s) waiting for $name"
|
||||
return 1
|
||||
}
|
||||
|
||||
_capture_logs() {
|
||||
local container="$1" logfile="$2"
|
||||
docker logs "$container" > "$logfile" 2>&1
|
||||
}
|
||||
|
||||
check_log_contains() {
|
||||
local container="$1" pattern="$2" description="$3"
|
||||
local tmplog; tmplog=$(mktemp)
|
||||
_capture_logs "$container" "$tmplog"
|
||||
if grep -q "$pattern" "$tmplog"; then
|
||||
log_pass "$description"
|
||||
else
|
||||
log_fail "$description (pattern not found: $pattern)"
|
||||
fi
|
||||
rm -f "$tmplog"
|
||||
}
|
||||
|
||||
check_log_absent() {
|
||||
local container="$1" pattern="$2" description="$3"
|
||||
local tmplog; tmplog=$(mktemp)
|
||||
_capture_logs "$container" "$tmplog"
|
||||
if grep -q "$pattern" "$tmplog"; then
|
||||
log_fail "$description (unexpected pattern found: $pattern)"
|
||||
else
|
||||
log_pass "$description"
|
||||
fi
|
||||
rm -f "$tmplog"
|
||||
}
|
||||
|
||||
check_migrations_done() {
|
||||
local container="$1"
|
||||
local tmplog; tmplog=$(mktemp)
|
||||
_capture_logs "$container" "$tmplog"
|
||||
if grep -qE "Running migrations|No migrations to apply|Operations to perform|Applying .+\.\.\. OK" "$tmplog"; then
|
||||
log_pass "Django migrations completed"
|
||||
elif grep -q "uwsgi started with PID" "$tmplog"; then
|
||||
log_pass "Django migrations completed (confirmed via uwsgi startup)"
|
||||
else
|
||||
log_fail "Django migrations did not complete"
|
||||
fi
|
||||
rm -f "$tmplog"
|
||||
}
|
||||
|
||||
check_no_permission_errors() {
|
||||
local container="$1"
|
||||
local tmplog; tmplog=$(mktemp)
|
||||
_capture_logs "$container" "$tmplog"
|
||||
local errors
|
||||
errors=$(grep -iE "permission denied|operation not permitted" "$tmplog" \
|
||||
| grep -v "GPU acceleration" | grep -v "Warning:" | head -5)
|
||||
rm -f "$tmplog"
|
||||
if [ -n "$errors" ]; then
|
||||
log_fail "Permission errors in logs:"
|
||||
echo "$errors" | while read -r line; do echo " $line"; done
|
||||
else
|
||||
log_pass "No permission errors in logs"
|
||||
fi
|
||||
}
|
||||
|
||||
dump_logs_on_fail() {
|
||||
local container="$1"
|
||||
if [ $FAIL -gt ${SCENARIO_FAIL_BEFORE:-0} ]; then
|
||||
echo -e " ${YELLOW}--- Container logs ($container) ---${NC}"
|
||||
docker logs "$container" 2>&1 | tail -30 | sed 's/^/ /'
|
||||
echo -e " ${YELLOW}--- End logs ---${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Certificate generation
|
||||
###############################################################################
|
||||
generate_test_certs() {
|
||||
CERT_DIR=$(mktemp -d)
|
||||
log_info "Generating test certificates in $CERT_DIR"
|
||||
|
||||
# Generate certs inside a container for cross-platform compatibility.
|
||||
# Shared CA for both PG and Redis. CN of server certs must match their
|
||||
# Docker container hostnames for verify-full mode.
|
||||
docker run --rm --entrypoint sh \
|
||||
-v "$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR"):/certs" \
|
||||
-w /certs alpine/openssl -c '
|
||||
# Shared CA
|
||||
openssl req -new -x509 -days 1 -nodes \
|
||||
-keyout ca.key -out ca.crt -subj "/CN=Test CA" 2>/dev/null &&
|
||||
|
||||
# PostgreSQL server cert (CN = PG container hostname)
|
||||
openssl req -new -nodes \
|
||||
-keyout pg-server.key -out pg-server.csr -subj "/CN='"${TEST_PREFIX}"'_pg" 2>/dev/null &&
|
||||
openssl x509 -req -days 1 -in pg-server.csr \
|
||||
-CA ca.crt -CAkey ca.key -CAcreateserial -out pg-server.crt 2>/dev/null &&
|
||||
# PostgreSQL client cert (CN = POSTGRES_USER)
|
||||
openssl req -new -nodes \
|
||||
-keyout pg-client.key -out pg-client.csr -subj "/CN=dispatch" 2>/dev/null &&
|
||||
openssl x509 -req -days 1 -in pg-client.csr \
|
||||
-CA ca.crt -CAkey ca.key -CAcreateserial -out pg-client.crt 2>/dev/null &&
|
||||
|
||||
# Redis server cert (CN = Redis container hostname)
|
||||
openssl req -new -nodes \
|
||||
-keyout redis-server.key -out redis-server.csr -subj "/CN='"${TEST_PREFIX}"'_redis" 2>/dev/null &&
|
||||
openssl x509 -req -days 1 -in redis-server.csr \
|
||||
-CA ca.crt -CAkey ca.key -CAcreateserial -out redis-server.crt 2>/dev/null &&
|
||||
|
||||
# Backwards-compat aliases (existing PG-only scenarios use these names)
|
||||
cp pg-server.crt server.crt && cp pg-server.key server.key &&
|
||||
cp pg-client.crt client.crt && cp pg-client.key client.key &&
|
||||
|
||||
chmod 600 pg-server.key pg-client.key redis-server.key server.key client.key
|
||||
' || { log_fail "Certificate generation failed"; return 1; }
|
||||
|
||||
log_pass "Test certificates generated"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Start a TLS-enabled Redis container
|
||||
###############################################################################
|
||||
start_tls_redis() {
|
||||
local name="$1" net="$2"
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
# Redis needs certs owned by redis user (uid 999 in the official image).
|
||||
# Mount certs, copy to a writable location, fix ownership, then start
|
||||
# with TLS flags.
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
redis:latest \
|
||||
sh -c '
|
||||
cp /certs/redis-server.crt /certs/redis-server.key /certs/ca.crt /tmp/ &&
|
||||
chmod 600 /tmp/redis-server.key &&
|
||||
chown redis:redis /tmp/redis-server.crt /tmp/redis-server.key /tmp/ca.crt &&
|
||||
exec redis-server \
|
||||
--tls-port 6379 --port 0 \
|
||||
--tls-cert-file /tmp/redis-server.crt \
|
||||
--tls-key-file /tmp/redis-server.key \
|
||||
--tls-ca-cert-file /tmp/ca.crt \
|
||||
--tls-auth-clients no
|
||||
' >/dev/null
|
||||
|
||||
# Wait for Redis TLS to be ready
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt 20 ]; do
|
||||
if docker exec "$name" redis-cli --tls \
|
||||
--cert /certs/redis-server.crt --key /certs/redis-server.key --cacert /certs/ca.crt \
|
||||
ping 2>/dev/null | grep -q "PONG"; then
|
||||
break
|
||||
fi
|
||||
sleep 2; elapsed=$((elapsed + 2))
|
||||
done
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Start a TLS-enabled PostgreSQL container
|
||||
###############################################################################
|
||||
start_tls_postgres() {
|
||||
local name="$1" net="$2" hba_auth="$3"
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=tempsetup \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
postgres:17 >/dev/null
|
||||
|
||||
# Wait for PG to initialize
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt 30 ]; do
|
||||
if docker exec "$name" su postgres -c "/usr/lib/postgresql/17/bin/pg_isready" 2>/dev/null | grep -q "accepting"; then
|
||||
break
|
||||
fi
|
||||
sleep 2; ((elapsed+=2))
|
||||
done
|
||||
|
||||
# Configure SSL and pg_hba.conf
|
||||
docker exec "$name" bash -c "
|
||||
cp /certs/server.crt /certs/server.key /certs/ca.crt /var/lib/postgresql/
|
||||
chown postgres:postgres /var/lib/postgresql/server.crt /var/lib/postgresql/server.key /var/lib/postgresql/ca.crt
|
||||
chmod 600 /var/lib/postgresql/server.key
|
||||
echo \"ssl = on\" >> /var/lib/postgresql/data/postgresql.conf
|
||||
echo \"ssl_cert_file = '/var/lib/postgresql/server.crt'\" >> /var/lib/postgresql/data/postgresql.conf
|
||||
echo \"ssl_key_file = '/var/lib/postgresql/server.key'\" >> /var/lib/postgresql/data/postgresql.conf
|
||||
echo \"ssl_ca_file = '/var/lib/postgresql/ca.crt'\" >> /var/lib/postgresql/data/postgresql.conf
|
||||
cat > /var/lib/postgresql/data/pg_hba.conf << HBA
|
||||
local all all trust
|
||||
hostssl all all 0.0.0.0/0 ${hba_auth}
|
||||
hostssl all all ::0/0 ${hba_auth}
|
||||
HBA
|
||||
su postgres -c '/usr/lib/postgresql/17/bin/pg_ctl reload -D /var/lib/postgresql/data'
|
||||
" >/dev/null 2>&1
|
||||
sleep 1
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Test scenarios
|
||||
###############################################################################
|
||||
|
||||
test_modular_mtls_no_password() {
|
||||
CURRENT_SCENARIO="modular_mtls_no_password"
|
||||
section "Modular mode — mTLS cert-only auth (no password)"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
start_tls_postgres "$pg_name" "$net" "cert"
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
# No POSTGRES_PASSWORD — cert-only auth
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e POSTGRES_SSL=true \
|
||||
-e POSTGRES_SSL_MODE=verify-ca \
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt \
|
||||
-e POSTGRES_SSL_CERT=/certs/client.crt \
|
||||
-e POSTGRES_SSL_KEY=/certs/client.key \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with mTLS cert-only auth"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed with mTLS"
|
||||
check_log_contains "$name" "PostgreSQL TLS: enabled" \
|
||||
"Django sees TLS enabled"
|
||||
check_migrations_done "$name"
|
||||
check_no_permission_errors "$name"
|
||||
else
|
||||
log_fail "Container failed to start with mTLS cert-only auth"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_mtls_with_password() {
|
||||
CURRENT_SCENARIO="modular_mtls_with_password"
|
||||
section "Modular mode — mTLS + password auth"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
# cert + md5 password
|
||||
start_tls_postgres "$pg_name" "$net" "cert"
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=tempsetup \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e POSTGRES_SSL=true \
|
||||
-e POSTGRES_SSL_MODE=verify-ca \
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt \
|
||||
-e POSTGRES_SSL_CERT=/certs/client.crt \
|
||||
-e POSTGRES_SSL_KEY=/certs/client.key \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with mTLS + password"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed with mTLS + password"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start with mTLS + password"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_tls_server_only() {
|
||||
CURRENT_SCENARIO="modular_tls_server_only"
|
||||
section "Modular mode — server-only TLS (no client cert)"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
# md5 auth over TLS (no client cert required)
|
||||
start_tls_postgres "$pg_name" "$net" "md5"
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=tempsetup \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e POSTGRES_SSL=true \
|
||||
-e POSTGRES_SSL_MODE=verify-ca \
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with server-only TLS"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed with server-only TLS"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start with server-only TLS"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_tls_key_permission() {
|
||||
CURRENT_SCENARIO="modular_tls_key_permission"
|
||||
section "Modular mode — mTLS with 0777 client key (Docker Desktop scenario)"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
start_tls_postgres "$pg_name" "$net" "cert"
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
# Create a copy of certs with 0777 key permissions
|
||||
local bad_perms_dir
|
||||
bad_perms_dir=$(mktemp -d)
|
||||
cp "$CERT_DIR"/ca.crt "$CERT_DIR"/client.crt "$CERT_DIR"/client.key "$bad_perms_dir/"
|
||||
chmod 777 "$bad_perms_dir/client.key"
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$bad_perms_dir" 2>/dev/null || echo "$bad_perms_dir")"
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e POSTGRES_SSL=true \
|
||||
-e POSTGRES_SSL_MODE=verify-ca \
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt \
|
||||
-e POSTGRES_SSL_CERT=/certs/client.crt \
|
||||
-e POSTGRES_SSL_KEY=/certs/client.key \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with 0777 client key"
|
||||
check_log_contains "$name" "Fixed PostgreSQL client key" \
|
||||
"Key permission fix triggered"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed after key fix"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start with 0777 client key"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
rm -rf "$bad_perms_dir"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_no_tls_regression() {
|
||||
CURRENT_SCENARIO="modular_no_tls_regression"
|
||||
section "Modular mode — no TLS (regression check)"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
# Plain PostgreSQL — no TLS
|
||||
docker run -d --name "$pg_name" --network "$net" \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=secret \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
postgres:17 >/dev/null
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt 30 ]; do
|
||||
if docker exec "$pg_name" su postgres -c "/usr/lib/postgresql/17/bin/pg_isready" 2>/dev/null | grep -q "accepting"; then
|
||||
break
|
||||
fi
|
||||
sleep 2; ((elapsed+=2))
|
||||
done
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=secret \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started without TLS (regression check)"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed without TLS"
|
||||
check_log_absent "$name" "Fixed PostgreSQL client key" \
|
||||
"No key fix when TLS disabled"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start without TLS"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_pg_verify_full() {
|
||||
CURRENT_SCENARIO="modular_pg_verify_full"
|
||||
section "Modular mode — PG mTLS with verify-full (CN must match hostname)"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
start_tls_postgres "$pg_name" "$net" "cert"
|
||||
|
||||
docker run -d --name "$redis_name" --network "$net" redis:latest >/dev/null
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
# verify-full requires server cert CN to match the hostname used to connect.
|
||||
# Our PG server cert CN is "${TEST_PREFIX}_pg", which matches the container name
|
||||
# used in POSTGRES_HOST.
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e POSTGRES_SSL=true \
|
||||
-e POSTGRES_SSL_MODE=verify-full \
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt \
|
||||
-e POSTGRES_SSL_CERT=/certs/client.crt \
|
||||
-e POSTGRES_SSL_KEY=/certs/client.key \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with verify-full"
|
||||
check_log_contains "$name" "PostgreSQL version check passed" \
|
||||
"Version check passed with verify-full"
|
||||
check_log_contains "$name" "sslmode=verify-full" \
|
||||
"Django reports verify-full mode"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start with verify-full"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_redis_tls() {
|
||||
CURRENT_SCENARIO="modular_redis_tls"
|
||||
section "Modular mode — Redis with TLS"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"; track_container "$name"
|
||||
|
||||
# Plain PG (no TLS) — isolate Redis TLS testing
|
||||
docker run -d --name "$pg_name" --network "$net" \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=secret \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
postgres:17 >/dev/null
|
||||
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt 30 ]; do
|
||||
if docker exec "$pg_name" su postgres -c "/usr/lib/postgresql/17/bin/pg_isready" 2>/dev/null | grep -q "accepting"; then
|
||||
break
|
||||
fi
|
||||
sleep 2; elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
start_tls_redis "$redis_name" "$net"
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
-e DISPATCHARR_ENV=modular \
|
||||
-e POSTGRES_HOST="$pg_name" \
|
||||
-e POSTGRES_PORT=5432 \
|
||||
-e POSTGRES_USER=dispatch \
|
||||
-e POSTGRES_PASSWORD=secret \
|
||||
-e POSTGRES_DB=dispatcharr \
|
||||
-e REDIS_HOST="$redis_name" \
|
||||
-e REDIS_SSL=true \
|
||||
-e REDIS_SSL_VERIFY=false \
|
||||
-e REDIS_SSL_CA_CERT=/certs/ca.crt \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if wait_for_ready "$name"; then
|
||||
log_pass "Container started with Redis TLS"
|
||||
check_log_contains "$name" "Redis TLS: enabled" \
|
||||
"Django reports Redis TLS enabled"
|
||||
check_log_contains "$name" "Redis at ${redis_name}" \
|
||||
"Redis connected via TLS"
|
||||
check_migrations_done "$name"
|
||||
else
|
||||
log_fail "Container failed to start with Redis TLS"
|
||||
fi
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
test_modular_full_tls_celery() {
|
||||
CURRENT_SCENARIO="modular_full_tls_celery"
|
||||
section "Modular mode — PG mTLS + Redis TLS with Celery container"
|
||||
|
||||
local name="${TEST_PREFIX}_app"
|
||||
local celery_name="${TEST_PREFIX}_celery"
|
||||
local pg_name="${TEST_PREFIX}_pg"
|
||||
local redis_name="${TEST_PREFIX}_redis"
|
||||
local net="${TEST_PREFIX}_net"
|
||||
local vol="${name}_data"
|
||||
cleanup_scenario
|
||||
|
||||
docker network create "$net" >/dev/null 2>&1
|
||||
fresh_volume "$vol"
|
||||
track_network "$net"
|
||||
track_container "$pg_name"; track_container "$redis_name"
|
||||
track_container "$name"; track_container "$celery_name"
|
||||
|
||||
start_tls_postgres "$pg_name" "$net" "cert"
|
||||
start_tls_redis "$redis_name" "$net"
|
||||
|
||||
local cert_mount
|
||||
cert_mount="$(cygpath -w "$CERT_DIR" 2>/dev/null || echo "$CERT_DIR")"
|
||||
|
||||
# Shared env vars for both web and celery containers
|
||||
local -a tls_env=(
|
||||
-e DISPATCHARR_ENV=modular
|
||||
-e POSTGRES_HOST="$pg_name"
|
||||
-e POSTGRES_PORT=5432
|
||||
-e POSTGRES_USER=dispatch
|
||||
-e POSTGRES_DB=dispatcharr
|
||||
-e REDIS_HOST="$redis_name"
|
||||
-e POSTGRES_SSL=true
|
||||
-e POSTGRES_SSL_MODE=verify-ca
|
||||
-e POSTGRES_SSL_CA_CERT=/certs/ca.crt
|
||||
-e POSTGRES_SSL_CERT=/certs/client.crt
|
||||
-e POSTGRES_SSL_KEY=/certs/client.key
|
||||
-e REDIS_SSL=true
|
||||
-e REDIS_SSL_VERIFY=false
|
||||
-e REDIS_SSL_CA_CERT=/certs/ca.crt
|
||||
)
|
||||
|
||||
# Start web container first (generates JWT, runs migrations)
|
||||
docker run -d --name "$name" --network "$net" \
|
||||
"${tls_env[@]}" \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
if ! wait_for_ready "$name"; then
|
||||
log_fail "Web container failed to start with full TLS"
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
return
|
||||
fi
|
||||
log_pass "Web container started with PG mTLS + Redis TLS"
|
||||
|
||||
# Start Celery container (shares /data volume for JWT, waits for migrations)
|
||||
docker run -d --name "$celery_name" --network "$net" \
|
||||
"${tls_env[@]}" \
|
||||
-e DJANGO_SETTINGS_MODULE=dispatcharr.settings \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-v "${cert_mount}:/certs:ro" \
|
||||
-v "${vol}:/data" \
|
||||
--entrypoint /app/docker/entrypoint.celery.sh \
|
||||
"$IMAGE_NAME" >/dev/null
|
||||
|
||||
# Wait for Celery to start (look for "starting Celery" message)
|
||||
local elapsed=0
|
||||
local celery_ok=false
|
||||
while [ $elapsed -lt 90 ]; do
|
||||
if ! docker ps -q -f "name=^${celery_name}$" 2>/dev/null | grep -q .; then
|
||||
echo " Celery container exited unexpectedly"
|
||||
break
|
||||
fi
|
||||
if docker logs "$celery_name" 2>&1 | grep -q "starting Celery"; then
|
||||
celery_ok=true
|
||||
break
|
||||
fi
|
||||
sleep 3; elapsed=$((elapsed + 3))
|
||||
done
|
||||
|
||||
if [ "$celery_ok" = true ]; then
|
||||
log_pass "Celery container started with PG mTLS + Redis TLS"
|
||||
check_log_contains "$celery_name" "Migrations complete" \
|
||||
"Celery confirmed migrations complete via TLS"
|
||||
check_log_contains "$celery_name" "PostgreSQL TLS: enabled" \
|
||||
"Celery sees PostgreSQL TLS enabled"
|
||||
check_log_contains "$celery_name" "Redis TLS: enabled" \
|
||||
"Celery sees Redis TLS enabled"
|
||||
else
|
||||
log_fail "Celery container failed to start with full TLS"
|
||||
echo -e " ${YELLOW}--- Celery logs ---${NC}"
|
||||
docker logs "$celery_name" 2>&1 | tail -20 | sed 's/^/ /'
|
||||
echo -e " ${YELLOW}--- End logs ---${NC}"
|
||||
fi
|
||||
|
||||
dump_logs_on_fail "$name"
|
||||
cleanup_scenario
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Main
|
||||
###############################################################################
|
||||
echo -e "${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}║ Dispatcharr — TLS Integration Tests ║${NC}"
|
||||
echo -e "${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||||
|
||||
# Build image
|
||||
if [ "$SKIP_BUILD" = false ]; then
|
||||
echo -e "\n${BOLD}Building test image...${NC}"
|
||||
if ! docker build -t "$IMAGE_NAME" -f docker/Dockerfile . 2>&1 | tail -5; then
|
||||
echo -e "${RED}Build failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}Build complete${NC}"
|
||||
else
|
||||
echo -e "\n${YELLOW}Skipping build (--skip-build)${NC}"
|
||||
fi
|
||||
|
||||
# Generate certificates
|
||||
generate_test_certs || exit 1
|
||||
|
||||
# Run scenarios
|
||||
SCENARIOS=(
|
||||
modular_mtls_no_password
|
||||
modular_mtls_with_password
|
||||
modular_tls_server_only
|
||||
modular_tls_key_permission
|
||||
modular_no_tls_regression
|
||||
modular_pg_verify_full
|
||||
modular_redis_tls
|
||||
modular_full_tls_celery
|
||||
)
|
||||
|
||||
for scenario in "${SCENARIOS[@]}"; do
|
||||
if [ -n "$SINGLE_SCENARIO" ] && [ "$scenario" != "$SINGLE_SCENARIO" ]; then
|
||||
continue
|
||||
fi
|
||||
"test_${scenario}"
|
||||
done
|
||||
|
||||
# Clean up certs
|
||||
rm -rf "$CERT_DIR"
|
||||
|
||||
# Summary
|
||||
echo -e "\n${BOLD}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e " ${GREEN}Passed: $PASS${NC} ${RED}Failed: $FAIL${NC} ${YELLOW}Skipped: $SKIP${NC}"
|
||||
if [ ${#ERRORS[@]} -gt 0 ]; then
|
||||
echo -e "\n ${RED}Failures:${NC}"
|
||||
for err in "${ERRORS[@]}"; do
|
||||
echo -e " ${RED}• $err${NC}"
|
||||
done
|
||||
fi
|
||||
echo -e "${BOLD}═══════════════════════════════════════════════════════════${NC}"
|
||||
|
||||
[ $FAIL -eq 0 ]
|
||||
@@ -0,0 +1,84 @@
|
||||
[uwsgi]
|
||||
; exec-before = python manage.py collectstatic --noinput
|
||||
; exec-before = python manage.py migrate --noinput
|
||||
|
||||
; First run Redis availability check script once
|
||||
exec-before = python /app/scripts/wait_for_redis.py
|
||||
|
||||
; Start Redis first
|
||||
attach-daemon = redis-server
|
||||
; Then start other services with configurable nice level (default: 5 for low priority)
|
||||
; Users can override via CELERY_NICE_LEVEL environment variable in docker-compose
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr worker --autoscale=6,1
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr beat
|
||||
attach-daemon = daphne -b 0.0.0.0 -p 8001 dispatcharr.asgi:application
|
||||
attach-daemon = cd /app/frontend && npm run dev
|
||||
|
||||
# Core settings
|
||||
chdir = /app
|
||||
module = scripts.debug_wrapper:application
|
||||
virtualenv = /dispatcharrpy
|
||||
master = true
|
||||
env = DJANGO_SETTINGS_MODULE=dispatcharr.settings
|
||||
socket = /app/uwsgi.sock
|
||||
chmod-socket = 777
|
||||
vacuum = true
|
||||
die-on-term = true
|
||||
static-map = /static=/app/static
|
||||
|
||||
# Worker configuration
|
||||
workers = 1
|
||||
lazy-apps = true
|
||||
|
||||
# HTTP server
|
||||
http = 0.0.0.0:5656
|
||||
http-keepalive = 1
|
||||
buffer-size = 65536
|
||||
http-timeout = 600
|
||||
|
||||
# Async mode (use gevent for high concurrency)
|
||||
gevent = 100
|
||||
async = 100
|
||||
|
||||
# Performance tuning
|
||||
thunder-lock = true
|
||||
log-4xx = true
|
||||
log-5xx = true
|
||||
disable-logging = false
|
||||
log-buffering = 1024 # Add buffer size limit for logging
|
||||
|
||||
; Longer timeouts for debugging sessions
|
||||
harakiri = 3600
|
||||
socket-timeout = 3600
|
||||
http-timeout = 3600
|
||||
|
||||
|
||||
# Ignore unknown options
|
||||
ignore-sigpipe = true
|
||||
ignore-write-errors = true
|
||||
disable-write-exception = true
|
||||
|
||||
# Debugging settings
|
||||
py-autoreload = 1
|
||||
honour-stdin = true
|
||||
|
||||
# Environment variables
|
||||
env = PYTHONPATH=/app
|
||||
env = PYTHONUNBUFFERED=1
|
||||
env = PYDEVD_DISABLE_FILE_VALIDATION=1
|
||||
env = PYTHONUTF8=1
|
||||
env = PYTHONXOPT=-Xfrozen_modules=off
|
||||
env = PYDEVD_DEBUG=1
|
||||
env = DEBUGPY_LOG_DIR=/app/debugpy_logs
|
||||
|
||||
# Debugging control variables
|
||||
env = WAIT_FOR_DEBUGGER=false
|
||||
env = DEBUG_TIMEOUT=30
|
||||
|
||||
# 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 the environment variable in log format - ensure consistent formatting with other files
|
||||
log-format = %(ftime) $(DISPATCHARR_LOG_LEVEL) uwsgi.requests Worker ID: %(wid) %(method) %(status) %(uri) %(msecs)ms
|
||||
@@ -0,0 +1,60 @@
|
||||
[uwsgi]
|
||||
; Remove file creation commands since we're not logging to files anymore
|
||||
; exec-pre = mkdir -p /data/logs
|
||||
; exec-pre = touch /data/logs/uwsgi-dev.log
|
||||
; exec-pre = chmod 666 /data/logs/uwsgi-dev.log
|
||||
|
||||
; First run Redis availability check script once
|
||||
exec-pre = python /app/scripts/wait_for_redis.py
|
||||
|
||||
; Start Redis first
|
||||
attach-daemon = redis-server --protected-mode no
|
||||
; Then start other services with configurable nice level (default: 5 for low priority)
|
||||
; Users can override via CELERY_NICE_LEVEL environment variable in docker-compose
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr worker --autoscale=6,1
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr beat
|
||||
attach-daemon = daphne -b 0.0.0.0 -p 8001 dispatcharr.asgi:application
|
||||
attach-daemon = cd /app/frontend && npm run dev
|
||||
|
||||
# Core settings
|
||||
chdir = /app
|
||||
module = dispatcharr.wsgi:application
|
||||
virtualenv = /dispatcharrpy
|
||||
master = true
|
||||
env = DJANGO_SETTINGS_MODULE=dispatcharr.settings
|
||||
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
|
||||
lazy-apps = true # Improve memory efficiency
|
||||
|
||||
# Async mode (use gevent for high concurrency)
|
||||
gevent = 100
|
||||
async = 100
|
||||
|
||||
# Performance tuning
|
||||
thunder-lock = true
|
||||
log-4xx = true
|
||||
log-5xx = true
|
||||
disable-logging = false
|
||||
|
||||
# Logging configuration - development mode
|
||||
# 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
|
||||
@@ -0,0 +1,62 @@
|
||||
[uwsgi]
|
||||
; 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 Redis first
|
||||
attach-daemon = redis-server
|
||||
; Then start other services with configurable nice level (default: 5 for low priority)
|
||||
; Users can override via CELERY_NICE_LEVEL environment variable in docker-compose
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr worker --autoscale=6,1
|
||||
attach-daemon = nice -n $(CELERY_NICE_LEVEL) celery -A dispatcharr beat
|
||||
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
|
||||
@@ -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
|
||||
|
||||
; Redis wait + flush is handled by the entrypoint in modular mode
|
||||
; (uWSGI exec-pre runs under 'su -' which strips Docker env vars)
|
||||
|
||||
; 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
|
||||
Reference in New Issue
Block a user