I’ve been using Fedora Silverblue as my daily driver for a while now. Over time, I’ve fully adopted Flatpak, which I really enjoy using alongside Flatseal. I particularly appreciate how Flatpak gives each application its own isolated directory structure, making backups incredibly straightforward.
However, I eventually hit a wall when it came to proprietary GPU drivers for workloads like media decoding, 3D rendering, and running local LLMs. In these scenarios, Flatpak’s runtime abstraction can sometimes become a liability.
While this can sometimes be solved by enabling the Flathub repository or installing custom packages, I wanted to avoid that route. After comparing various alternatives, I decided to write my own lightweight application launcher.
The Problem
Flatpak achieves portability by bundling apps inside standardized
runtime environments (like the Freedesktop or GNOME runtimes). While
this solves the “dependency hell” of the 2000s, it introduces a subtle
friction point for heavy graphics workloads. The host system runs a
specific kernel, Mesa build, or proprietary NVIDIA driver stack. Flatpak
attempts to bridge this by downloading matched runtime extensions (e.g.,
org.freedesktop.Platform.GL.default), but version drift,
Vulkan layer mismatches, and synchronization quirks still occur.
Where Existing Tools Fall Short
Before writing my own launcher, I surveyed the common options:
- Flathub: Installing GPU drivers directly from Flathub can theoretically enable GPU support for apps like Blender. However, I couldn’t find an easy way to prevent Flatpak from automatically pulling in other packages from the repo. Normally, enabling a repository implies trusting everything in it, which I wanted to avoid.
- Custom Flatpak Packages: Packaging arbitrary binary distributions into a custom Flatpak would be nice, allowing me to use standard Flatseal configurations and directory structures. Unfortunately, this requires packaging host-system libraries and keeping them constantly in sync—a non-trivial and highly annoying process.
- Firejail: Relies on setuid-root binaries (increasing the attack surface) and uses complex, brittle profile formats that are easy to misconfigure.
- Bubblejail / BubbleBox: These are great community efforts built on Bubblewrap. However, they either lean too heavily into desktop GUI managers or create fully isolated home directory trees rather than standard XDG state management. Furthermore, they lack the common, coarse-grained toggles (e.g., a simple switch for network or GPU access) that I wanted. That said, I did eventually borrow some design elements from them.
- Raw Shell Scripts: Writing simple
bwrapscripts is fast, and it’s actually how I started. But once you have more than a couple of applications, maintaining raw scripts quickly becomes unmanageable.
I needed a sweet spot: a single-file, zero-dependency launcher that
takes a simple app.toml file, manages persistent state
cleanly, and handles desktop integration.
High-Level Design
Vertically, I want to grant each application a minimal set of permissions and expose only the bare minimum of system information. Horizontally, I need a way to manage all these options effortlessly—recognizing common patterns while still accommodating the differences between individual applications.
One of Flatpak’s best ideas is isolating application data into dedicated subdirectories. I adopted this exact pattern:
- Every application lives under
~/applications/<app>/. - The launcher automatically provisions three isolated subdirectories:
config/,data/, andcache/. - Inside the sandbox,
$HOMEis mounted as an emptytmpfs, and standard environment variables (XDG_CONFIG_HOME,XDG_DATA_HOME,XDG_CACHE_HOME) are mapped directly to these provisioned directories. - The application can never see or accidentally pollute your real
$HOME.
Configuration
I wanted to eliminate boilerplate and repeated patterns wherever possible. The solution was to create a per-application configuration file that ultimately translates into Bubblewrap flags.
I decided to use TOML. I’d normally reach for JSON, but TOML is now supported in the Python standard library, and Bubblebox uses it to great effect. It just looks cleaner.
For each application, the app.toml looks something like
this:
[vars]
bin_dir = "~/opt/my-app"
[app]
executable = "${bin_dir}/binary"
args = ["--no-sandbox"]
[permissions]
gpu = true
audio = false
network = false
dbus = true
[[bind]]
src = "${bin_dir}"
[[bind]]
src = "/mnt/storage/assets"
dest = "/workspace/assets"
[dbus.rules]
"org.freedesktop.Notifications" = "none"
[desktop]
name = "App"
icon = "${bin_dir}/icon.svg"
categories = ["Graphics", "3DGraphics"]The goal is to provide coarse-grained switches with sensible defaults, while still allowing fine-grained control when necessary.
Extra Notes
- Installing a new application means manually downloading the binaries
and configuring the
app.toml. It also means I have to handle updates manually. It’s not a perfect system, but it’s a trade-off I’m willing to make for complete control. - The launcher script is written entirely in Python using only the
standard library. At its core, it simply translates the TOML
configuration into
bwrapflags and executes them. - Passing a special flag to the launcher will automatically generate
and install a
.desktopfile based on the configuration.
Comments