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:
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>
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[@]}"
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:
- “OBS is already running! Unless you meant to do this...”
- “OBS Studio did not properly shut down. Run in Safe 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?”