Last active
July 21, 2026 18:11
-
-
Save Potherca/0d9897e5d0b28e5c02f9ad65f75e003d to your computer and use it in GitHub Desktop.
BASH script to create a Spotify single-purpose device on a Debian machine https://gist.pother.ca/spotify-setup.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # spotify-setup.sh | |
| # | |
| # Turns a freshly installed, desktop-less Debian machine into a | |
| # dedicated "Spotify appliance": power on -> auto-login -> X starts -> | |
| # Openbox starts -> Spotify appears full-screen. No taskbar, no file | |
| # manager, no desktop environment. | |
| # | |
| # --------------------------------------------------------------------- | |
| # PREREQUISITES (must already be true before you run this): | |
| # - Debian 13 (or current Stable) installed with ONLY the "Standard | |
| # system utilities" task selected. No GNOME/KDE/XFCE/etc. | |
| # - You are logged in as the regular (non-root) user who should own | |
| # the Spotify session, and that user is in the sudoers group. | |
| # - The machine has a working internet connection. | |
| # | |
| # USAGE: | |
| # 1. Download this script: `wget 'https://gist.pother.ca/spotify-setup.sh'` | |
| # 2. Run it: `bash ./spotify-setup.sh` | |
| # 3. Answer the prompts as they appear | |
| # | |
| # DESIGN NOTES FOR FUTURE MAINTAINERS: | |
| # - Re-running this script is mostly safe (apt installs are | |
| # idempotent, config files are regenerated from scratch or guarded | |
| # against duplicate appends) but always skim the diff of anything | |
| # it overwrites if you're re-running after a partial failure. | |
| # | |
| # For debugging, packages can be installed, command run and output shared | |
| # through pastebin: | |
| # | |
| # sudo apt install -y alsa-utils pastebinit pulseaudio-utils | |
| # systemctl --user status pipewire pipewire-pulse wireplumber \ | |
| # | pastebinit -b pastebin.com -E | |
| # | |
| # Or output multiple commands to an output file and paste that: | |
| # systemctl --no-pager --user status pipewire pipewire-pulse wireplumber >> output.txt | |
| # grep -nE '(pipewire|wireplumber|systemctl)' spotify-setup.sh >> output.txt | |
| # aplay -l >> output.txt | |
| # ps -ef | grep -E '(pipe|pulse)' >> output.txt | |
| # pactl info >> output.txt | |
| # wpctl status >> output.txt | |
| # journalctl --user -u wireplumber -b --no-pager | grep -i fail >> output.txt | |
| # systemctl --user show-environment >> output.txt | |
| # pastebinit -b pastebin.com -i output.txt | |
| set -e # Fail fast - don't limp forward with a half-configured box. | |
| # We ask a lot of yes/no questions, so we need a real terminal attached. | |
| if [ ! -t 0 ]; then | |
| echo "This script asks interactive yes/no questions and needs a real" | |
| echo "terminal on stdin. Run it directly, e.g.:" | |
| echo " ./spotify-setup.sh" | |
| echo "rather than piping input into it or redirecting stdin." | |
| exit 1 | |
| fi | |
| echo "==================================================================" | |
| echo " Spotify Appliance Setup" | |
| echo "==================================================================" | |
| echo "This will install Xorg, Openbox, PipeWire and the official" | |
| echo "Spotify client, and configure this machine to boot straight into" | |
| echo "a full-screen Spotify window with no desktop environment." | |
| echo | |
| read -p "Continue? [y/N]: " CONFIRM_START | |
| if [[ ! "$CONFIRM_START" =~ ^[Yy]$ ]]; then | |
| echo "Aborted - nothing was changed." | |
| exit 0 | |
| fi | |
| # Refuse to run as root: we need a real, non-root "appliance user" whose | |
| # home directory we can drop dotfiles into, and who we can autologin as. | |
| if [ "$(id -u)" -eq 0 ]; then | |
| echo "Please run this as the regular user who should own the Spotify" | |
| echo "session (it calls sudo itself whenever it needs root), not as root." | |
| exit 1 | |
| fi | |
| APPLIANCE_USER="$(whoami)" | |
| APPLIANCE_HOME="$HOME" | |
| echo "Setting up the appliance for user: $APPLIANCE_USER (home: $APPLIANCE_HOME)" | |
| echo | |
| # Cache sudo credentials up front, then keep the sudo timestamp alive in | |
| # the background for the rest of the script. Without this, a slow step | |
| # like 'apt full-upgrade' can cause sudo's cached credentials to expire | |
| # and re-prompt for a password in the middle of an unattended-feeling run. | |
| echo "This script needs sudo for package installs and system config -" | |
| echo "you may be asked for your password now." | |
| sudo -v | |
| ( while true; do sudo -n true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) 2>/dev/null & | |
| SUDO_KEEPALIVE_PID=$! | |
| trap 'kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true' EXIT | |
| # ======================================================================= | |
| # STEP 1/11: Update the system | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 1/11: Updating system packages ----" | |
| sudo apt update | |
| sudo apt full-upgrade -y | |
| # ======================================================================= | |
| # STEP 2/11: Install packages | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 2/11: Installing packages ----" | |
| sudo apt install -y \ | |
| dbus-x11 \ | |
| fonts-dejavu \ | |
| gnupg \ | |
| mesa-utils \ | |
| openbox \ | |
| pipewire \ | |
| pipewire-pulse \ | |
| wireplumber \ | |
| xinit \ | |
| xorg | |
| # ======================================================================= | |
| # STEP 3/11: Install Spotify from its official Debian repository | |
| # ======================================================================= | |
| # Copied from https://www.spotify.com/us/download/linux/ | |
| echo | |
| echo "---- Step 3/11: Install Spotify ----" | |
| wget --output-document - --quiet https://download.spotify.com/debian/pubkey_5384CE82BA52C83A.asc \ | |
| | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/spotify.gpg | |
| echo "deb https://repository.spotify.com stable non-free" \ | |
| | sudo tee /etc/apt/sources.list.d/spotify.list | |
| sudo apt-get update && sudo apt-get install -y spotify-client | |
| # Electron apps (Spotify included) sometimes refuse to start on a minimal | |
| # system with a sandbox permission error. If that happens, the quick | |
| # troubleshooting step is launching it with `spotify --no-sandbox` to | |
| # confirm that's the cause, then fix the chrome-sandbox helper's setuid | |
| # bit properly rather than leaving --no-sandbox on permanently. | |
| # ======================================================================= | |
| # STEP 4/11 (OPTIONAL, default yes): Manual smoke test | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 4/11: Manual test (optional, recommended) ----" | |
| echo "Before wiring up autologin and auto-start, it's worth confirming" | |
| echo "X actually starts, Spotify logs in, and audio plays on THIS" | |
| echo "hardware, rather than finding out after autologin is already live." | |
| echo | |
| echo "If you say yes, this starts a temporary X session with Spotify as" | |
| echo "its only window (no window manager yet - that comes later)." | |
| echo "Close the Spotify window to end the test and return here." | |
| echo "This must be run from the local console, not over SSH." | |
| read -p "Run a manual test of Spotify now? [Y/n]: " DO_TEST | |
| if [[ ! "$DO_TEST" =~ ^[Nn]$ ]]; then | |
| if [ -n "$DISPLAY" ]; then | |
| echo "It looks like you're already inside a graphical session -" | |
| echo "skipping the test to avoid starting a nested X server." | |
| else | |
| startx /usr/bin/spotify || echo "Test session exited." | |
| echo "Test session ended. Continuing setup..." | |
| fi | |
| else | |
| echo "Skipping manual test." | |
| fi | |
| # ======================================================================= | |
| # STEP 5/11: Configure Openbox autostart (core step, with two optional | |
| # sub-choices baked into the generated file: hiding the mouse cursor, | |
| # and auto-restarting Spotify if it crashes) | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 5/11: Configuring Openbox autostart ----" | |
| mkdir -p "$APPLIANCE_HOME/.config/openbox" | |
| AUTOSTART_FILE="$APPLIANCE_HOME/.config/openbox/autostart" | |
| echo | |
| echo "OPTIONAL: hide the mouse cursor when idle." | |
| echo "Installs 'unclutter', which hides the mouse pointer after 1 second" | |
| echo "of inactivity - useful for a kiosk display with no cursor wanted" | |
| echo "on screen. If this machine has no monitor (audio-only), this has" | |
| echo "no visible effect either way." | |
| read -p "Install and enable unclutter (hide mouse cursor)? [y/N]: " USE_UNCLUTTER | |
| if [[ "$USE_UNCLUTTER" =~ ^[Yy]$ ]]; then | |
| sudo apt install -y unclutter | |
| fi | |
| echo | |
| echo "OPTIONAL: automatically restart Spotify if it crashes or is force-" | |
| echo "closed (e.g. by an update). Recommended for a machine nobody will" | |
| echo "be sitting in front of to manually relaunch Spotify." | |
| read -p "Enable auto-restart for Spotify? [Y/n]: " AUTO_RESTART_SPOTIFY | |
| # Regenerate the autostart file from scratch each run, based on current | |
| # answers - simplest way to keep it idempotent across re-runs. | |
| cat > "$AUTOSTART_FILE" << 'EOF' | |
| #!/bin/sh | |
| # Openbox autostart - runs once the Openbox session begins. | |
| # Generated by spotify-setup.sh - edits here will be | |
| # overwritten if that script is re-run. | |
| EOF | |
| if [[ "$USE_UNCLUTTER" =~ ^[Yy]$ ]]; then | |
| echo 'unclutter --timeout 1 &' >> "$AUTOSTART_FILE" | |
| fi | |
| if [[ ! "$AUTO_RESTART_SPOTIFY" =~ ^[Nn]$ ]]; then | |
| cat >> "$AUTOSTART_FILE" << 'EOF' | |
| # Auto-restart loop: if Spotify exits for any reason, relaunch it. | |
| while true | |
| do | |
| spotify | |
| sleep 2 | |
| done & | |
| EOF | |
| else | |
| echo 'spotify &' >> "$AUTOSTART_FILE" | |
| fi | |
| chmod +x "$AUTOSTART_FILE" | |
| # ======================================================================= | |
| # STEP 6/11: Create .xinitrc (core step; bundles in disabling screen | |
| # blanking, since that's what the original guide does too) | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 6/11: Creating .xinitrc ----" | |
| cat > "$APPLIANCE_HOME/.xinitrc" << 'EOF' | |
| #!/bin/sh | |
| # .xinitrc - run by 'startx' to set up the X session. | |
| # Generated by spotify-setup.sh. | |
| # Disable screen blanking / DPMS power-saving / screensaver so a | |
| # connected display never goes dark - appropriate for a kiosk that | |
| # should always show the Spotify UI. Harmless no-op if no monitor | |
| # is attached. | |
| xset s off | |
| xset -dpms | |
| xset s noblank | |
| exec openbox-session | |
| EOF | |
| chmod +x "$APPLIANCE_HOME/.xinitrc" | |
| # ======================================================================= | |
| # STEP 7/11 (OPTIONAL, default no): Force Spotify to always open | |
| # maximized/fullscreen via an Openbox window rule | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 7/11: Force Spotify fullscreen (optional) ----" | |
| echo "Spotify remembers its own window size between runs, so pressing" | |
| echo "F11 (or maximizing) once by hand is often enough. If this machine" | |
| echo "has no keyboard/mouse attached to do that with, you can instead" | |
| echo "force it via an Openbox rule so it ALWAYS opens maximized." | |
| read -p "Force Spotify to always open maximized/fullscreen? [y/N]: " FORCE_FULLSCREEN | |
| if [[ "$FORCE_FULLSCREEN" =~ ^[Yy]$ ]]; then | |
| RC_XML="$APPLIANCE_HOME/.config/openbox/rc.xml" | |
| if [ ! -f "$RC_XML" ]; then | |
| # No custom rc.xml yet - start from Openbox's own default config | |
| # so we don't have to hand-write the rest of it (it's long). | |
| cp /etc/xdg/openbox/rc.xml "$RC_XML" | |
| fi | |
| if grep -q 'class="Spotify"' "$RC_XML"; then | |
| echo "rc.xml already has a Spotify rule - leaving it as-is." | |
| elif grep -q '</applications>' "$RC_XML"; then | |
| # Default rc.xml already has an <applications> section - add | |
| # our rule inside it rather than creating a second one, since | |
| # Openbox's handling of duplicate top-level sections is not | |
| # something we want to rely on. | |
| sed -i '/<\/applications>/i\ | |
| <application class="Spotify">\ | |
| <maximized>yes</maximized>\ | |
| <fullscreen>yes</fullscreen>\ | |
| </application>' "$RC_XML" | |
| else | |
| # No <applications> section at all - add a whole new one just | |
| # before the closing </openbox_config> tag. | |
| sed -i '/<\/openbox_config>/i\ | |
| <applications>\ | |
| <application class="Spotify">\ | |
| <maximized>yes</maximized>\ | |
| <fullscreen>yes</fullscreen>\ | |
| </application>\ | |
| </applications>' "$RC_XML" | |
| fi | |
| echo "rc.xml updated. This is read fresh each time Openbox starts," | |
| echo "so it will take effect on next boot/login." | |
| fi | |
| # ======================================================================= | |
| # STEP 8/11: Automatic login on tty1 (confirm - real security trade-off) | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 8/11: Automatic login on tty1 ----" | |
| echo "This makes '$APPLIANCE_USER' log in automatically on the text" | |
| echo "console (tty1), no password prompt. Combined with the next step" | |
| echo "(auto-starting X), this is what makes 'power on' turn straight" | |
| echo "into Spotify with no login screen at all." | |
| echo | |
| echo "SECURITY NOTE: anyone who can power this machine on gets a logged" | |
| echo "-in session as '$APPLIANCE_USER' with no password. Only enable" | |
| echo "this on a machine that is physically dedicated to this one job." | |
| read -p "Enable automatic login for '$APPLIANCE_USER' on tty1? [y/N]: " ENABLE_AUTOLOGIN | |
| if [[ "$ENABLE_AUTOLOGIN" =~ ^[Yy]$ ]]; then | |
| sudo mkdir -p /etc/systemd/system/getty@tty1.service.d | |
| sudo tee /etc/systemd/system/getty@tty1.service.d/autologin.conf > /dev/null << EOF | |
| [Service] | |
| ExecStart= | |
| ExecStart=-/sbin/agetty --autologin $APPLIANCE_USER --noclear %I \$TERM | |
| EOF | |
| sudo systemctl daemon-reload | |
| AUTOLOGIN_ENABLED=1 | |
| echo "Autologin configured. Takes effect on next boot." | |
| else | |
| AUTOLOGIN_ENABLED=0 | |
| echo "Skipping autologin - you'll log in manually at each boot." | |
| fi | |
| # ======================================================================= | |
| # STEP 9/11: Automatically start X after login (core step, with one | |
| # optional sub-choice: restart X in a loop if it ever exits) | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 9/11: Auto-starting X after login ----" | |
| echo "This appends a snippet to ~/.bash_profile so that as soon as" | |
| echo "'$APPLIANCE_USER' logs into tty1 (whether typed manually or via" | |
| echo "autologin above), X starts automatically -> Openbox -> Spotify." | |
| echo | |
| echo "OPTIONAL: if X ever exits or crashes (e.g. a graphics driver" | |
| echo "hiccup), should it restart automatically in a loop, or drop back" | |
| echo "to a plain text login prompt so you can investigate by hand?" | |
| read -p "Automatically restart X if it exits? [Y/n]: " RESTART_X | |
| BASH_PROFILE="$APPLIANCE_HOME/.bash_profile" | |
| if grep -q "Spotify appliance" "$BASH_PROFILE" 2>/dev/null; then | |
| echo "~/.bash_profile already has an auto-start-X block from a" | |
| echo "previous run of this script - leaving it as-is. Edit" | |
| echo "$BASH_PROFILE by hand if you want to change this behavior." | |
| elif [[ ! "$RESTART_X" =~ ^[Nn]$ ]]; then | |
| cat >> "$BASH_PROFILE" << 'EOF' | |
| # --- Spotify appliance: auto-start X on tty1, restart if it exits --- | |
| if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then | |
| while true | |
| do | |
| startx | |
| sleep 2 | |
| done | |
| fi | |
| EOF | |
| else | |
| cat >> "$BASH_PROFILE" << 'EOF' | |
| # --- Spotify appliance: auto-start X on tty1 (once) --- | |
| if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then | |
| exec startx | |
| fi | |
| EOF | |
| fi | |
| # ======================================================================= | |
| # STEP 10/11 (OPTIONAL, default no): Restrict virtual terminals | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 10/11: Restrict virtual terminals (optional) ----" | |
| echo "This is a 'best effort' step, not a hard lock. It reduces the" | |
| echo "number of virtual terminals systemd auto-spawns a login prompt on," | |
| echo "from the default of 6 down to just tty1 (where Spotify runs)." | |
| echo "It does NOT stop someone with a physical keyboard from pressing" | |
| echo "Ctrl+Alt+F2..F6 to try to switch away - it only stops systemd from" | |
| echo "pre-spawning logins on the other consoles. For a real guarantee," | |
| echo "the simplest fix (straight from the original guide) is to just" | |
| echo "not attach a keyboard to this machine." | |
| read -p "Reduce auto-spawned virtual terminals to 1 (tty1 only)? [y/N]: " LIMIT_VTS | |
| if [[ "$LIMIT_VTS" =~ ^[Yy]$ ]]; then | |
| sudo sed -i 's/^#\?NAutoVTs=.*/NAutoVTs=1/' /etc/systemd/logind.conf | |
| sudo sed -i 's/^#\?ReserveVT=.*/ReserveVT=1/' /etc/systemd/logind.conf | |
| grep -q '^NAutoVTs=1' /etc/systemd/logind.conf || echo "NAutoVTs=1" | sudo tee -a /etc/systemd/logind.conf > /dev/null | |
| grep -q '^ReserveVT=1' /etc/systemd/logind.conf || echo "ReserveVT=1" | sudo tee -a /etc/systemd/logind.conf > /dev/null | |
| echo "Updated /etc/systemd/logind.conf - takes effect after a reboot." | |
| fi | |
| # ======================================================================= | |
| # STEP 11/11 (OPTIONAL, default no per service): Disable unnecessary | |
| # services | |
| # ======================================================================= | |
| echo | |
| echo "---- Step 11/11: Disable unnecessary services (optional) ----" | |
| echo "These are 'commonly' unneeded on a dedicated Spotify appliance -" | |
| echo "'commonly' is not 'always', so each one is asked separately:" | |
| echo " - ModemManager : cellular/mobile broadband modems. Safe to" | |
| echo " disable unless this PC gets online via a" | |
| echo " 3G/4G USB modem." | |
| echo " - cups : printing support. Safe to disable - this" | |
| echo " machine doesn't print." | |
| echo " - bluetooth : Bluetooth stack. DO NOT disable this if you" | |
| echo " play audio through Bluetooth speakers or a" | |
| echo " Bluetooth keyboard/remote." | |
| echo " - avahi-daemon : mDNS/Bonjour network discovery. Safe to" | |
| echo " disable unless something needs to find this" | |
| echo " machine on the network by name." | |
| echo | |
| for svc in ModemManager cups bluetooth avahi-daemon; do | |
| if systemctl list-unit-files | grep -q "^${svc}\.service"; then | |
| read -p "Disable '$svc'? [y/N]: " DISABLE_SVC | |
| if [[ "$DISABLE_SVC" =~ ^[Yy]$ ]]; then | |
| sudo systemctl disable --now "$svc" | |
| echo "$svc disabled." | |
| fi | |
| else | |
| echo "$svc is not installed - nothing to disable." | |
| fi | |
| done | |
| # ======================================================================= | |
| # Done - summary | |
| # ======================================================================= | |
| echo | |
| echo "==================================================================" | |
| echo " Setup complete!" | |
| echo "==================================================================" | |
| echo "Configured for user '$APPLIANCE_USER':" | |
| echo " - Xorg + Openbox + PipeWire + Spotify installed" | |
| echo " - $AUTOSTART_FILE" | |
| echo " - $APPLIANCE_HOME/.xinitrc" | |
| if [ "$AUTOLOGIN_ENABLED" -eq 1 ]; then | |
| echo " - Automatic login: ENABLED" | |
| else | |
| echo " - Automatic login: not enabled (manual login required)" | |
| fi | |
| echo " - $BASH_PROFILE (auto-starts X on tty1 login)" | |
| echo | |
| echo "Reboot to see the full appliance experience:" | |
| echo " sudo reboot" | |
| echo | |
| echo "Files worth knowing about if something looks wrong later:" | |
| echo " ~/.config/openbox/autostart" | |
| echo " ~/.xinitrc" | |
| echo " ~/.config/openbox/rc.xml (only if fullscreen rule enabled)" | |
| echo " /etc/systemd/system/getty@tty1.service.d/autologin.conf" | |
| echo " ~/.bash_profile" | |
| echo " /etc/systemd/logind.conf (only if VT step enabled)" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment