This guide explains how to play audio and send desktop notifications from a Debian- or Ubuntu-based container to a native Linux desktop host.
The setup shares two UNIX sockets from the host desktop session:
- The PulseAudio socket, or PipeWire’s PulseAudio-compatible socket, for audio playback
- The session D-Bus socket for desktop notifications
A Python verification script checks the socket mounts, sends a test notification, generates a short WAV tone, and plays it through the host audio server.
Warning
Sharing the host session D-Bus and audio sockets gives the container access to parts of your desktop session. Use this configuration only with trusted images and trusted code.
With Docker, the container process should run under the same UID and GID as the host desktop user who owns the runtime sockets.
Export the required values before starting the container:
export HOST_UID="$(id -u)"
export HOST_GID="$(id -g)"
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"You can verify them with:
printf 'UID=%s\nGID=%s\nXDG_RUNTIME_DIR=%s\n' \
"$HOST_UID" \
"$HOST_GID" \
"$XDG_RUNTIME_DIR"Create the following compose.yml:
services:
audio-dev-container:
build: .
user: "${HOST_UID}:${HOST_GID}"
environment:
XDG_RUNTIME_DIR: /run/host-desktop
PULSE_SERVER: unix:/run/host-desktop/pulse/native
DBUS_SESSION_BUS_ADDRESS: unix:path=/run/host-desktop/bus
DISPLAY: "${DISPLAY:-}"
WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}"
volumes:
- "${XDG_RUNTIME_DIR}/pulse/native:/run/host-desktop/pulse/native"
- "${XDG_RUNTIME_DIR}/bus:/run/host-desktop/bus"The host sockets are mounted under /run/host-desktop rather than replacing the container’s own /run/user directory.
The verification script uses DISPLAY or WAYLAND_DISPLAY as a check that it was launched from a graphical desktop session. notify-send itself communicates through the mounted D-Bus session socket, so this setup does not need to mount the X11 or Wayland display socket.
Start the service with:
docker compose up --build -dRootless Podman supports keep-id user namespaces. This maps the calling host user’s UID and GID to the same numeric UID and GID inside the container.
As a result, this setup does not need HOST_UID, HOST_GID, or an explicit user: entry:
services:
audio-dev-container:
build: .
userns_mode: keep-id
environment:
XDG_RUNTIME_DIR: /run/host-desktop
PULSE_SERVER: unix:/run/host-desktop/pulse/native
DBUS_SESSION_BUS_ADDRESS: unix:path=/run/host-desktop/bus
DISPLAY: "${DISPLAY:-}"
WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}"
volumes:
- "${XDG_RUNTIME_DIR}/pulse/native:/run/host-desktop/pulse/native"
- "${XDG_RUNTIME_DIR}/bus:/run/host-desktop/bus"Make sure the host runtime directory is available:
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"Then start the service:
podman compose up --build -dThe equivalent option when invoking Podman directly is:
podman run --userns=keep-id ...keep-id is specific to rootless Podman. Docker supports other forms of user-namespace isolation and remapping, but its default configuration does not provide a direct equivalent that automatically preserves the calling user’s numeric UID and GID. For the Docker example in Section 1, explicitly setting user: "${HOST_UID}:${HOST_GID}" remains the simplest approach.
Note
Compose support for Podman-specific options can depend on the Compose provider and version being used. If userns_mode: keep-id is rejected, run the container directly with podman run --userns=keep-id or check the documentation for your installed Compose provider.
Add the following packages to a Debian- or Ubuntu-based Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libnotify-bin \
pulseaudio-utils \
python3-minimal \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY check_integration.py /app/check_integration.py
RUN chmod +x /app/check_integration.py
CMD ["sleep", "infinity"]The installed packages provide:
libnotify-bin: thenotify-sendcommandpulseaudio-utils: thepaplaycommandpython3-minimal: the Python interpreter used by the verification script
On a PipeWire-based desktop, paplay normally connects through PipeWire’s PulseAudio-compatible socket.
Save the following as check_integration.py.
Run the script inside the active service:
docker compose exec audio-dev-container \
python3 /app/check_integration.pyRun:
podman compose exec audio-dev-container \
python3 /app/check_integration.pyA successful run should produce output similar to:
Sent desktop notification.
Played test sound through PulseAudio/PipeWire.
🎉 Container notification and sound checks passed successfully!
You should also see a desktop notification and hear a short 880 Hz tone.
The script confirms that the socket paths exist and that notify-send and paplay exit successfully. It cannot automatically confirm that the notification was visible to the user or that the sound was audible, so verify both manually.
On the host, the relevant paths normally look like this:
${XDG_RUNTIME_DIR}/bus
${XDG_RUNTIME_DIR}/pulse/native
For a host user with UID 1000, these are commonly:
/run/user/1000/bus
/run/user/1000/pulse/native
Inside the container, they are mounted as:
/run/host-desktop/bus
/run/host-desktop/pulse/native
The container environment is then configured as follows:
XDG_RUNTIME_DIR=/run/host-desktop
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/host-desktop/bus
PULSE_SERVER=unix:/run/host-desktop/pulse/native
The Python script derives both socket paths from XDG_RUNTIME_DIR, so these values must remain consistent.
Check the environment inside the container:
docker compose exec audio-dev-container \
printenv XDG_RUNTIME_DIRThe expected value is:
/run/host-desktop
For Podman, replace docker compose with podman compose.
Check the host socket:
ls -l "${XDG_RUNTIME_DIR}/bus"Check the mounted socket inside the container:
docker compose exec audio-dev-container \
ls -l /run/host-desktop/busThe D-Bus session socket normally exists only for an active user session. A container started by a system service, cron job, SSH-only session, or another user may not have access to the graphical desktop session.
Check the host path:
ls -l "${XDG_RUNTIME_DIR}/pulse/native"On a PipeWire system, inspect the user services:
systemctl --user status pipewire pipewire-pulseOn a PulseAudio system, run:
pactl infoCheck the mounted socket inside the container:
docker compose exec audio-dev-container \
ls -l /run/host-desktop/pulse/nativeCheck the values on the host:
printf 'DISPLAY=%s\nWAYLAND_DISPLAY=%s\n' \
"${DISPLAY:-}" \
"${WAYLAND_DISPLAY:-}"At least one must be non-empty because the verification script uses these variables to confirm that it was launched from a graphical session.
Check the values inside the container:
docker compose exec audio-dev-container \
env | grep -E '^(DISPLAY|WAYLAND_DISPLAY)='These variables are used only as a session check by the script. This guide does not mount the X11 or Wayland display socket because the container is not running a graphical application.
Verify the D-Bus address:
docker compose exec audio-dev-container \
printenv DBUS_SESSION_BUS_ADDRESSThe expected value is:
unix:path=/run/host-desktop/bus
Try the command manually:
docker compose exec audio-dev-container \
notify-send \
"Container Test" \
"Manual notification test"A notification may still be hidden when:
- Do Not Disturb mode is enabled
- Notifications are disabled by the desktop environment
- The desktop session is locked
- No notification daemon is running
- The notification daemon rejects the request
Verify the configured server:
docker compose exec audio-dev-container \
printenv PULSE_SERVERThe expected value is:
unix:/run/host-desktop/pulse/native
Inspect the connection:
docker compose exec audio-dev-container \
pactl infoConfirm that the socket files belong to the same user used by the container:
ls -ln \
"${XDG_RUNTIME_DIR}/bus" \
"${XDG_RUNTIME_DIR}/pulse/native"For Docker, inspect the effective container user:
docker compose exec audio-dev-container idIts UID and GID should match HOST_UID and HOST_GID.
For rootless Podman with keep-id, inspect the mapping:
podman compose exec audio-dev-container idThe UID and GID should match those of the user who started Podman.
On SELinux-enabled systems, the container may be prevented from accessing the mounted UNIX sockets even when the UID and GID are correct.
As a troubleshooting measure, disable SELinux labeling for this container:
services:
audio-dev-container:
security_opt:
- label=disableA complete rootless Podman example is:
services:
audio-dev-container:
build: .
userns_mode: keep-id
security_opt:
- label=disable
environment:
XDG_RUNTIME_DIR: /run/host-desktop
PULSE_SERVER: unix:/run/host-desktop/pulse/native
DBUS_SESSION_BUS_ADDRESS: unix:path=/run/host-desktop/bus
DISPLAY: "${DISPLAY:-}"
WAYLAND_DISPLAY: "${WAYLAND_DISPLAY:-}"
volumes:
- "${XDG_RUNTIME_DIR}/pulse/native:/run/host-desktop/pulse/native"
- "${XDG_RUNTIME_DIR}/bus:/run/host-desktop/bus"Caution
label=disable weakens SELinux isolation for the container. Use it only with trusted workloads and only when the default configuration is blocked by SELinux.
Avoid applying :z or :Z labels to desktop session sockets without understanding the consequences. Relabeling files under the host runtime directory may interfere with the desktop session or other applications.
The session D-Bus socket is not limited to notifications. Depending on the desktop environment and available services, a process with access to the bus may be able to interact with other applications and desktop components.
The audio socket may also expose more than playback. Depending on the server configuration and permissions, clients may be able to enumerate audio devices, inspect streams, or access recording sources.
Recommended precautions include:
- Use only trusted container images and applications.
- Do not expose these sockets to containers that process untrusted code.
- Remove the socket mounts when audio and notifications are not needed.
- Avoid running the container with unnecessary Linux capabilities.
- Keep the rest of the container filesystem and host mounts restricted.
- Consider a filtered D-Bus proxy when only notification access is required.
- Consider a dedicated or restricted audio endpoint for stronger isolation.
This UNIX-socket configuration is intended for native Linux desktop sessions.
WSL2 with WSLg may provide a PulseAudio-compatible endpoint, but its paths and environment variables differ from those of a native Linux desktop.
Inspect the existing value inside WSL:
printf '%s\n' "$PULSE_SERVER"Do not assume that /run/user/<UID>/pulse/native exists or that the Linux Compose example works unchanged.
Desktop notification forwarding may also require a WSL-specific integration mechanism.
Docker Desktop and Podman on macOS run Linux containers inside a virtual machine. Podman’s documentation likewise describes its macOS environment as using a Podman-managed VM.
macOS does not provide a native Linux PulseAudio socket or Linux session D-Bus socket to those containers.
A setting such as:
PULSE_SERVER=tcp:host.docker.internal:4713
works only when a compatible host-side audio server has been installed and explicitly configured to accept TCP connections.
Desktop notifications require a separate host-side bridge or integration service.
AGPL v3, shirayu