#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' # Root check if [[ $EUID -ne 0 ]]; then echo "[ERROR] This script must be run as root." >&2 exit 1 fi trap 'echo -e "\n[ERROR] Line $LINENO failed. Exiting." >&2; exit 1' ERR ############################################################################## # 0) Warning / Disclaimer ############################################################################## show_disclaimer() { echo "**************************************************************" echo "WARNING: While we do not anticipate any problems, we disclaim all" echo "responsibility for anything that happens to your machine." echo "" echo "This script is intended for **Debian-based operating systems only**." echo "Running it on other distributions WILL cause unexpected issues." echo "" echo "This script is **NOT RECOMMENDED** for use on your primary machine." echo "For safety and best results, we strongly advise running this inside a" echo "clean virtual machine (VM) or LXC container environment." echo "" echo "Additionally, there is NO SUPPORT for this method; Docker is the only" echo "officially supported way to run Dispatcharr." echo "**************************************************************" echo "" echo "If you wish to proceed, type \"I understand\" and press Enter." read user_input if [ "$user_input" != "I understand" ]; then echo "Exiting script..." exit 1 fi } ############################################################################## # 1) Configuration ############################################################################## configure_variables() { DISPATCH_USER="dispatcharr" DISPATCH_GROUP="dispatcharr" APP_DIR="/opt/dispatcharr" DISPATCH_BRANCH="main" POSTGRES_DB="dispatcharr" POSTGRES_USER="dispatch" POSTGRES_PASSWORD="secret" NGINX_HTTP_PORT="9191" WEBSOCKET_PORT="8001" GUNICORN_RUNTIME_DIR="dispatcharr" GUNICORN_SOCKET="/run/${GUNICORN_RUNTIME_DIR}/dispatcharr.sock" PYTHON_BIN=$(command -v python3) SYSTEMD_DIR="/etc/systemd/system" NGINX_SITE="/etc/nginx/sites-available/dispatcharr" } ############################################################################## # 2) Install System Packages ############################################################################## install_packages() { echo ">>> Installing system packages..." apt-get update declare -a packages=( git curl wget build-essential gcc libpq-dev python3-dev python3-venv python3-pip nginx redis-server postgresql postgresql-contrib ffmpeg procps streamlink sudo ) apt-get install -y --no-install-recommends "${packages[@]}" if ! command -v node >/dev/null 2>&1; then echo ">>> Installing Node.js..." curl -sL https://deb.nodesource.com/setup_24.x | bash - apt-get install -y nodejs fi systemctl enable --now postgresql redis-server } ############################################################################## # 3) Create User/Group ############################################################################## create_dispatcharr_user() { if ! getent group "$DISPATCH_GROUP" >/dev/null; then groupadd "$DISPATCH_GROUP" fi if ! id -u "$DISPATCH_USER" >/dev/null; then useradd -m -g "$DISPATCH_GROUP" -s /bin/bash "$DISPATCH_USER" fi } ############################################################################## # 4) PostgreSQL Setup ############################################################################## setup_postgresql() { echo ">>> Checking PostgreSQL database and user..." db_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='$POSTGRES_DB'") if [[ "$db_exists" != "1" ]]; then echo ">>> Creating database '${POSTGRES_DB}'..." sudo -u postgres createdb "$POSTGRES_DB" else echo ">>> Database '${POSTGRES_DB}' already exists, skipping creation." fi user_exists=$(sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='$POSTGRES_USER'") if [[ "$user_exists" != "1" ]]; then echo ">>> Creating user '${POSTGRES_USER}'..." sudo -u postgres psql -c "CREATE USER $POSTGRES_USER WITH PASSWORD '$POSTGRES_PASSWORD';" else echo ">>> User '${POSTGRES_USER}' already exists, skipping creation." fi echo ">>> Granting privileges..." sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_DB TO $POSTGRES_USER;" sudo -u postgres psql -c "ALTER DATABASE $POSTGRES_DB OWNER TO $POSTGRES_USER;" sudo -u postgres psql -d "$POSTGRES_DB" -c "ALTER SCHEMA public OWNER TO $POSTGRES_USER;" } ############################################################################## # 5) Clone Dispatcharr Repository ############################################################################## clone_dispatcharr_repo() { echo ">>> Installing or updating Dispatcharr in ${APP_DIR} ..." if [ ! -d "$APP_DIR" ]; then mkdir -p "$APP_DIR" chown "$DISPATCH_USER:$DISPATCH_GROUP" "$APP_DIR" fi if [ -d "$APP_DIR/.git" ]; then echo ">>> Updating existing Dispatcharr repo..." su - "$DISPATCH_USER" <>> Cloning Dispatcharr repo into ${APP_DIR}..." rm -rf "$APP_DIR"/* chown "$DISPATCH_USER:$DISPATCH_GROUP" "$APP_DIR" su - "$DISPATCH_USER" -c "git clone -b $DISPATCH_BRANCH https://github.com/Dispatcharr/Dispatcharr.git $APP_DIR" fi } ############################################################################## # 6) Setup Python Environment ############################################################################## setup_python_env() { echo ">>> Setting up Python virtual environment..." su - "$DISPATCH_USER" <>> Building frontend..." su - "$DISPATCH_USER" <>> Running Django migrations & collectstatic..." su - "$DISPATCH_USER" <>> Creating systemd service files..." # Gunicorn cat <${SYSTEMD_DIR}/dispatcharr.service [Unit] Description=Gunicorn for Dispatcharr After=network.target postgresql.service redis-server.service [Service] User=${DISPATCH_USER} Group=${DISPATCH_GROUP} WorkingDirectory=${APP_DIR} RuntimeDirectory=${GUNICORN_RUNTIME_DIR} RuntimeDirectoryMode=0775 Environment="PATH=${APP_DIR}/env/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" Environment="POSTGRES_DB=${POSTGRES_DB}" Environment="POSTGRES_USER=${POSTGRES_USER}" Environment="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" Environment="POSTGRES_HOST=localhost" ExecStartPre=/usr/bin/bash -c 'until pg_isready -h localhost -U ${POSTGRES_USER}; do sleep 1; done' ExecStart=${APP_DIR}/env/bin/gunicorn \\ --workers=4 \\ --worker-class=gevent \\ --timeout=300 \\ --bind unix:${GUNICORN_SOCKET} \\ dispatcharr.wsgi:application Restart=always KillMode=mixed SyslogIdentifier=dispatcharr StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF # Celery cat <${SYSTEMD_DIR}/dispatcharr-celery.service [Unit] Description=Celery Worker for Dispatcharr After=network.target redis-server.service Requires=dispatcharr.service [Service] User=${DISPATCH_USER} Group=${DISPATCH_GROUP} WorkingDirectory=${APP_DIR} Environment="PATH=${APP_DIR}/env/bin" Environment="POSTGRES_DB=${POSTGRES_DB}" Environment="POSTGRES_USER=${POSTGRES_USER}" Environment="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" Environment="POSTGRES_HOST=localhost" Environment="CELERY_BROKER_URL=redis://localhost:6379/0" ExecStart=${APP_DIR}/env/bin/celery -A dispatcharr worker -l info Restart=always KillMode=mixed SyslogIdentifier=dispatcharr-celery StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF # Celery Beat cat <${SYSTEMD_DIR}/dispatcharr-celerybeat.service [Unit] Description=Celery Beat Scheduler for Dispatcharr After=network.target redis-server.service Requires=dispatcharr.service [Service] User=${DISPATCH_USER} Group=${DISPATCH_GROUP} WorkingDirectory=${APP_DIR} Environment="PATH=${APP_DIR}/env/bin" Environment="POSTGRES_DB=${POSTGRES_DB}" Environment="POSTGRES_USER=${POSTGRES_USER}" Environment="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" Environment="POSTGRES_HOST=localhost" Environment="CELERY_BROKER_URL=redis://localhost:6379/0" ExecStart=${APP_DIR}/env/bin/celery -A dispatcharr beat -l info Restart=always KillMode=mixed SyslogIdentifier=dispatcharr-celerybeat StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF # Daphne cat <${SYSTEMD_DIR}/dispatcharr-daphne.service [Unit] Description=Daphne for Dispatcharr (ASGI/WebSockets) After=network.target Requires=dispatcharr.service [Service] User=${DISPATCH_USER} Group=${DISPATCH_GROUP} WorkingDirectory=${APP_DIR} Environment="PATH=${APP_DIR}/env/bin" Environment="POSTGRES_DB=${POSTGRES_DB}" Environment="POSTGRES_USER=${POSTGRES_USER}" Environment="POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" Environment="POSTGRES_HOST=localhost" ExecStart=${APP_DIR}/env/bin/daphne -b 0.0.0.0 -p ${WEBSOCKET_PORT} dispatcharr.asgi:application Restart=always KillMode=mixed SyslogIdentifier=dispatcharr-daphne StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF echo ">>> Creating Nginx config..." cat </etc/nginx/sites-available/dispatcharr.conf server { listen ${NGINX_HTTP_PORT}; location / { include proxy_params; proxy_pass http://unix:${GUNICORN_SOCKET}; } location /static/ { alias ${APP_DIR}/static/; } location /assets/ { alias ${APP_DIR}/frontend/dist/assets/; } location /media/ { alias ${APP_DIR}/media/; } location /ws/ { proxy_pass http://127.0.0.1:${WEBSOCKET_PORT}; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header Host \$host; } } EOF ln -sf /etc/nginx/sites-available/dispatcharr.conf /etc/nginx/sites-enabled/dispatcharr.conf [ -f /etc/nginx/sites-enabled/default ] && rm /etc/nginx/sites-enabled/default nginx -t systemctl restart nginx systemctl enable nginx } ############################################################################## # 11) Start Services ############################################################################## start_services() { echo ">>> Enabling and starting services..." systemctl daemon-reexec systemctl daemon-reload systemctl enable --now dispatcharr dispatcharr-celery dispatcharr-celerybeat dispatcharr-daphne } ############################################################################## # 12) Summary ############################################################################## show_summary() { server_ip=$(ip route get 1 | awk '{print $7; exit}') cat <