# 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.

Author: Ruhani Rabin
Canonical: https://rabin.blog/declarative-distrobox-ubuntu-zsh-nixos/
Published: 2026-10-22T00:00:00.000Z
Updated: 2026-10-22T01:45:00.000Z

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](https://distrobox.it/) 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](https://distrobox.it/#what-it-does).

## What is declarative, and what still changes inside Ubuntu?

This approach makes the setup much easier to maintain.

| Part                                            | Owner                           |
| ----------------------------------------------- | ------------------------------- |
| Podman and Distrobox installation               | NixOS configuration             |
| Container image choice and initial package list | Home Manager’s Distrobox recipe |
| Ubuntu aliases, prompt and plugin loading       | Repo-owned `.zshrc`             |
| Packages installed later with APT               | The running Ubuntu container    |
| Shell history, application data and credentials | Mutable data outside Git        |

**Note**: I am **declaring** how to set the environment up. I am **not** turning Ubuntu’s package manager into Nix.

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

```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](https://distrobox.it/compatibility/) 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

```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;
    };
  };
}
```

**Note**: `package = null` is intentional. NixOS already installs Distrobox in the previous step, so Home Manager only manages its configuration. You can instead let Home Manager install it by leaving `package` at its default and removing Distrobox from the system package list. Choose one owner.

`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](https://github.com/nix-community/home-manager/blob/release-26.05/modules/programs/distrobox.nix) supports both the defaults and the container declarations used here.

The result is two generated files:

```text
~/.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](https://rabin.blog/best-terminal-tools-nixos-zsh/), 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

```zsh
# 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](https://rabin.blog/best-terminal-tools-nixos-zsh/) 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

```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.

**Note**: If the target config file already exists as a regular file, back it up before activating Home Manager. Do not replace a directory containing other guest settings just to make a link succeed.

## 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.

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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](https://distrobox.it/usage/distrobox-assemble/) 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:

```bash
❯ ubuntu-shell
rabin@ubuntu:~/git-projects/nixos-config %
```

## Where DistroShelf fits

[DistroShelf](https://github.com/ranfdev/DistroShelf) 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

```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

```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](https://distrobox.it/compatibility/#containers-distros).

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:

```bash
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.
