Multiple headless OBS instances via systemd trigger “already running” and “safe mode” when opening GUI manually

goriofab

New Member
Hi everyone

I’m running multiple headless OBS Studio instances on a Linux production server (Ubuntu 22.04.5 LTS).
Each instance runs under its own systemd unit (obs@.service) and works perfectly for continuous live streaming — the only issue occurs when I open the OBS GUI through VNC.

Whenever I manually launch OBS via GUI on the same machine, I get these dialogs:

  1. “OBS is already running! Unless you meant to do this...”
  2. “OBS Studio did not properly shut down. Run in Safe Mode?”
Everything else (streaming, restarts, timers) runs great in headless mode.


System overview

Each OBS instance is managed by a systemd template unit, for example:


sudo systemctl restart obs@CGH1.service

Inspecting the unit:


$ systemctl show -p ExecStart obs@CGH1.service | sed 's/^ExecStart=//'
{ path=/opt/obs-mgr/obs-instance.sh ; argv[]=/opt/obs-mgr/obs-instance.sh CGH1 ; ... }

So every OBS process is launched through this wrapper:


/opt/obs-mgr/obs-instance.sh <instance_name>


⚙️ Launcher script (/opt/obs-mgr/obs-instance.sh)​

Below is the full script used by all instances.
It isolates configs per instance, runs Xvfb, fluxbox, and x11vnc, and finally starts OBS in headless mode.


#!/usr/bin/env bash
set -euo pipefail

INSTANCE_NAME="${1:-}"
if [[ -z "$INSTANCE_NAME" ]]; then
echo "Usage: $0 <instance-name>"; exit 1
fi

ENV_FILE="/etc/obs-instances/${INSTANCE_NAME}.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Env file not found: $ENV_FILE"; exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"

: "${DISPLAY_ID:?define DISPLAY_ID in env}"
: "${VNC_PORT:?define VNC_PORT in env}"
: "${RESOLUTION:=1920x1080}"
: "${DEPTH:=24}"

: "${OBS_PROFILE:=$INSTANCE_NAME}"
: "${OBS_COLLECTION:=$INSTANCE_NAME}"

: "${VNC_PASSFILE:=/etc/obs-instances/${INSTANCE_NAME}.vncpass}"

export DISPLAY=":${DISPLAY_ID}"
export XDG_CONFIG_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.config"
export XDG_CACHE_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.cache"
export XDG_DATA_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.local/share"

TMP_BASE="/tmp/obsrunner-runtime"
mkdir -p "$TMP_BASE" || true
chown obsrunner:obsrunner "$TMP_BASE" || true
chmod 700 "$TMP_BASE" || true
export XDG_RUNTIME_DIR="$TMP_BASE"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-xcb}"
export GTK_USE_PORTAL=0
export XDG_CURRENT_DESKTOP=generic
export XDG_SESSION_TYPE=headless

# --- Remove leftover locks and crash markers ---
lockdir="${XDG_CONFIG_HOME}/obs-studio"
rm -f "${lockdir}/obs-studio.lock" "${lockdir}/.obs-studio.lock" "${lockdir}/obs-studio.pid" 2>/dev/null || true

crashdir="${XDG_CONFIG_HOME}/obs-studio/basic"
rm -f "${crashdir}/crashmarker" "${crashdir}/last_crash.txt" "${crashdir}/crash.log" 2>/dev/null || true

# --- Start Xvfb / Fluxbox / x11vnc ---
if ! pgrep -f "Xvfb :${DISPLAY_ID} " >/dev/null; then
/usr/bin/Xvfb "$DISPLAY" -screen 0 "${RESOLUTION}x${DEPTH}" -nolisten tcp -noreset &
fi
for _ in {1..50}; do
[[ -S "/tmp/.X11-unix/X${DISPLAY_ID}" ]] && break
sleep 0.2
done

if ! pgrep -f "fluxbox -display ${DISPLAY}" >/dev/null; then
fluxbox -display "$DISPLAY" >/dev/null 2>&1 &
fi

if ! pgrep -f "x11vnc .*:${VNC_PORT}\b" >/dev/null; then
if [[ -f "$VNC_PASSFILE" ]]; then
/usr/bin/x11vnc -display "$DISPLAY" -rfbport "$VNC_PORT" -rfbauth "$VNC_PASSFILE" \
-forever -shared -noxdamage -xkb >/dev/null 2>&1 &
else
/usr/bin/x11vnc -display "$DISPLAY" -rfbport "$VNC_PORT" \
-forever -shared -noxdamage -xkb -nopw >/dev/null 2>&1 &
fi
fi

# --- Cleanup again to ensure no startup popups ---
rm -f "${lockdir}/obs-studio.lock" "${lockdir}/.obs-studio.lock" "${lockdir}/obs-studio.pid" 2>/dev/null || true
rm -f "${crashdir}/crashmarker" "${crashdir}/last_crash.txt" "${crashdir}/crash.log" 2>/dev/null || true

# --- Start OBS headless ---
export LIBGL_ALWAYS_SOFTWARE=1
OBS_BIN="/usr/bin/obs"
OBS_FLAGS=( --startstreaming --disable-shutdown-check )
[[ -n "${OBS_PROFILE}" ]] && OBS_FLAGS+=( --profile "${OBS_PROFILE}" )
[[ -n "${OBS_COLLECTION}" ]] && OBS_FLAGS+=( --collection "${OBS_COLLECTION}" )

exec "$OBS_BIN" "${OBS_FLAGS[@]}"


Behavior

  • Each instance runs fine headlessly via systemctl restart obs@CGH1.service.
  • Config and cache directories are isolated (using XDG paths).
  • All lock/crash files are deleted at startup to suppress dialogs.
  • Still, when launching OBS GUI manually via VNC, these dialogs appear:
    • “OBS is already running! Unless you meant to do this…”
    • “OBS Studio did not properly shut down. Run in Safe Mode?”
 

Attachments

  • Captura de Tela 2025-11-10 às 20.02.05.png
    Captura de Tela 2025-11-10 às 20.02.05.png
    779 KB · Views: 1
  • Captura de Tela 2025-11-10 às 20.02.13.png
    Captura de Tela 2025-11-10 às 20.02.13.png
    743.2 KB · Views: 1

goriofab

New Member
Resolvido

#!/usr/bin/env bash
set -euo pipefail

INSTANCE_NAME="${1:-}"
if [[ -z "$INSTANCE_NAME" ]]; then
echo "Uso: $0 <instance-name>"; exit 1
fi

ENV_FILE="/etc/obs-instances/${INSTANCE_NAME}.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "Arquivo de env não encontrado: $ENV_FILE"; exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"

: "${DISPLAY_ID:?defina DISPLAY_ID no env}"
: "${VNC_PORT:?defina VNC_PORT no env}"
: "${RESOLUTION:=1920x1080}"
: "${DEPTH:=24}"

# Defaults: se não vierem no env, usa o próprio nome da instância
: "${OBS_PROFILE:=$INSTANCE_NAME}"
: "${OBS_COLLECTION:=$INSTANCE_NAME}"

# VNC passfile opcional
: "${VNC_PASSFILE:=/etc/obs-instances/${INSTANCE_NAME}.vncpass}"

export DISPLAY=":${DISPLAY_ID}"

# Isola as pastas de config/cache/dados por instância
export XDG_CONFIG_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.config"
export XDG_CACHE_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.cache"
export XDG_DATA_HOME="/var/lib/obsrunner/${INSTANCE_NAME}/.local/share"

# HOME isolado (alguns artefatos do OBS ignoram XDG e usam $HOME)
export HOME="/var/lib/obsrunner/${INSTANCE_NAME}"
mkdir -p "$HOME"

# Evita integrações gráficas desnecessárias em headless e corrige runtime
TMP_BASE="/tmp/obsrunner-runtime"
mkdir -p "$TMP_BASE" || true
chown obsrunner:obsrunner "$TMP_BASE" || true
chmod 700 "$TMP_BASE" || true
export XDG_RUNTIME_DIR="$TMP_BASE"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-xcb}"
export GTK_USE_PORTAL=0
export XDG_CURRENT_DESKTOP=generic
export XDG_SESSION_TYPE=headless

# Remove locks e PIDs antigos (evita 'Launch Anyway')
lockdir="${XDG_CONFIG_HOME}/obs-studio"
rm -f "${lockdir}/obs-studio.lock" "${lockdir}/.obs-studio.lock" "${lockdir}/obs-studio.pid" 2>/dev/null || true

# Remove crash markers (evita popup 'Safe Mode')
crashdir="${XDG_CONFIG_HOME}/obs-studio/basic"
rm -f "${crashdir}/crashmarker" "${crashdir}/last_crash.txt" "${crashdir}/crash.log" 2>/dev/null || true

# Garante diretórios da instância (sem chown recursivo pesado)
install -d -m 0755 -o obsrunner -g obsrunner \
"$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME"

# ------------------------------------------------------------------
# Xvfb
# ------------------------------------------------------------------
if ! pgrep -f "Xvfb :${DISPLAY_ID} " >/dev/null; then
/usr/bin/Xvfb "$DISPLAY" -screen 0 "${RESOLUTION}x${DEPTH}" -nolisten tcp -noreset &
fi

# Aguarda o socket UNIX do X aparecer (Xvfb com -nolisten tcp não abre :6000/TCP)
for _ in {1..50}; do
[[ -S "/tmp/.X11-unix/X${DISPLAY_ID}" ]] && break
sleep 0.2
done

# ------------------------------------------------------------------
# Window Manager leve (fluxbox)
# ------------------------------------------------------------------
if ! pgrep -f "fluxbox -display ${DISPLAY}" >/dev/null; then
fluxbox -display "$DISPLAY" >/dev/null 2>&1 &
fi

# ------------------------------------------------------------------
# VNC (sem -o /var/log/… para evitar permission denied)
# ------------------------------------------------------------------
if ! pgrep -f "x11vnc .*:${VNC_PORT}\b" >/dev/null; then
if [[ -f "$VNC_PASSFILE" ]]; then
/usr/bin/x11vnc -display "$DISPLAY" -rfbport "$VNC_PORT" -rfbauth "$VNC_PASSFILE" \
-forever -shared -noxdamage -xkb >/dev/null 2>&1 &
else
/usr/bin/x11vnc -display "$DISPLAY" -rfbport "$VNC_PORT" \
-forever -shared -noxdamage -xkb -nopw >/dev/null 2>&1 &
fi
fi

# ------------------------------------------------------------------
# Limpezas que bloqueiam o autostart do OBS (reforço)
# ------------------------------------------------------------------

# 1) Lock antigo do OBS (evita “LAUNCH ANYWAY”)
lockdir="${XDG_CONFIG_HOME}/obs-studio"
rm -f "${lockdir}/obs-studio.lock" "${lockdir}/.obs-studio.lock" "${lockdir}/obs-studio.pid" 2>/dev/null || true

# 2) Crash markers (eliminam popup “Safe Mode/Run in Normal Mode”)
crashdir="${XDG_CONFIG_HOME}/obs-studio/basic"
rm -f "${crashdir}/crashmarker" "${crashdir}/last_crash.txt" "${crashdir}/crash.log" 2>/dev/null || true
rm -f "${XDG_CONFIG_HOME}/obs-studio/logs/last_crash"* "${XDG_CONFIG_HOME}/obs-studio/crashes/"* 2>/dev/null || true

# ------------------------------------------------------------------
# OBS (headless)
# ------------------------------------------------------------------
# Opcional: força render por software se a GPU não estiver liberada
: "${LIBGL_ALWAYS_SOFTWARE:=1}"
export LIBGL_ALWAYS_SOFTWARE

OBS_BIN="/usr/bin/obs"
# --multi: permite múltiplas instâncias; --no-splash: evita telas iniciais
OBS_FLAGS=( --startstreaming --disable-shutdown-check --multi --no-splash )
[[ -n "${OBS_PROFILE}" ]] && OBS_FLAGS+=( --profile "${OBS_PROFILE}" )
[[ -n "${OBS_COLLECTION}" ]] && OBS_FLAGS+=( --collection "${OBS_COLLECTION}" )

exec "$OBS_BIN" "${OBS_FLAGS[@]}"
 
Top