Installation — Docker Compose
Requirements
Section titled “Requirements”- Docker Engine and Docker Compose
- Bluetooth adapter on the host
- PulseAudio or PipeWire on the host
- Music Assistant on your network
The published image supports linux/amd64, linux/arm64, and linux/arm/v7.
Quick start
Section titled “Quick start”-
Pair the speaker on the host first
Terminal window bluetoothctlscan onpair AA:BB:CC:DD:EE:FFtrust AA:BB:CC:DD:EE:FFconnect AA:BB:CC:DD:EE:FFexit -
Create
.envAUDIO_UID=1000AUDIO_GID=1000TZ=Europe/LondonWEB_PORT=8080BASE_LISTEN_PORT=8928 -
Create
docker-compose.ymlservices:sendspin-client:image: ghcr.io/trudenboy/sendspin-bt-bridge:latestcontainer_name: sendspin-clientrestart: unless-stoppednetwork_mode: hostvolumes:- /var/run/dbus:/var/run/dbus- /run/user/${AUDIO_UID:-1000}/pulse:/run/user/${AUDIO_UID:-1000}/pulse- /run/user/${AUDIO_UID:-1000}/pipewire-0:/run/user/${AUDIO_UID:-1000}/pipewire-0- /etc/docker/Sendspin:/configenvironment:- SENDSPIN_SERVER=auto- TZ=${TZ:-UTC}- WEB_PORT=${WEB_PORT:-8080}- BASE_LISTEN_PORT=${BASE_LISTEN_PORT:-8928}- CONFIG_DIR=/config- AUDIO_UID=${AUDIO_UID:-1000}- AUDIO_GID=${AUDIO_GID:-1000}- PULSE_SERVER=unix:/run/user/${AUDIO_UID:-1000}/pulse/native- XDG_RUNTIME_DIR=/run/user/${AUDIO_UID:-1000}devices:- /dev/bus/usb:/dev/bus/usbcap_add:- NET_ADMIN- NET_RAW -
Start the container
Terminal window mkdir -p /etc/docker/Sendspindocker compose up -d -
Open the web UI
http://<host-ip>:<WEB_PORT>
Port planning
Section titled “Port planning”WEB_PORTcontrols the direct web UI/API listener in Docker mode.BASE_LISTEN_PORTis the default Sendspin listener base for devices that do not definelisten_portexplicitly.- Each device without a manual port uses
BASE_LISTEN_PORT + device_index. - Advanced setups can assign a per-device
listen_portandlisten_hostin the web UI or/config/config.jsonafter the first start.
Example device block in /config/config.json:
{ "mac": "11:22:33:44:55:66", "player_name": "Kitchen Speaker", "listen_port": 8935, "listen_host": "192.168.1.50"}listen_host only changes the advertised host/IP shown for the player; it does not change the bind address inside the container.
Multiple bridge containers on one host
Section titled “Multiple bridge containers on one host”If you run more than one bridge container on the same machine:
- give each container a unique
WEB_PORT - give each container a unique
BASE_LISTEN_PORT - do not configure the same Bluetooth speaker in two running containers
Network and capabilities
Section titled “Network and capabilities”network_mode: host is required for:
- mDNS discovery when
SENDSPIN_SERVER=auto - access to the host Bluetooth stack through D-Bus
Required capabilities:
| Capability | Purpose |
|---|---|
NET_ADMIN | Bluetooth adapter control |
NET_RAW | Raw Bluetooth/HCI socket access |
On Ubuntu 24.04+ and other hosts with strict AppArmor/seccomp defaults, add security_opt to avoid Bluetooth permission errors:
security_opt: - apparmor:unconfined - seccomp:unconfinedThis is already included in the project’s docker-compose.yml. HAOS and other minimal OSes ignore these options safely.
Verify the container
Section titled “Verify the container”docker logs -f sendspin-clientcurl -s http://localhost:${WEB_PORT:-8080}/api/preflight | python3 -m json.toolRecent images also print startup diagnostics for:
- the init UID/GID inside the container
- the app UID/GID used for the running bridge process
- the selected audio socket path
- the socket owner/mode
- a live
pactl infoprobe result - whether the container had to wait for late D-Bus / Bluetooth / audio readiness on a cold host boot
- a warning when the running bridge process does not match the user-scoped host audio socket
If you have configured Bluetooth devices, recent images also wait briefly for late host startup dependencies before launching the bridge process. This reduces the common “first boot after host restart needs one extra container restart” race.
If your host comes up especially slowly, you can tune the wait with:
environment: - STARTUP_DEPENDENCY_WAIT_ATTEMPTS=60 - STARTUP_DEPENDENCY_WAIT_DELAY_SECONDS=1Troubleshooting user-scoped PipeWire / PulseAudio
Section titled “Troubleshooting user-scoped PipeWire / PulseAudio”A UID / session-owner mismatch for the user-scoped audio socket typically surfaces as “Connection refused” from pactl inside the container, but on some hosts it also shows up as the onboarding stalling on step 2 — “No Bluetooth controller detected” — because the same misalignment can sever host D-Bus access for Bluetooth. If both symptoms appear on a Raspberry Pi, see Preflight blocks on step 2 in the Raspberry Pi guide for the concrete fix sequence.
If the host audio stack is healthy but the container still shows Connection refused or cannot reach pactl, check:
docker exec sendspin-client ls -la /run/user/${AUDIO_UID:-1000}/pulse/docker exec sendspin-client env | grep -E 'PULSE|XDG'docker exec sendspin-client ps -o user:20,pid,command -C python3docker inspect sendspin-client --format '{{json .Mounts}}'And on the host:
idpactl infols -la /run/user/${AUDIO_UID:-1000}/pulse/If audio still fails, first confirm that the startup diagnostics show an App UID matching your host audio user. The old global Compose user: override is now only a temporary diagnostic test for older images:
services: sendspin-client: user: "${AUDIO_UID:-1000}:${AUDIO_UID:-1000}"Then restart the container and see whether pactl starts working. Treat that as a troubleshooting step first: it helps confirm a UID/session mismatch with the host audio socket.
Headless PipeWire: Bluetooth sinks not appearing after reboot
Section titled “Headless PipeWire: Bluetooth sinks not appearing after reboot”On PipeWire systems (Ubuntu 22.04+, Fedora, Raspberry Pi OS Bookworm), Bluetooth A2DP audio sinks are created by WirePlumber — a session manager that runs as a systemd --user service. By default, user services only start when the user logs in (GUI or SSH), and they may only be active when there’s an active interactive session.
Symptom: After a host reboot the bridge connects to the Bluetooth speaker (bluetoothctl shows Connected: yes), but pactl list sinks short shows no bluez_output.* or bluez_sink.* sink — only sendspin_fallback. Audio works again once you log in interactively.
There are two independent fixes — pick whichever fits your distro.
Option A — enable linger for the audio user so PipeWire + WirePlumber start at boot:
# Replace 1000 with your AUDIO_UID if differentsudo loginctl enable-linger $(id -un 1000)Verify it took effect:
loginctl show-user $(id -un 1000) -p Linger# Expected: Linger=yesAfter enabling linger, reboot and confirm that Bluetooth sinks appear without logging in:
# Should list bluez_output.* or bluez_sink.* entriespactl list sinks short | grep -i bluezOption B — disable WirePlumber’s with-logind if connections also drop on logout, or churn every ~10 s even with linger enabled. See WirePlumber with-logind endpoint churn below for the per-user override.
WirePlumber with-logind endpoint churn
Section titled “WirePlumber with-logind endpoint churn”Even after enabling linger, WirePlumber’s with-logind integration can cause two related Bluetooth symptoms on headless installations:
- Endpoint churn:
sudo journalctl -u bluetoothshows continuous endpoint registered / endpoint unregistered cycles every ~10 seconds, and BT connections succeed briefly then drop. - Logout-bound disconnects: observed on Ubuntu Server 24.04 — BT stays connected while an interactive user is logged in (e.g. via SSH), and drops the moment that session ends.
Both share the same root cause: on hosts without a graphical seat, WirePlumber’s logind integration continuously re-registers and unregisters A2DP media endpoints with BlueZ as sessions come and go.
Fix: Disable with-logind in WirePlumber:
mkdir -p ~/.config/wireplumber/bluetooth.lua.dcat > ~/.config/wireplumber/bluetooth.lua.d/51-disable-logind.lua << 'EOF'bluez_monitor.properties["with-logind"] = falseEOFsystemctl --user restart wireplumberVerify the churn stopped:
# Should show NO 'Endpoint unregistered' entriessudo journalctl -u bluetooth --since '30 sec ago' | grep -i unregisteredApplying configuration changes
Section titled “Applying configuration changes”Changes to devices, adapters, WEB_PORT, BASE_LISTEN_PORT, and Music Assistant connection settings require a container restart.