Play with Distrobox!

Distrobox is a powerful tool which enables us to run multiple linux distributions and versions simultaneously, and switch among them seemlessly. In addition to the basic features provided by distrobox, I write some more scripts to make it easier to use.

Basic usage

Create a container(distrobox)

See the official repository to install and use it. For example, I can use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[rijuyuezhu@rjyz-linux:~] 
% distrobox create -n u22 -i ubuntu:22.04 130 ↵ ✭
Creating 'u22' using image ubuntu:22.04 [ OK ]
Distrobox 'u22' successfully created.
To enter, run:

distrobox enter u22

[rijuyuezhu@rjyz-linux:~]
% distrobox enter u22 ✭
Starting container... [ OK ]
Installing basic packages... [ OK ]
Setting up devpts mounts... [ OK ]
...
Container Setup Complete!

And now I get into the container.

Some properties to notice:

  • Many parts of the filesystem are shared between the container(u22) and host, including /dev, /tmp, /home, etc. However, Some other places, such as /etc, /usr, /opt are not shared. Hence, some programs can be effortlessly run in the container (those installed locally, e.g. anaconda), but some cannot (those installed by system-wide package manager like apt).
  • To access the host's filesystem from containers, use the /run/host directory, which mounts the host's root directory.

Export apps & bins to outside

Use distrobox export to export an app (*.desktop file) or a binary file from a container out to the host. The tutorial is clear enough, so I won't repeat it here. An example of using this command could be find at another blog of mine.

The official tutorial is quite useful and I recommend you to read it. Also, it provides some useful tips to play with distrobox.

My advanced configurations

Distinguish in/out prompt

It could be quite confusing to distinguish between host and container, since we are using the same username, the same hostname, the same shell and the same configuration file, and sadly, the same shell prompt between them. It's necessary to make some changes to the prompt.

Since I prefer using oh my zsh, I try to modify its theme. A tutorial to modify zsh theme could be found here. Previously, I use the omz theme kphoen, and I create my own theme kphoen-my on its basis. Firstly copy the kphoen theme to kphoen-my:

1
2
3
4
[rijuyuezhu@rjyz-linux:~] 
% cp ~/.oh-my-zsh/themes/kphoen.zsh-theme ~/.oh-my-zsh/custom/themes/kphoen-my.zsh-theme
[rijuyuezhu@rjyz-linux:~]
% vim ~/.oh-my-zsh/custom/themes/kphoen-my.zsh-theme

Then I modify kphoen-my.zsh-theme as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# kphoen-my.zsh-theme

function ZSH_MYTHEME_CONDAENV() {
if [[ -n "${CONDA_DEFAULT_ENV:-}" && "${CONDA_DEFAULT_ENV}" != base ]]; then
echo "(${CONDA_DEFAULT_ENV}) "
fi
}

function ZSH_MYTHEME_MACHINE() {
if [[ -n "${CONTAINER_ID:-}" && ( -e /run/.containerenv || -e /.dockerenv ) ]]; then
echo "%{$fg[cyan]%}${CONTAINER_ID}%{$reset_color%}"
else
echo "%{$fg[magenta]%}%m%{$reset_color%}"
fi
}

function ZSH_MYTHEME_SHLVL() {
local show_SHLVL=${SHLVL}
if [[ -n "${CONTAINER_ID:-}" && ( -e /run/.containerenv || -e /.dockerenv ) ]]; then
(( show_SHLVL-- ))
fi
if [[ "${show_SHLVL}" -gt 1 ]]; then
echo "%{$fg[yellow]%}(${show_SHLVL})%{$reset_color%}"
fi
}

PROMPT='[%{$fg[red]%}%n%{$reset_color%}@$(ZSH_MYTHEME_MACHINE)$(ZSH_MYTHEME_SHLVL):%{$fg[blue]%}%~%{$reset_color%}$(git_prompt_info)] $(ZSH_MYTHEME_CONDAENV)
%# '

ZSH_THEME_GIT_PROMPT_PREFIX=" on %{$fg[green]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
ZSH_THEME_GIT_PROMPT_DIRTY=""
ZSH_THEME_GIT_PROMPT_CLEAN=""

# display exitcode on the right when >0
ZSH_MYTHEME_RETURNCODE="%(?..%{$fg[red]%}%? ↵%{$reset_color%})"

RPROMPT='${ZSH_MYTHEME_RETURNCODE}$(git_prompt_status)%{$reset_color%}'

ZSH_THEME_GIT_PROMPT_ADDED="%{$fg[green]%} ✚"
ZSH_THEME_GIT_PROMPT_MODIFIED="%{$fg[blue]%} ✹"
ZSH_THEME_GIT_PROMPT_DELETED="%{$fg[red]%} ✖"
ZSH_THEME_GIT_PROMPT_RENAMED="%{$fg[magenta]%} ➜"
ZSH_THEME_GIT_PROMPT_UNMERGED="%{$fg[yellow]%} ═"
ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[cyan]%} ✭"

To view its effect, when at host the prompt is

1
2
[rijuyuezhu@rjyz-linux:~ on main] 
%
where rjyz-linux is the hostname. When in the container the prompt is
1
2
[rijuyuezhu@u22:~] 
%
where u22 is the container id. And I give container id and hostname different colors.

The crux to manage to do this modification is to notice that, when inside a container, the environment variable CONTAINER_ID is set to be the container id.

Seemlessly switch

When using distrobox enter to go from host to container, $PWD will be preserved; however, when using exit to go from container to host, it rewinds to the directory where you entered the container. To make it seemlessly switch, I write a script. Put the following script in ~/.zshrc or ~/.bashrc and ensure that your host and container share that.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# distrobox
if [[ -n "${CONTAINER_ID:-}" && ( -e /run/.containerenv || -e /.dockerenv ) ]]; then
# inside container
if command -v distrobox-host-exec &> /dev/null; then
function de() {
# the path is a subdirectory of /home
if [[ -n "${DISTROBOX_CHGDIR_FILE:-}" &&
-e "${DISTROBOX_CHGDIR_FILE}" &&
"$(pwd)" =~ "^$(realpath /home)*" ]]; then
pwd > "${DISTROBOX_CHGDIR_FILE}"
fi
exit
}

#----------------------------------------------------------------#
# #
# The commented part below will be use later in the tutorial #
# #
#----------------------------------------------------------------#
#
# function dh() {
# if [[ $# -eq 0 ]]; then
# echo "Usage: dh <command>" >&2
# return 127
# fi
# distrobox-host-exec zsh -ic "$*"
# }
# if [ -n "${ZSH_VERSION:-}" ]; then
# function command_not_found_handler() {
# dh "$@"
# }
# else
# function command_not_found_handle() {
# dh "${@}"
# }
# fi
fi
else
# outside container
if command -v distrobox &> /dev/null; then
function de() {
function enter_container() {
local tmpfile="$(mktemp /tmp/distrobox.XXXXXX)"

DISTROBOX_CHGDIR_FILE="${tmpfile}" distrobox enter "$1"

if [[ -e "${tmpfile}" ]]; then
local dir="$(cat ${tmpfile})"
if [[ -n "${dir:-}" && -d "${dir:-}" ]]; then
cd "${dir}"
fi
fi
rm -f "${tmpfile}"
}

if [[ $# -eq 0 ]]; then
local default_container="u22"
enter_container "${default_container}"
else
enter_container "$1"
fi
}
fi
fi

This enables us to use de to switch between host and container seemlessly1 , and it will preserve $PWD if $PWD is a subdirectory of /home. This requirement prohibits confusing situations where you are in a directory that is not shared between host and container.

An example:

1
2
3
4
5
6
7
8
[rijuyuezhu@rjyz-linux:~/Playground] 
% de
[rijuyuezhu@u22:~/Playground]
% mkdir test && cd test
[rijuyuezhu@u22:~/Playground/test]
% de
[rijuyuezhu@rjyz-linux:~/Playground/test]
%

Execute host commands directly in container

Distrobox provides an interesting mechanism to run host commands from container. The official tutorial lies here. Firstly, install flatpak on the host. Then, when inside container, and you want to run host commands, simply run distrobox-host-exec <cmd>. For example,

1
2
3
4
5
6
7
[rijuyuezhu@u22:~] 
% distrobox-host-exec cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
...

EXTREMELY IMPORTANT thing to notice: the command runs on the host system directly, instead of "borrowing" binary from the host. Hence, as you see, the command above prints my host's information(Ubuntu 24.04), instead of the container's(Ubuntu 22.04). However, in most cases, this is not a problem: the host and container share the same /home directory. But remember this to avoid unexpected results.

Also, notice that environment variables are not preserved in most cases.

The official tutorial also provides an interesting method to run host commands seemlessly in container. It uses the command_not_found_handle(r?)(zsh has one more r) mechanism in bash/zsh. When the command is not found in container, bash/zsh will call the function command_not_found_handle(r?). Hence we can use it to forward the corresponding command to host, as shown in the commented part in above script. The complement part is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# distrobox
if [[ -n "${CONTAINER_ID:-}" && ( -e /run/.containerenv || -e /.dockerenv ) ]]; then
# inside container
if command -v distrobox-host-exec &> /dev/null; then
function de() {
# the path is a subdirectory of /home
if [[ -n "${DISTROBOX_CHGDIR_FILE:-}" &&
-e "${DISTROBOX_CHGDIR_FILE}" &&
"$(pwd)" =~ "^$(realpath /home)*" ]]; then
pwd > "${DISTROBOX_CHGDIR_FILE}"
fi
exit
}

function dh() {
if [[ $# -eq 0 ]]; then
echo "Usage: dh <command>" >&2
return 127
fi
distrobox-host-exec zsh -ic "$*"
}
if [ -n "${ZSH_VERSION:-}" ]; then
function command_not_found_handler() {
dh "$@"
}
else
function command_not_found_handle() {
dh "${@}"
}
fi
fi
else
# outside container
if command -v distrobox &> /dev/null; then
function de() {
function enter_container() {
local tmpfile="$(mktemp /tmp/distrobox.XXXXXX)"

DISTROBOX_CHGDIR_FILE="${tmpfile}" distrobox enter "$1"

if [[ -e "${tmpfile}" ]]; then
local dir="$(cat ${tmpfile})"
if [[ -n "${dir:-}" && -d "${dir:-}" ]]; then
cd "${dir}"
fi
fi
rm -f "${tmpfile}"
}

if [[ $# -eq 0 ]]; then
local default_container="u22"
enter_container "${default_container}"
else
enter_container "$1"
fi
}
fi
fi

Now you can use dh to run host commands in container. Also, when the command cannot be found in container, it will be forwarded to the host. For example,

1
2
3
4
5
6
7
8
[rijuyuezhu@u22:~] 
% env node
env: ‘node’: No such file or directory
[rijuyuezhu@u22:~]
% node
Welcome to Node.js v18.19.1.
Type ".help" for more information.
>
I do not have node installed in the container, but I can use node command in the container to make it run at host.

Some may argue that this is not good - we do not know which command is run in the container and which is run at host. However, this is not a problem, since we can use tools like zsh-syntax-highlighting to distinguish those commands that can be found in the container and those cannot. Also, this mechanism is only used for interactive shells (since we put it in ~/.zshrc or ~/.bashrc), and it has no effect on shell scripts.


  1. When entering a container from host, you can use one argument to indicate which container to use. Otherwise the script uses the default, hardcoded in the script(u22 here)↩︎