Distrobox on NixOS: Declare an Ubuntu Toolbox and Its Zsh Setup

Keep NixOS as your host, use familiar Ubuntu tools inside Distrobox, and manage the container recipe and a separate Zsh configuration with Home Manager.

On this page

I became accustomed to the behavior of keeping my workstation in NixOS configuration. But I also know my way around Ubuntu commands since I have used it since early 2000s. Sometimes I just want to follow an Ubuntu development guide, install a familiar tool with APT, and get on with the work.

For this though, I do not need every experiment to become another package in my NixOS setup. That was the reason I started using Distrobox.

It gives me an Ubuntu environment on the same desktop. I can use its package manager and tools, while NixOS continues to look after the workstation. Then I can keep the useful parts of that environment in Git: which image to start from, which packages to install, and how I want the shell to behave.

The interesting part came with Zsh. My Ubuntu container shared the host home directory, but its terminal did not act like my NixOS terminal. Sharing a .zshrc was only part of the story. Most of my host setup was actually loaded through NixOS’s system Zsh configuration.

I ended up giving Ubuntu its own repo-owned shell configuration and launcher. Familiar commands, a separate startup path, and no need to change the working host shell.

TL;DR

  • Enable Podman and install Distrobox through NixOS.
  • Use Home Manager to write the Distrobox defaults and container recipe.
  • Create the container explicitly, then use APT inside Ubuntu for experiments.
  • Keep Ubuntu’s .zshrc in your repo and select it with ZDOTDIR through a launcher.
  • Record useful packages in the recipe. The running container still has mutable package state; a NixOS rebuild does not roll back APT changes.

Why use this when Nix already has development shells?

A Nix development shell is still a good choice when I want a project to declare its build tools and dependencies through Nix. Distrobox is useful when I want the conventions of another distribution as well: its package manager, filesystem layout, and available library versions.

For example, a tool’s installation guide might assume apt install, /usr/bin, and Ubuntu libraries. Or I might need an older toolchain that is available in a particular supported distribution release. I can keep that environment beside NixOS without adding those packages to the host.

“Older” needs a little care here. Pick a maintained release that provides the version you need. An old image tag does not make an unsupported distribution safe or guarantee an exact package version forever.

Distrobox (opens in a new tab) uses a container manager such as Podman underneath. It shares the host kernel, so it is useful for command-line tools and applications. A full virtual machine is the better fit when I need another kernel or a complete machine to test.

The home directory and desktop integration are convenient, but Distrobox is not a place where I would run unverified software; expecting strong isolation. The upstream project explains its integration and security boundary (opens in a new tab).

What is declarative, and what still changes inside Ubuntu?

This approach makes the setup much easier to maintain.

PartOwner
Podman and Distrobox installationNixOS configuration
Container image choice and initial package listHome Manager’s Distrobox recipe
Ubuntu aliases, prompt and plugin loadingRepo-owned .zshrc
Packages installed later with APTThe running Ubuntu container
Shell history, application data and credentialsMutable data outside Git

The walkthrough below assumes you already have Home Manager integrated with your NixOS configuration, and that home.nix is your user module. Keep your existing flake inputs, username, home directory and state versions. If your files have different names, merge the blocks into their existing owners.

My tested container was an Ubuntu toolbox image. To give this guide a clear starting point, the recipe below creates a new container called ubuntu-cli from Ubuntu 24.04. It does not replace an existing toolbox. I tested the separate Zsh approach in my running container; the fresh-container recipe is an example to build and check on your machine.

1. Enable Podman and install the host tools

In your NixOS configuration.nix, merge these settings into the existing module. If Podman is already enabled, keep that declaration rather than adding another one.

configuration.nix
{ pkgs, ... }:
{
virtualisation.podman.enable = true;
environment.systemPackages = with pkgs; [
distrobox
# Optional graphical container manager:
distroshelf
];
}

In my repo the package list lives in packages.nix; putting them there has the same effect. There is no need to enable Docker compatibility merely to use Distrobox with Podman.

NixOS is a supported Distrobox host. The compatibility page (opens in a new tab) also calls out executable paths as something to watch on NixOS. A command being available on the host does not mean its path or dependencies will work inside Ubuntu.

2. Declare the Ubuntu recipe in Home Manager

Add this block inside your existing home.nix module. Its module arguments need config and pkgs; keep any other arguments and settings you already use.

home.nix
{ config, pkgs, ... }:
{
programs.distrobox = {
enable = true;
package = null;
enableSystemdUnit = false;
settings.container_manager = "podman";
containers.ubuntu-cli = {
image = "docker.io/library/ubuntu:24.04";
additional_packages = "zsh git curl ca-certificates fzf ripgrep fd-find bat jq tmux neovim zsh-autosuggestions zsh-syntax-highlighting";
root = false;
replace = false;
start_now = false;
};
};
}

enableSystemdUnit = false keeps creation manual. I want to decide when to download an image or create a container, rather than have that happen at login. The current Home Manager module (opens in a new tab) supports both the defaults and the container declarations used here.

The result is two generated files:

~/.config/distrobox/distrobox.conf
~/.config/distrobox/containers.ini

The image tag selects Ubuntu 24.04. It does not pin an immutable image digest or freeze the Ubuntu package repositories. This gives us a repeatable starting recipe, with normal Ubuntu updates still available.

3. Give Ubuntu its own Zsh configuration

By default, Distrobox shares your host home directory. Editing ~/.zshrc inside the container can therefore edit the same file used by NixOS. That was the part I wanted to avoid.

There is another detail: Ubuntu reads its own system Zsh startup files. My NixOS aliases, tools and prompt initialization live mostly in the host’s generated /etc/zshrc. Sharing the user file did not bring all of that across.

I keep the guest settings in a separate file instead. Create this small tree inside your NixOS configuration repo:

  • nixos-config/
    • configuration.nix
    • home.nix
    • app-configs/
      • distrobox/
        • ubuntu-zsh/
          • .zshrc

Create app-configs/distrobox/ubuntu-zsh/.zshrc with the following contents. This is a small starting setup, using the Ubuntu packages declared above.

app-configs/distrobox/ubuntu-zsh/.zshrc
# Author: Ruhani Rabin
autoload -Uz compinit
mkdir -p "$HOME/.cache/ubuntu-cli"
compinit -d "$HOME/.cache/ubuntu-cli/zcompdump"
bindkey -e
setopt HIST_IGNORE_DUPS SHARE_HISTORY HIST_FCNTL_LOCK
mkdir -p "$HOME/.local/state/ubuntu-cli"
HISTFILE="$HOME/.local/state/ubuntu-cli/zsh_history"
HISTSIZE=10000
SAVEHIST=10000
export EDITOR=nvim
export VISUAL=nvim
PROMPT='%F{green}%n@ubuntu%f:%F{blue}%~%f %# '
alias ..='cd ..'
alias ...='cd ../..'
alias ll='ls -lh --color=auto'
alias la='ls -lah --color=auto'
alias fd='fdfind'
alias bat='batcat'
export FZF_CTRL_T_COMMAND='fdfind --type f --hidden --exclude .git'
export FZF_ALT_C_COMMAND='fdfind --type d --hidden --exclude .git'
export FZF_CTRL_T_OPTS="--preview 'batcat --color=always --style=numbers --line-range=:200 -- {}'"
export FZF_ALT_C_OPTS="--preview 'ls -la -- {}'"
[[ -r /usr/share/doc/fzf/examples/completion.zsh ]] &&
source /usr/share/doc/fzf/examples/completion.zsh
[[ -r /usr/share/doc/fzf/examples/key-bindings.zsh ]] &&
source /usr/share/doc/fzf/examples/key-bindings.zsh
[[ -r /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]] &&
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
# Load highlighting after the other widgets and plugins.
[[ -r /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]] &&
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

Ubuntu calls the fd executable fdfind and bat batcat. The aliases keep my normal typing habits, while fzf uses the actual executable names in its subprocesses.

The Ubuntu 24.04 container I checked had fzf 0.44.1. Its packaged Zsh scripts work for this setup; copying my newer host’s fzf --zsh initialization would not be the right approach. Plugin packages also need to be sourced, which is why installing them alone did not finish the shell setup.

History and the completion cache have their own locations outside Git. I want to review changes to aliases and keybindings in a commit, not thousands of generated completion lines.

This is the benefit of declaring the shell as well as the packages. The recipe says which tools should exist; the .zshrc says how I use them together. I can review an alias change, undo a broken plugin setup, or recreate the same shortcuts without remembering what I once pasted into a terminal. My NixOS terminal tools setup uses the same idea on the host.

Link the file and add an entry command

Merge these two assignments into the same home.nix module from step 2. Append the launcher to your existing home.packages list if you already have one.

home.nix
xdg.configFile."distrobox/ubuntu-zsh/.zshrc".source =
./app-configs/distrobox/ubuntu-zsh/.zshrc;
home.packages = [
(pkgs.writeShellScriptBin "ubuntu-cli-shell" ''
# Author: Ruhani Rabin
exec ${pkgs.distrobox}/bin/distrobox enter ubuntu-cli -- \
env ZDOTDIR="${config.xdg.configHome}/distrobox/ubuntu-zsh" \
/usr/bin/zsh "$@"
'')
];

ZDOTDIR tells Zsh where to find its user startup files. The launcher enters Ubuntu first, then sets that directory for the guest’s /usr/bin/zsh. Your host .zshrc and .zshenv are left alone.

This example uses a normal Home Manager source link: the file goes through the Nix store and an activation applies changes. That is a useful starting point because a Nix generation retains the configuration it was built with.

My live workstation uses an out-of-store link into the checked-out repo instead. That makes edits visible in the next guest shell without a rebuild, but older Nix generations still point to the currently edited file. Use Git to undo those edits. Either approach can keep the authored configuration in Git; they have different rollback behavior.

The Ubuntu shell file in my workstation repo has the fuller version, including project/file search helpers and fallbacks for optional tools. Start with the small version here before borrowing those extras.

4. Build, activate, then create the container

For a flake-based NixOS configuration, run these commands from your configuration repo on the host. Replace nixos with your nixosConfigurations name.

Terminal window
git add configuration.nix home.nix app-configs/distrobox/ubuntu-zsh/.zshrc
nix build .#nixosConfigurations.nixos.config.system.build.toplevel --no-link
sudo nixos-rebuild switch --flake .#nixos

Staging matters here: newly created files need to be visible to a Git-backed flake. If you put the package list in packages.nix, stage that file too. These are standard commands; my local nixcfg and ns shortcuts are not prerequisites.

Now check the installed tools and generated recipe:

Terminal window
distrobox version
podman info --format '{{.Host.Security.Rootless}}'
cat ~/.config/distrobox/containers.ini

The Podman check should print true for this rootless workflow. If it fails, fix the Podman error before trying to create containers. Do not work around it with sudo distrobox.

Preview the creation command, then create only the declared Ubuntu entry:

Terminal window
distrobox assemble create --dry-run \
--file ~/.config/distrobox/containers.ini --name ubuntu-cli
distrobox assemble create \
--file ~/.config/distrobox/containers.ini --name ubuntu-cli
ubuntu-cli-shell

The first real entry can take a while while Distrobox prepares the guest and installs packages. Inside Ubuntu, check:

Terminal window
cat /etc/os-release
print -r -- "$ZDOTDIR"
alias fd bat

You should see Ubuntu, the separate Zsh config directory, and the two Ubuntu executable aliases. Try Tab completion, Ctrl+T for file selection, Alt+C for directories and Ctrl+R for history. Autosuggestions use previous commands, so they need some history before there is much to suggest.

Use exit to return to the host. From the host, distrobox stop ubuntu-cli stops the container; the next ubuntu-cli-shell starts it again. Installed packages survive stop/start.

5. The daily workflow: try with APT, then record what matters

Once inside Ubuntu, use Ubuntu commands. For example, if I want to try its build tools:

Terminal window
sudo apt update
sudo apt install build-essential pkg-config

That changes the Ubuntu container. It does not add those packages to environment.systemPackages on NixOS.

If they become useful, append build-essential pkg-config to the recipe’s additional_packages string. Now a future container creation has a record of what I wanted installed.

Changing that string and rebuilding NixOS does not establish that an existing container has been updated. Keep using APT for the running guest. The recipe is for creation, not continuous synchronization of every package change. The assemble documentation (opens in a new tab) describes the manifest fields and replacement behavior.

For a tool with its own per-project version manager, keep the project’s lock files or version files as well. A declaration of nodejs or python3 alone is not an exact development environment.

My useful maintenance loop is:

  1. Enter the container with its dedicated shell launcher.
  2. Try the tool with Ubuntu’s package manager.
  3. Keep useful package names and authored shell changes in the repo.
  4. Back up valuable guest data separately.
  5. Test a new container name when checking whether the recipe recreates the setup.

Here is the simplest way I enter the Ubuntu shell:

Terminal window
❯ ubuntu-shell
rabin@ubuntu:~/git-projects/nixos-config %

Where DistroShelf fits

DistroShelf (opens in a new tab) is an optional graphical interface for Distrobox. It is handy for seeing the containers and opening terminals. It does not replace the recipe or shell files.

On my machine, its GNOME Terminal choice did not launch correctly, while Alacritty worked. Entering the same container from Kitty also worked. I kept Alacritty selected rather than treating that launcher issue as a broken container.

If you have Alacritty installed and confirm that choice works, Home Manager can preserve it:

home.nix
dconf.settings."com/ranfdev/DistroShelf".selected-terminal = "Alacritty";

My dedicated ubuntu-shell command and DistroShelf’s ordinary terminal entry intentionally behave differently. The command selects my repo-owned Zsh setup. The normal GUI action follows the container’s ordinary shell startup path. Both enter the same container; the launcher decides how the shell starts.

For the new example in this guide, use ubuntu-cli-shell when you want the declared configuration. A terminal emulator, a container and a shell are three separate choices.

Debian or Alpine instead?

The same Home Manager structure can describe other supported guests. Add another entry alongside containers.ubuntu-cli, using package names from that distribution:

home.nix
containers.debian-cli = {
image = "docker.io/library/debian:13";
additional_packages = "zsh git curl jq tmux";
root = false;
replace = false;
start_now = false;
};
containers.alpine-cli = {
image = "docker.io/library/alpine:3.22";
additional_packages = "zsh git curl jq tmux";
root = false;
replace = false;
start_now = false;
};

These assignments belong inside programs.distrobox, beside the Ubuntu entry, not at the top level of home.nix. Rebuild and activate, then use the same assemble command with --name debian-cli or --name alpine-cli.

Debian uses APT; Alpine uses apk, for example sudo apk add jq. Alpine uses musl rather than glibc, so an application binary built for Ubuntu may need a different package or image. Start with a supported image from the Distrobox compatibility list (opens in a new tab).

The Ubuntu plugin paths and executable aliases above are specific to that setup. Check the equivalent files before copying them into Debian, and especially Alpine. The guest alternatives here have not been part of my Ubuntu runtime test.

Changing or removing the setup later

Commit the authored .zshrc and Nix files when the behavior is right. Leave history, completions, downloaded images, container layers and credentials out of Git. If a shell edit causes trouble, restore the earlier file and reactivate Home Manager for the source-link version shown here.

For a container experiment, use a new name such as ubuntu-cli-test. Avoid --replace on the working one while you still need its installed packages or files. A NixOS generation rollback does not restore a container’s writable layer.

Removing a container declaration from Home Manager removes the recipe entry; it does not delete the existing container. When you intentionally want to remove one, protect anything valuable stored inside it, then run on the host:

Terminal window
distrobox stop ubuntu-cli
distrobox rm ubuntu-cli

The default shared home remains host data. A separate container home can be selected with the recipe’s home field if you prefer less dotfile sharing, but include that path in your backup plan and check where your launcher expects its config.

For me, the benefit is a familiar Ubuntu workspace with the useful setup written down. I can try a tool quickly, keep the pieces I want, and still know which file controls the shell when I come back to it later.

Comments

In this series: Building My NixOS Workstation

8 published posts

View series →
  1. Moving into Niri & DankMaterialShell in NixOS
  2. Install the ChatGPT and Codex Desktop App Declaratively on NixOS
  3. How I Run Affinity V2 on NixOS with affinity-nix
  4. Use Elgato Wave:3 on Linux with Noise Reduction, EQ, Compression and a Virtual Microphone
  5. Install Vicinae on NixOS with a Modular Niri Setup
  6. 5 Best Terminal Tools for NixOS: My Zsh Setup and Wrappers
  7. Moving from Niri and DMS to Noctalia v5 on NixOS
  8. Distrobox on NixOS: Declare an Ubuntu Toolbox and Its Zsh Setup
← All posts

Type to search the published collection.