A Decade of Linux Setups — Why I Stopped Looking for ‘The One’
How I Landed on Fedora Workstation
For many years, I used a Debian-based distribution as my primary operating system. I started with Debian itself, and later switched to Ubuntu for convenience. I had a good experience with both, but the more I learnt about the Linux ecosystem, the more I felt I did not need the extra layers Ubuntu puts on top of Debian. I started exploring a variety of popular and expert-level distributions. I wanted a more “pure” Linux experience — closer to the upstream projects and communities. Soon, I realised that the Debian package ecosystem is far wider than the Red Hat ecosystem, and switching to a Red Hat-based distribution such as Fedora or CentOS was difficult because some of the software I used daily was not published as .rpm packages.
Considering simplicity, full control over my system, and the breadth of available packages, Arch Linux eventually made its way onto my list. My first installation attempt was unpleasant, but going through the installation process — feeling that fingerprint of control over every part of the OS — made me warm to it quickly. I could find almost anything in the AUR, spanning from packages available in other distributions to wrapped legacy packages whose development had been discontinued and removed from mainstream repositories. I had a lot of fun, even though I nearly broke my system more than once. But the complexity of installation and personalisation, and the differences between my desktop and server environments, eventually made Arch difficult to sustain. Along the way I tried some Arch-based distributions — Manjaro, EndeavourOS, and BlackArch as an alternative to Kali. All of them were pleasant experiences, but none of them addressed my real problem: a unified ecosystem for both production and personal use.
I had always had a soft spot for Red Hat and was a fan of it for production servers. At the time, Red Hat was not free for personal use, so I went with CentOS to unify my environment. But as a developer who was not yet familiar with containerisation — tools like Podman and Docker — I needed bleeding-edge packages to practise with and keep up with the software I was building. CentOS was not the answer either.
I had always known about Fedora — it has long been one of the pioneer distributions in the Linux ecosystem — but I had run into problems configuring my Nvidia GPU drivers and occasional glitches in video games due to missing or improperly configured components. Around that time, a new distribution was rising: Nobara, a highly optimised Fedora-based distribution built for gaming. If a distribution is tuned for gaming — with up-to-date kernels, fresh GPU drivers, and low-level performance tweaks — it tends to get along well with machine learning setups too, and Nobara became my daily driver for a while. But I eventually realised it is not a community-driven project — it is maintained by a single person. I needed to respect my commitment to the open-source and community-driven spirit, so I started looking for an alternative and stumbled upon the Fedora Post Install Guide.
I thought: why not give Fedora one final, honest try? That try ended years of distro-hopping on the desktop. Following the guide transformed Fedora — I have always been a KDE user and have never warmed to GNOME — into an OS that was smooth, stable, and genuinely enjoyable to live in. Everything I had been looking for was there. One problem remained, though: some packages were unavailable in the default repositories, or required adding custom sources. Especially with the recent wave of Rust-based tools for Linux, I had adopted several in my daily workflow that simply were not in DNF.
How I Fixed the Packages Problem
That remaining problem — missing packages — was not unique to Fedora. It had been following me across every distribution I had ever used, in different forms. On Debian it was outdated versions. On Arch it was AUR maintenance risk. On CentOS it was the gap between stability and currency. Fedora brought me closer than anything else had, but it did not fully close it. The answer, when it finally arrived, did not come from the distribution at all.
Before getting to that, I want to talk about my relationship with Python — because the solution to the packages problem and the solution to surviving the Python ecosystem turned out to be the same thing.
Pixi: The Missing Piece for the Python Ecosystem and Beyond
I started my programming journey with Java in 2010. It was one of the most elegant languages available at the time, and I vividly remember compiling Android apps before Android Studio with Eclipse, installing them through a BlueStacks emulator. Later I switched to C/C++ to get closer to the underlying machine — learning the hard way about setting up projects, installing libraries system-wide, writing Makefiles, and eventually CMakeLists.txt. Cross-compiling for another architecture was painful, but it was structured and I understood every piece of it. I never touched package managers like Conan or vcpkg until much later.
Python was initially forced on me by an assignment. I just started writing it — some trial and error — and it worked. There was no AI to answer questions in 2020, so I learnt mostly by searching. Within a year Python had become my default language for assignments and eventually for shell scripting, replacing Bash in many places. Its package manager made the ecosystem feel welcoming, and the breadth of available libraries was remarkable.
To avoid dependency conflicts I kept a set of virtual environments in $HOME/.venv/ — one for computer vision, one for networking, one for scripting, one for signal processing — and activated whichever I needed. It worked, until I tried moving a project between systems. Binary packages were a particular nightmare. Conda was the standard answer for that, but it was too slow and too heavy for my taste. I kept trying to avoid it, compiling packages from source with my own compiler out of old C/C++ habits. Something was fundamentally wrong at the portability level — Python is interpreted, it should be portable, and yet reproducing an environment reliably across machines felt like a constant struggle. I tried requirements.txt, then Poetry, then uv. Both Poetry and uv were excellent tools, but neither handled binary packages independently — I still needed mamba alongside them for torch-based projects, and anything involving torch_scatter could turn into a nightmare.
Then I read a post about pixi somewhere and it immediately caught my attention. I ran to try it — a project manager that handles pip via uv and conda via mamba simultaneously, in a single environment file, without installing a heavy default package set. It worked across platforms: not just x86_64, but aarch64 and others too. You could even add custom channels. I had found what I had been looking for — a single tool to manage everything from a simple shell script to a complex multi-platform project with heavy binary dependencies.
Managing Packages on Fedora
The default package manager DNF remains my favourite package manager in Linux and is always the first place I look. For GUI applications, Flatpak has become indispensable — developers tend to keep their Flatpak releases more up to date than distribution repositories, and it covers the applications I use most: Telegram, Discord, and others. I install almost everything GUI-related through Flatpak.
For terminal tools and binary utilities, however, neither DNF nor Flatpak covers everything — particularly the newer wave of Rust-based CLI tools. On remote servers where I have no installation privileges, the situation is even more constrained.
Pixi turned out to solve this problem too. I had initially found its pixi shell activation requirement slightly annoying for tools I wanted available globally. Then I discovered:
pixi global install <package_name>This was a game changer. I could install tools outside any virtual environment — not as project dependencies, but as system-level utilities available anywhere. It became the first command I run on any new machine:
pixi global install zoxide ripgrep fd-find bat eza btm dust ncdu delta hyperfine xh sd neovim tmux zellij btop zsh xonsh skim yaziNo custom repositories. No extra layers. No searching for obscure PPAs or Copr entries. Everything in one command.
Different Distros for Different Jobs
While Fedora settled in as my desktop daily driver, the story does not end there — because Linux is not just my desktop. It is also every server I deploy to, every container image I build from, and every machine I do not personally sit in front of.
Around the same time I was settling into Fedora, I tried openSUSE Tumbleweed. I was sceptical at first — SUSE had always felt like the “other” enterprise ecosystem, the one I had less exposure to. But Tumbleweed surprised me. As a rolling release, it has no version numbers to worry about — no “Fedora 40 to 41” migration anxiety, no EOL countdown to plan around. And despite being rolling, it felt more stable day-to-day than Fedora ever had for me, thanks to openSUSE’s extensive automated testing (openQA) gating every update before it ships.
The catch is that openSUSE is SUSE-based, not Red Hat-based. For a desktop, that distinction barely matters any more — Flatpak and pixi cover the gap regardless of what is underneath. But for servers, it matters a great deal: if I am deploying to infrastructure running SLES rather than RHEL, openSUSE Tumbleweed (or openSUSE Leap for a non-rolling option) is the natural match, whereas a Fedora-family choice makes more sense alongside RHEL-compatible infrastructure.
Which brings me to AlmaLinux. Most of my Docker base images are AlmaLinux — it is a clean, community-driven RHEL rebuild, and that compatibility matters when the goal is reproducible builds across HPC systems, cloud instances, and local containers that all ultimately trace back to a RHEL lineage. For a long time, AlmaLinux on the desktop felt like a non-starter — too conservative, too server-oriented, missing too much. But with Flatpak filling in GUI applications and pixi filling in everything else, that gap has effectively closed. I now run AlmaLinux on some of my desktops too, and it feels far less like a compromise than it would have a few years ago.
So the honest version of the story is less “I found The One Distro” and more “I stopped needing to find one.” Fedora remains my favourite for a general-purpose desktop — KDE, gaming, day-to-day tinkering. openSUSE Tumbleweed is my pick when SUSE or SLES is the target, or when I want the most stable rolling release I have used. AlmaLinux sits underneath almost everything I containerise, and increasingly on desktops too. What used to feel like indecision now feels like having the right tool for the job — and the credit, again, goes to Flatpak and pixi for making the switching cost low enough that it no longer feels like a trade-off.
The Setup
If you are curious about the tools and how I configure them, this section is for you. The nice thing about all of this is that it is entirely distro-agnostic — even my IDEs, Zed (the main one) and VS Code (the reserve), are available everywhere these days. Whether I am on Fedora, AlmaLinux, or openSUSE Tumbleweed, the setup below is identical, because Flatpak, pixi global install, and IDE installers all work the same way regardless of distro.
Terminal Tools
| Tool | What it does |
|---|---|
zoxide |
Smarter cd with directory history and frecency ranking |
ripgrep |
Fast recursive text search (rg) |
fd-find |
Fast, ergonomic find replacement |
bat |
cat with syntax highlighting and Git integration |
eza |
Modern ls replacement with icons and tree view |
btm |
Interactive terminal system monitor |
dust / ncdu |
Fast, intuitive disk usage analyser |
delta |
Improved git diff pager with syntax highlighting |
hyperfine |
CLI benchmarking tool |
xh |
Friendly, fast HTTP client |
sd |
Intuitive search-and-replace for the command line |
neovim |
Modal text editor |
zellij / tmux |
Terminal workspace and session managers |
btop |
Beautiful terminal resource monitor |
skim |
Fuzzy finder (sk) for files, history, and more |
yazi |
Fast, terminal-based file manager |
Queuing GPU Work with pueue
One more tool that has quietly become part of my daily workflow is pueue — a lightweight command-line task manager and scheduler. You hand it shell commands, and it queues them up, running them one after another (or in parallel, if you configure multiple workers) regardless of whether you’re still logged in. I use it specifically to keep my GPU busy: I queue up several experiments before I step away from my desk or laptop, and pueue works through them in sequence, starting the next one the moment the previous one finishes — so the GPU keeps working even when I’m not.
Installation is just as easy via pixi, and the daemon only needs enabling once:
# Install pueue (client + daemon) globally via pixi
pixi global install pueue
# Enable and start the daemon as a systemd user service
systemctl --user enable pueued
systemctl --user start pueued
systemctl --user status pueuedFrom there, queuing experiments is as simple as:
pueue add -- pixi run python train.py --config exp1.yaml
pueue add -- pixi run python train.py --config exp2.yaml
pueue statusShell Aliases
I add the following to my .zshrc or .bashrc:
# zoxide must be initialized before the `cd` alias below, otherwise the `z`
# function doesn't exist yet and `cd` fails until you manually re-source
# this file
eval "$(zoxide init zsh)"
# Modern replacements
alias ls="eza --icons --group-directories-first"
alias tree="eza --icons --tree"
alias treed="eza --icons --tree --level"
alias lsdu='du -a -h --max-depth=1 | sort -hr'
alias du='dust'
alias top='btm'
alias cd='z'
alias grep='rg'
alias vim='nvim'
alias search='sk'
# Pipe --help and -h output through bat for syntax highlighting
alias -g -- -h='-h 2>&1 | bat --language=help --style=plain'
alias -g -- --help='--help 2>&1 | bat --language=help --style=plain'
# Set of general useful functions
# Create directory and instantly cd into it
mkcd() {
mkdir -p "$1" && cd "$1"
}
# Easily move UP 'N' directories (e.g., 'up 3' moves up 3 levels)
up() {
local levels="${1:-1}"
local path=""
for ((i=1; i<=levels; i++)); do
path="../$path"
done
cd "$path"
}
# Check public and local IP addresses cleanly
myip() {
local local_ip=$(hostname -I 2>/dev/null | awk '{print $1}' || ipconfig getifaddr en0 2>/dev/null || echo "Unknown")
echo "Local IP: $local_ip"
echo "Public IP: $(curl -s https://ifconfig.me)"
echo "Location: $(curl -s https://ipapi.co/city/), $(curl -s https://ipapi.co/country_name/)"
}
# Runs a clean multi-record (A, AAAA, MX, NS, TXT) DNS check for a domain.
dnslookup() {
if [[ -z "$1" ]]; then
echo "Usage: dnslookup <domain>"
return 1
fi
echo "=== A / AAAA Records ==="
dig +short "$1" A "$1" AAAA
echo "\n=== MX Records ==="
dig +short "$1" MX
echo "\n=== NS Records ==="
dig +short "$1" NS
echo "\n=== TXT Records ==="
dig +short "$1" TXT
}
# Find which process is listening on a specific port (e.g., 'port 8080')
port() {
if [[ -z "$1" ]]; then
echo "Usage: port <port_number>"
return 1
fi
lsof -i :"$1"
}
# Locates and forcibly terminates (kill -9) whatever process is occupying a given port.
killport() {
if [[ -z "$1" ]]; then
echo "Usage: killport <port_number>"
return 1
fi
local pid=$(lsof -t -i:"$1")
if [[ -n "$pid" ]]; then
echo "Killing process $pid on port $1..."
kill -9 $pid
else
echo "No process found on port $1."
fi
}
# Quick HTTP server in the current directory (Python 3)
serve() {
local port="${1:-8000}"
echo "Serving current directory on http://localhost:${port}"
python3 -m http.server "$port"
}
# Pretty-prints each directory in your $PATH on a new line for easy inspection.
path() {
echo "$PATH" | tr ':' '\n'
}
# Side-by-side file comparison using your default Neovim alias.
vdiff() {
if [[ $# -lt 2 ]]; then echo "Usage: vdiff <file1> <file2>"; return 1; fi
nvim -d "$1" "$2"
}
# Previews the head of gzipped or zstd-compressed text/log files without extraction.
vgz() {
if [[ -z "$1" ]]; then echo "Usage: vgz <file.gz|file.zst> [lines]"; return 1; fi
local lines="${2:-20}"
case "$1" in
*.gz) gzip -dc "$1" | head -n "$lines" ;;
*.zst) zstdcat "$1" | head -n "$lines" ;;
*) echo "Unsupported archive format." ;;
esac
}
# Lists files inside .zip, .tar, .tar.gz, or .whl archives without extracting.
vzip() {
if [[ -z "$1" ]]; then echo "Usage: vzip <archive_file>"; return 1; fi
case "$1" in
*.zip|*.whl) unzip -l "$1" ;;
*.tar.gz|*.tgz) tar -tzvf "$1" ;;
*.tar) tar -tvf "$1" ;;
*) echo "Unsupported extension." ;;
esac
}For Neovim, I use AstroNvim as my configuration framework:
git clone --depth 1 https://github.com/AstroNvim/template ~/.config/nvim \
&& rm -rf ~/.config/nvim/.git \
&& nvimSLURM Helpers for Supercomputers
This part is for the HPC crowd. If you have never touched a SLURM cluster, feel free to skip ahead to the conclusion — nothing below will be relevant to you.
For HPC environments running SLURM, I keep these functions in my shell configuration. They make interactive GPU sessions much less tedious to manage — particularly on Isambard-AI, where I spend a lot of time.
export NODE_SHELL="zsh"
getgnode() {
local ngpus=1
local time_limit="02:00:00"
local partition="workq" # Adjust for your cluster
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
echo "Usage: getgnode [OPTIONS]"
echo "Allocate and attach to an interactive SLURM GPU node."
echo ""
echo "Options:"
echo " -g, --gpus INT Number of GPUs to request (default: 1)"
echo " -p, --partition NAME SLURM partition (default: workq)"
echo " -t, --time HH:MM:SS Time limit (default: 02:00:00)"
echo " -h, --help Show this help"
return 0
;;
-g|--gpus) ngpus="$2"; shift 2 ;;
-p|--partition) partition="$2"; shift 2 ;;
-t|--time) time_limit="$2"; shift 2 ;;
*)
echo "Error: Unknown argument '$1'"
echo "Type 'getgnode --help' for usage."
return 1
;;
esac
done
salloc --partition="${partition}" \
--gres=gpu:"${ngpus}" \
--time="${time_limit}" \
srun --pty "${NODE_SHELL:-zsh}"
}
fetchgnode() {
if [[ "$1" == "-h" || "$1" == "--help" || -z "$1" ]]; then
echo "Usage: fetchgnode <jobid>"
echo "Attach a new shell to an existing SLURM job."
return 0
fi
srun --jobid="$1" --overlap --pty "${NODE_SHELL:-zsh}"
}
slog() {
if [[ -z "$1" ]]; then
echo "Usage: slog <job_id> [lines]"
echo "Tails the stdout log file for a given Slurm job."
return 1
fi
local lines="${2:-50}"
# Search current dir for slurm-<jobid>.out or <name>-<jobid>.out
local logfile=$(ls -t *"$1"*.out 2>/dev/null | head -n 1)
if [[ -n "$logfile" ]]; then
echo "Tailing log: $logfile"
tail -n "$lines" -f "$logfile"
else
echo "Error: No log file matching *'$1'*.out found in current directory."
return 1
fi
}
sfree() {
echo "=== Available Cluster Resources ==="
sinfo -N -h -o "%15P %10t %10G %N" | grep -E "idle|mix" | grep -v "drain" | column -t
}
snode() {
if [[ -z "$1" ]]; then
echo "Usage: snode <job_id>"
echo "Lookup the host node of a running job and attach to it."
return 1
fi
local node=$(squeue -j "$1" -h -o "%N" 2>/dev/null)
if [[ -z "$node" || "$node" == "N/A" ]]; then
echo "Error: Job $1 is not actively running."
return 1
fi
echo "Connecting to job $1 on node [$node]..."
srun --jobid="$1" --overlap --pty "${NODE_SHELL:-zsh}"
}
seffme() {
if [[ -z "$1" ]]; then
echo "Usage: seffme <job_id>"
echo "Check peak memory, CPU efficiency, and exit codes for a job."
return 1
fi
if command -v seff &>/dev/null; then
seff "$1"
else
sacct -j "$1" --format=JobID,JobName%20,State,ExitCode,MaxRSS,ReqMem,AllocCPUS,Elapsed -X
fi
}
rebatch() {
if [[ $# -lt 2 ]]; then
echo "Usage: rebatch <job_id> <script.sh>"
echo "Cancels <job_id> and submits <script.sh>."
return 1
fi
echo "Canceling job $1..."
scancel "$1"
echo "Submitting $2..."
sbatch "$2"
}
alias sqm="squeue --me"What Actually Stuck
Looking back, the journey was never really about finding the right distribution — though it took me a long time to admit that. Every distribution I used taught me something: Debian gave me discipline, Arch gave me depth, CentOS gave me an appreciation for stability, Nobara reminded me why community matters, and openSUSE Tumbleweed showed me that rolling release and reliability are not opposites.
What I was actually searching for was a coherent philosophy: a system I could understand completely, trust in production, enjoy on the desktop, and extend without fighting it. The distribution itself turned out to be the least important variable. Fedora with KDE is where I am happiest on the desktop. AlmaLinux is where most of my containers and HPC environments live, and it has earned a place on some of my desktops too. openSUSE Tumbleweed is on standby for whenever SUSE-based infrastructure is the target, or when I want the smoothest rolling release I have used. And across every single one of them, Flatpak handles GUI applications and pixi handles everything else — Python environments, binary tools, cross-platform reproducibility — with a single unified interface that works identically whether I am on my workstation, my laptop, or a GH200 node on Isambard-AI.
The packages problem that once felt like an unsolvable compromise turned out to have a clean answer. I just had to wait for the Rust ecosystem to mature enough to build it — and stop insisting that the answer had to come bundled with a single distribution.