Ever wonder how to install your web browser in a container with everything working, e.g. hardware accelerations, usable PWAs shortcuts, keyring encryption, light/dark theme scheme that follows the host, auto-update, etc.? Basically, there's zero downside plus these many benefits:
This won't pull a ton of dependencies directly on your base system. It's always very hard to keep track of those residues, you know it! Your base system (host) would also be more stable, less prone to upgrade conflicts in the process.
A clear separation of config files. You can always start anew easily. You can delete the container along with everything inside it without affecting your system in any way.
Better performance if you're running on a distro that doesn't deliver x86-64-v3 microarchitecture optimization packages (99% of all distros out there).
This setup works on all distros, without exceptions!
Probably many more, depending on your use cases.
Table of contents
- Installing Distrobox and Podman
- Configure Distrobox to use Podman
- Create a container
- Enable x86-64-v3 packages
- Install required packages
- Install GPU driver
- Prepare directories for PWAs
- Symlink with the user directories on the host
- Install your browser
- Export the browser to the host
- Create a script to run the browser with hardware acceleration flags
- Modify the browser desktop file that we previously exported on the host
- Fixing the PWAs issue in the container
- Auto-update the container to which will also update the browser and everything inside with zero maintenance
- Check the browser's keyring entry
- Enhancing the sandbox in a distro that doesn't enable the
ptrace_scope=1
by default
1. Installing Distrobox and Podman
The command will differ based on your specific package manager. Refer to your distro's docs. For example, on Arch based distros:
sudo pacman -S distrobox podman
2. Configure Distrobox to use Podman
echo 'container_manager="podman"' > ~/.config/distrobox/distrobox.conf
3. Create a container
I use the official container image from CachyOS because it has x86-64-v3 repositories configured out of the box. Moreover, it doesn't have any issues with proprietary codecs, as all the codecs, free and proprietary ones, are available on the main Arch repositories.
It's also possible to use x86-64-v3 packages with openSUSE image (the Distrobox variant) if you install patterns-glibc-hwcaps-x86_64_v3
then sudo zypper dup
. The only reason to use openSUSE image rather than CachyOS image is when the browser you want to use doesn't have an official build for Arch based distros, e.g. Google Chrome, Microsoft Edge.
The problem with openSUSE image is also depended on your GPU. If you're using Intel or NVIDIA GPU, it doesn't matter, as Intel relies on intel-media-driver
, and NVIDIA relies on libva-nvidia-driver
for VA-API.
Unfortunately, if you're with AMD, and for some reason, hurt yourself by not using Brave ๐, you will have to add PackMan repo in openSUSE image. The problem happens when you upgrade the container, it will be conflicted with the main repo from time to time, except now it will happen in your container instead of it happening directly on your system. It's in a way better, but not ideal.
Here's a summary of how to choose which container image to use:
Intel and NVIDIA users: Choose based on whether your browser has the official build for the container image, e.g. Brave users use CachyOS image, Google Chrome and Microsoft Edge users use openSUSE image.
AMD users: Use CachyOS image + Brave, and call it a day. Otherwise, use openSUSE image + PackMan repo (more on that later).
Now, it's time to actually create a container ๐
CachyOS Image
distrobox-create -i docker.io/cachyos/cachyos-v3:latest -n browser-v3-dbx -H ~/distrobox/browser-v3-dbx --volume /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
openSUSE Image
distrobox-create -i registry.opensuse.org/opensuse/distrobox:latest -n browser-v3-dbx -H ~/distrobox/browser-v3-dbx --volume /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
Then, enter the container with distrobox enter browser-v3-dbx
and follow the next step.
Note: For NVIDIA users, in order to use the GPU inside the container, you need to add --nvidia
flags to the distrobox-create
command. See more here.
4. Enable x86-64-v3 packages
Depends on the container image you choose...
CachyOS Image
Update all the packages:
sudo pacman -Syu
Reinstall all the packages from CachyOS repos (this will replace x86-64 AKA x86-64-v1 packages with x86-64-v3 ones):
sudo pacman -Qqn | sudo pacman -S -
openSUSE Image
Update all the packages:
sudo zypper dup
Enable x86-64-v3 packages:
sudo zypper install patterns-glibc-hwcaps-x86_64_v3
5. Install required packages
CachyOS Image
sudo pacman -S dbus xdg-utils glib2 pipewire adwaita-icon-theme adwaita-cursors adwaita-fonts watchexec
openSUSE Image
sudo zypper install dbus-1 dbus-1-x11 xdg-utils glib2 pipewire adwaita-icon-theme adwaita-fonts watchexec
6. Install GPU driver
pacman
command for CachyOS Image, zypper
for openSUSE Image.
Intel
sudo pacman -S intel-media-driver libva-utils
sudo zypper install intel-media-driver libva-utils
AMD
sudo pacman -S Mesa libva libva-utils
For openSUSE image, adding PackMan repo first:
sudo zypper ar -f https://ftp.gwdg.de/pub/linux/misc/packman/suse/openSUSE_Tumbleweed/ packman
Refresh the repos:
sudo zypper refresh && sudo zypper dup
Install the driver:
sudo zypper install --from packman Mesa-libva libva2 libva-utils
NVIDIA
sudo pacman -S libva-nvidia-driver libva-utils
sudo zypper install libva-nvidia-driver libva-utils
7. Prepare directories for PWAs
mkdir -p ~/.local/share/applications
mkdir -p ~/Desktop
8. Symlink with the user directories on the host
ln -s /var/home/archerallstars/.local/share/icons/hicolor ~/.local/share/icons/
ln -s /var/home/archerallstars/.local/share/fonts ~/.local/share/
Note: Replace /var/home/archerallstars
with your home directory's absolute path on your system (the path you can copy from your file manager, the path without ~
).
(optional) Symlink with fontconfig directory on the host
This is usually necessary for multilingual individuals in order to change the fonts for specific languages.
ln -s /var/home/archerallstars/.config/fontconfig/conf.d ~/.config/fontconfig/
9. Install your browser
Please refer to your browser installation instruction. It's the same regardless of the containerization. For example, Brave in CachyOS container:
sudo pacman -S yay
yay -Sy brave-bin
10. Export the browser to the host
For example, for Brave:
distrobox-export - a brave
Note: The actual name of the browser could be different. For example, if you're using CachyOS image, Brave uses brave for its binary, while it's brave-browser
when installing from the official openSUSE repo ๐
Try catching this with ls /usr/bin | grep brave
.
After the export, you can exit from the container and forget it with exit
.
11. Create a script to run the browser with hardware acceleration flags
Unfortunately, this is still necessary in 2025. So, create a file called brave-vaapi
or something, put it in ~/.local/bin
on your host. Here are the scripts depending on your GPU vendor:
Intel
#!/bin/bash
exec /usr/bin/distrobox-enter -n browser-v3-dbx -- /usr/bin/brave --enable-features=AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoEncoder "$@"
AMD
#!/bin/bash
exec /usr/bin/distrobox-enter -n browser-v3-dbx -- /usr/bin/brave --enable-features=AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoEncoder,VaapiIgnoreDriverChecks "$@"
NVIDIA
#!/bin/bash
exec /usr/bin/distrobox-enter -n browser-v3-dbx -- /usr/bin/brave --enable-features=AcceleratedVideoDecodeLinuxZeroCopyGL,AcceleratedVideoDecodeLinuxGL,AcceleratedVideoEncoder,VaapiIgnoreDriverChecks,VaapiOnNvidiaGPUs "$@"
Save the file, right-click > properties and make it executable/run as program.
Note: The reason I use the script to add these flags because these flags could change at any time. Instead of changing them in all the desktop files, changing here would take a much lesser effort ๐
12. Modify the browser desktop file that we previously exported on the host
It's in ~/.local/share/applications/
. The desktop file could be browser-v3-dbx-brave-browser.desktop
.
Open it with your text editor, then search for exec
, replace its value to your script from the previous step, like this:
Exec=/var/home/archerallstars/.local/bin/brave-vaapi %U
Search for all lines that starts with Exec=
, replace them with your script, but keep its flags intact (if there's any), like:
Exec=/var/home/archerallstars/.local/bin/brave-vaapi --incognito
Note: Remember to use the absolute path!
13. Fixing the PWAs issue in the container
There's one serious issue that would prevent us from the seamless integration, though. That's the broken PWAs icons, as the icons created in the container would refer to their executable path in the container, of which doesn't exist on the host side.
Moreover, it also has the same issue with every Chromium browser on Flathub when running in native Wayland mode.
However, you can both of the issues easily with a simple bash script, called it brave-pwa-fix
and save it in ~/.local/bin
on your host.
#!/bin/bash
yourBrowser="brave"
browserDesktopFilename="browser-v3-dbx-brave-browser.desktop"
binPath="/var/home/archerallstars/.local/bin/brave-with-video-accelerated"
hostAppsPath="/var/home/archerallstars/.local/share/applications"
containerAppsPath="/var/home/archerallstars/distrobox/browser-v3-dbx/Desktop"
process_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
return
fi
local temp_file
temp_file=$(mktemp)
local changed=0
local icon_value=""
# First pass to get icon_value
while IFS= read -r line; do
if [[ $line =~ ^Icon= ]]; then
icon_value="${line#Icon=}"
fi
done <"$file"
# Second pass to process
while IFS= read -r line; do
if [[ $line =~ ^Name= ]]; then
if [[ $line == *" ("* ]]; then
line="${line%% (*}"
changed=1
fi
elif [[ $line =~ ^Exec= ]]; then
local exec_value="${line#Exec=}"
if [[ "$exec_value" == *" --"* ]]; then
local suffix="${exec_value#* --}"
line="Exec=$binPath --$suffix"
else
line="Exec=$binPath"
fi
changed=1
elif [[ $line =~ ^StartupWMClass= ]] && [[ -n "$icon_value" ]]; then
current_value="${line#StartupWMClass=}"
if [[ "$current_value" != "$icon_value" ]]; then
line="StartupWMClass=$icon_value"
changed=1
fi
fi
echo "$line" >>"$temp_file"
done <"$file"
if [[ $changed -eq 1 ]]; then
cp "$temp_file" "$hostAppsPath/$(basename "$file")"
fi
rm "$temp_file"
}
# Process missing files from container
find "$containerAppsPath" -name "*$yourBrowser*.desktop" 2>/dev/null | while read -r file; do
basename=$(basename "$file")
host_file="$hostAppsPath/$basename"
if [[ ! -f "$host_file" ]]; then
process_file "$file"
fi
done
# Synchronize: remove orphaned files from hostAppsPath
find $hostAppsPath -name "*$yourBrowser*.desktop" 2>/dev/null | while read -r host_file; do
basename=$(basename "$host_file")
if [[ "$basename" == "$browserDesktopFilename" ]]; then
continue
fi
container_file="$containerAppsPath/$basename"
if [[ ! -f "$container_file" ]]; then
rm -f "$host_file"
fi
done
Save the file, right-click > properties and make it executable/run as program.
Explaination
You only need to change these variables values at the top of the script to match the one on your system:
yourBrowser
andbrowserDesktopFilename
should be obvious enough ๐binPath
is the path of the script you use to launch the browser with hardware accelerated flags above.hostAppsPath
is the path of the user desktop files on the host.containerAppsPath
is one of the paths of the user desktop files in the container where PWAs desktop files reside.
Using watchexec
to sync PWAs from inside the container to the host and vice versa
This script already handled everything. We just need a watcher that we already installed in step #5, watchexec
. We'll run this small yet powerful tool using a systemd service, in which we'll autostart it after user login (with some delay - container safe).
I will go quickly with nano
. By now, you should be familar with what we're doing now ๐
nano ~/.config/systemd/user/pwa-watcher.service
Inside the service file:
[Unit]
Description=Start PWA watcher in the background for the containerized browser
RequiresMountsFor=/run/user/1000/containers
[Service]
Type=exec
ExecStart=/usr/bin/distrobox-enter -n browser-v3-dbx -- bash -c 'watchexec -n --watch ~/Desktop distrobox-host-exec /var/home/archerallstars/.local/bin/brave-pwa-fix'
Note: Replace my host's absolute path with your host's absolute path.
nano ~/.config/systemd/user/pwa-watcher.timer
Inside the timer file:
[Unit]
Description=Start PWA watcher service with some delay.
[Timer]
OnStartupSec=23
RandomizedDelaySec=12
Persistent=true
[Install]
WantedBy=timers.target
Reload and enable the timer, also start the service if you will:
systemctl --user daemon-reload
systemctl --user enable pwa-watcher.timer
systemctl --user start pwa-watcher
14. Auto-update the container to which will also update the browser and everything inside with zero maintenance
We will use a systemd service and its timer agian. Not only that it will update the container that we created, if you have more fun with more containers, all of them will also be updated in the background.
There's a caveat, though. A misconfigured container would stop the update process of all containers after it, so make sure you're doing it right.
nano ~/.config/systemd/user/dbx-upgrade.service
Inside the service file:
[Unit]
Description=Upgrade all Distrobox containers
RequiresMountsFor=/run/user/1000/containers
StartLimitBurst=3
StartLimitIntervalSec=600
[Service]
Type=exec
ExecStart=/usr/bin/distrobox-upgrade --all
Restart=on-failure
RestartSec=60
nano ~/.config/systemd/user/dbx-upgrade.timer
Inside the timer file:
[Unit]
Description=Start Distrobox containers upgrade service with some delay.
[Timer]
OnStartupSec=30
RandomizedDelaySec=15
Persistent=true
[Install]
WantedBy=timers.target
Reload and enable the timer:
systemctl --user daemon-reload && systemctl --user enable dbx-upgrade.timer
15. Check the browser's keyring entry
After you turning on the sync feature in your browser, it's super important to check whether your setup has created the keyring's entries correctly to ensure you use the uncrompromised encrytion to store your data.
You can check this easily with GNOME Passwords and Keys. It looks dated than Key Rack, but the latter doesn't always find all the entries.
If you did everything right, you will see an entry like this:
Is it just easier to go with the Flatpak version if you're using Brave?
Yes, it's. But it has a questionable security implication, thus not recommended by Brave. When it comes to web browser, I want maximum security. Less is not more here, it's unacceptable!
You can check this with the ://sandbox
page, for example, in Brave: brave://sandbox
.
When running inside the container
Simply the same with running the browser natively on your system. Meaning your browser sandbox is not compromised when it's running inside the container.
When running in Flatpak
As you can see, the Layer 1 Sandbox is now SUID, and this is without Ptrace Protection with Yama LSM (Non-broker) enabled, of which is available as an alternative mode when namespaces aren't available, see more here and here
16. Enhancing the sandbox in a distro that doesn't enable the ptrace_scope=1
by default
For any users that doesn't debug anything on their PC, enable this security feature shouldn't break anything. It's enabled by default in Ubuntu, Arch, and openSUSE (by my request ๐).
Currently, I'm on Fedora Silverblue 42, of which doesn't enable ptrace_scope=1
by default. There's a proposal to enable this by default in Fedora 44, though.
You can check your current ptrace_scope
value with:
cat /proc/sys/kernel/yama/ptrace_scope
However, in the meantime, you can enable this easily by copying the default config file and edit it:
sudo cp /usr/lib/sysctl.d/10-default-yama-scope.conf /etc/sysctl.d/
sudo nano /etc/sysctl.d/10-default-yama-scope.conf
The last line should be kernel.yama.ptrace_scope = 1
instead of kernel.yama.ptrace_scope = 0
.
Then, reboot. cat /proc/sys/kernel/yama/ptrace_scope
again, now, it should return 1
. And when you open brave://sandbox/
again, there should be no red entries left (meaning a secure sandbox):
I hope I don't miss anything. Thanks for reading ๐
Cover Image by Aron Yigin on Unsplash
Top comments (0)