Audio (via Pulseaudio) inside Container

Usecase:
If you want to hear audio from applications running inside your container.

Note: This guide can be used for Incus and LXD.

You can use the host’s Pulseserver or Pipewire-Pulse.

We have two methods for this:
a. Use the Pulseaudio socket directly
b. Use the Pulseaudio network module


Method a - Pulseaudio socket:

  1. Add the Pulsesocket as unix-proxy device to your container (see also: devices documentation):
    Notes:

    • The below example assumes that both (main) users (on host and inside container) have the uid and gid 1000.
      You can check this with id username.
    • Adjust the paths and uids/gids according to the distributions you use.
    • Change /home/user to the chosen username inside the container (e.g. /home/ubuntu)

    Config:

     PulseSocket1:
         bind: container # or instance
         connect: unix:/run/username/1000/pulse/native
         listen: unix:/home/username/pulse-socket
         security.gid: "1000"
         security.uid: "1000"
         uid: "1000"
         gid: "1000"
         mode: "0777"
         type: proxy
    
  2. Add Pulseserver environment variable:
    Either:
    a) As @stgraber suggested below, you can apply environment.PULSE_SERVER: unix:/home/username/pulse-socket to your container configuration or profile (under config:).
    or:
    b) add the following line to .bashrc (inside the home folder of your main user inside the container):

    export PULSE_SERVER=unix:/home/username/pulse-socket

  3. See General Steps below.

Now reload the .bashrc file with source .bashrc or restart the container and you should be able to start applications and hear their sound.


Method b - Pulseaudio Network module:

  1. Add this line to etc/pulse/default.pa or ~/.pulse/default.pa (or similar, look at your distributions documentation):
    This will allow access over the network, but only from the IP 127.0.0.1 (which is only your computer).
    As an Alternative you can also use cookie authentication, see: Network Setup – PulseAudio

    load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1
    

    b) (Alternative) In case you use pipewire-pulse

    Add a .conf file (e.g. pulse-tcp.conf) to the config folder of pipewire (e.g. ~/.config/pipewire/pipewire-pulse.conf.d) and add the following lines:

    context.exec = [
     { path = "pactl"  args = "load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1" }
    ]
    
  2. Add a tcp-proxy device to your container:

     devices:
       Pulseovernetwork:
         bind: container #or instance
         connect: tcp:127.0.0.1:4713
         listen: tcp:127.0.0.1:4713
         type: proxy
    
  3. Add Pulseserver environment variable:
    a) As @stgraber suggested below, you can apply environment.PULSE_SERVER: tcp:127.0.0.1:4713 to your container configuration or profile (under config:).

    b) Or add the following line to .bashrc (inside the home folder of your main user inside the container):
    export PULSE_SERVER=tcp:127.0.0.1:4713

  4. You might need to configure your firewall (on host) to allow access for tcp port 4713.

  5. See General Steps below.

Now reload the .bashrc with source .bashrc or restart the container and you should be able to start applications and hear their sound.


General Steps (for all methods):

  • Install the following audio software inside your container:

    • pulseaudio or pipewire-pulse (in case you use pipewire)
    • pulseaudio-alsa (this is sometimes (e.g. in Debian) included in alsa-plugins (deb-package: libasound2-plugins))
    • 32bit versions of pulseaudio and alsa for compatibility with 32bit applications.

    The software packages might have different names in your distributions, look at their documentation and wikis for help or search in the software repos.

  • Control loudness etc.:
    You should be able to control the loudness of applications in your host’s pulseaudio control interface.


Todo:

  • Security: What are implications on host and container seperation?
    Can pulseaudio clients listen to other clients and can this be prevented?
  • Other methods: Are there other methods?

Credits:
Thx to simos for describing method a in his blog (with focus on Ubuntu though):

3 Likes

If you mostly care about lxc exec, you can set env variables through the environment.PULSE_SERVER config key. Those will be applied to all lxc exec sessions automatically.

Ok :+1: , I added this option to the text.

While following method A, In the syslog file, i’m getting a

apparmor=“DENIED” operation=“connect” profile=“lxd_forproxy-PulseSocket_xxx </var/snap/lxd/common/lxd>”

Can the apparmor profile for lxd be tweaked to allow this to work? And where are the lxd apparmor profiles?

You can manually put extra entries in the profile using raw.apparmor in the LXD config.

Is there more context on that DENIED log entry? Normally I’d expect apparmor to complain about exact paths rather than just connect and a profile name.

Yes i truncated it. it is

apparmor=“DENIED” operation=“connect” profile=“lxd_forproxy-PulseSocket_xxx </var/snap/lxd/common/lxd>” name=“/run/user/3000/pulse/native” comm=“lxd” requested_mask=“wr” denied_mask=“wr” >

One way to add requirements to an apparmor profile is to run aa-genprof or aa-logprof, but in this case aren’t lxd profiles generated on the fly?

Also comm lxd forkproxy looks like roughly like this

/snap/lxd/current/bin/lxd forkproxy … unix:/home/xxx/.pulse-native … unix:/var/lib/snapd/hostfs/var/run/user/3000/pulse/native …

I have another setup with a pulseaudio socket in /tmp and that works fine.

Looks like it could be because of this

OK managed to solve this by doing this

connect: unix:/run/user/<uid>/pulse/native

instead of

connect: unix:/var/run/user/<uid>/pulse/native

Ah yeah, apparmor doesn’t like symlinks :slight_smile:
(Well, they all get deferred and the target is considered so that leas to those kind of issues)

Thanks for the guide.
Sorry for the necrobump …
I struggled with this, and also added binding for pipewire itself, so I wrote a note to myself … and others:

2 Likes

Thanks for cracking the PipeWire puzzle.

For the rest of the setup, if you need GUI apps, see Incus / LXD profile for GUI apps: Wayland, X11 and Pulseaudio In this case, the PulseAudio socket is mounted as a disk device instead of a proxy device.

Is it better to use disk devices or proxy devices? It shouldn’t make a difference as long as both are reliable.

1 Like

It does work with “GUI apps”.
I needed it for running google-chrome in a container.
(Don’t want google backdooring my host).
The disk-approach seems rather complicated - and x11 + pulseaudio works fine for me.

I use X11 with this profile:
(In another container on Fedora 38)

# Profile x11:
config:
  environment.DISPLAY: :0
description: GUI X11 profile
devices:
  X0:
    bind: container
    connect: unix:@/tmp/.X11-unix/X0
    listen: unix:@/tmp/.X11-unix/X0
    security.gid: "1000"
    security.uid: "1000"
    type: proxy
  mygpu:
    type: gpu
name: x11

It might be important how you enter the container.
I use something like this approach in a script:

_incus_do exec "${container_name}" -- sudo \
    --user "${user_name}" --login "${_command}"

I did notice the guide on wayland in your link.
Maybe I should try that adventure … :wink:

When I experimented with Wayland, one thing I noticed is that adding environment.WAYLAND_DISPLAY: <path> to a profile didn’t work. You may need to add wayland socket path to your container’s user .profile file:
export WAYLAND_DISPLAY=<path>

And thanks for pipewire socket stuff, I’ll add this to my gui profile.

1 Like

There are several ways to get a shell into an Incus container.
When I investigated those different ways some time ago, I noticed that in some cases the environment variables that are set by Incus, are being cleared (not carried on) by the login scripts of the container.
There’s no single way to get a shell into an Incus instance, and this is why the results vary.

1 Like

For Ubuntu containers I use:

incus exec <container_name> -- sudo --login --user ubuntu

When I set environment.DISPLAY: in a profile, it’s being propagated perfectly well, but for some reason environment.WAYLAND_DISPLAY: is not. I never figured out why certain environment variables are ignored.

1 Like

Hi @toby63,

How to setup the config you’ve shown?

 PulseSocket1:
     bind: container # or instance
     connect: unix:/run/username/1000/pulse/native
     listen: unix:/home/username/pulse-socket
     security.gid: "1000"
     security.uid: "1000"
     uid: "1000"
     gid: "1000"
     mode: "0777"
     type: proxy

Is it a file that needs to be created?
Commands to be issued?

This is part of a profile.

Command on the other hand would look like this:
incus config device add <instance_name> PulseSocket1 proxy bind=instance listen=unix:/home/username/pulse-socket connect=unix:/run/username/1000/pulse/native security.uid=1000 security.gid=1000 uid=1000 gid=1000 mode=0777

You can see how I enable pipewire and pulseaudio inside containers here.

In this case, just have pulseaudio, no pipewire.
I adjusted the provided line to my use case.
Before, when trying to play a mp3 file with mpv, the file would play, but without sound.
Pulseaudio would only show a dummy sound device.

After giving the command you suggested (adapted), there is this error:
incus config device add brother PulseSocket1 proxy bind=instance listen=unix:/home/MYUSER/pulse-socket connect=unix:/run/user/1000/pulse/native security.uid=1000 security.gid=1000 uid=1000 gid=1000 mode=0777

So, listen is for the container and connect to the host system?

Also added this line to ~/.bashrc:

export PULSE_SERVER=unix:/home/MYUSERINSIDECONTAINER/pulse-socket

File exists.

$ mpv Death\ Grips\ -\ Lost\ Boys.mp3 
[ffmpeg/demuxer] mp3: Estimating duration from bitrate, this may be inaccurate                                                
 (+) Audio --aid=1 (mp3 2ch 44100Hz)                           
File tags:
 Artist: Death Grips
 Album: The Money Store
 Title: Lost Boys
[W][19148.495002] pw.conf      | [          conf.c:  939 try_load_conf()] can't load config client.conf: No such file or directory                                                           
[E][19148.495013] pw.conf      | [          conf.c:  963 pw_conf_load_conf_for_context()] can't load default config client.conf: No such file or directory                                   
[ao/pulse] Init failed: Access denied
ALSA lib pulse.c:242:(pulse_connect) PulseAudio: Unable to connect: Access denied

[ao/alsa] Playback open error: Connection refused
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
[ao/jack] cannot open server
Expected 1 memfd fd to be received over pipe; got 0            
Did we reach our open file descriptors limit?
Expected 1 memfd fd to be received over pipe; got 0
Did we reach our open file descriptors limit?
ALSA lib pulse.c:242:(pulse_connect) PulseAudio: Unable to connect: Protocol error

[ao/sdl] could not open audio: ALSA: Couldn't open audio device: Connection refused                                           
[ao] Failed to initialize audio driver 'sdl'                   
Could not open/initialize audio device -> no sound.            
Audio: no audio                                                

Exiting... (Errors when loading file)

Try installing pulseaudio-utils and dbus-user-session inside container:

sudo apt install pulseaudio-utils dbus-user-session

As for the command:

  • bind is on the instance side, so listen is for the container side and connect for the host side
  • security.uid and security.gid is set to your user’s uid/gid on the host
  • uid and gid is set to your user’s uid/gid inside the container

If sound still doesn’t work, see Troubleshooting section, where you can find information about pulseaudio cookie and how to copy it to the container.

1 Like

It’s an access denied error, which means that the access to the resource has been denied. Bad cookie, permissions, etc.