MMDEBSTRAP(1)

๐Ÿ‘‘ MMDEBSTRAP(1) โ€” User Contributed Perl Documentation ๐Ÿ‘‘

okay bestie, sit down, hydrate, because this is the page. it's MMDEBSTRAP(1), section 1, from the User Contributed Perl Documentation corner of the universe, and it's a banger. no cap. ๐Ÿ’–


โœจ NAME โœจ

mmdebstrap โ€” multi-mirror Debian chroot creation ๐ŸŒธ


๐Ÿฆ‹ SYNOPSIS ๐Ÿฆ‹

mmdebstrap [OPTION...] [SUITE [TARGET [MIRROR...]]]

that's it. that's the whole vibe. three little arguments and a dream ๐Ÿ’…


๐Ÿ’ซ DESCRIPTION ๐Ÿ’ซ

okay so mmdebstrap creates a Debian chroot of SUITE into TARGET from one or more MIRRORs. it's literally built as an alternative to the debootstrap tool (go peek at the DEBOOTSTRAP section if you want the tea โ˜•). and the way it differs? iconic behavior honestly: instead of doing its own dependency math, it uses apt to resolve dependencies, which means it can handle more than one mirror and untangle way more complex dependency relationships. if you wanna know how the sausage actually gets made internally, the OPERATION section has you covered ๐Ÿ”ฅ

the three arguments, bestie

SUITE ๐ŸŽ€

SUITE can be either a legit release code name (like sid, bookworm, trixie) or a symbolic name (like unstable, testing, stable, oldstable). honestly, any suite name that works with apt on the mirror you gave it will work. periodt.

SUITE is optional โ€” but only if you're not providing a TARGET and not providing a MIRROR either. if SUITE is missing, then the info about which suite you want has to come from standard input as part of a valid apt sources.list file, or be set up via hooks. no third option, sorry babe.

what's the value of SUITE even used for? it determines which apt index gets used to find the set of Essential:yes packages and/or the set of packages with the right priority for the variant you picked. and if you want that whole feature off? just choose the empty string for SUITE. that's the switch. see the VARIANTS section for more ๐Ÿ‘‘

TARGET ๐Ÿฆ‹

TARGET can be, and I need you to know the whole list:

  • a path to a directory
  • a path to a tarball filename
  • a path to a squashfs image
  • a path to an ext2 or ext4 image
  • a FIFO
  • a character special device
  • or just -

TARGET is optional if no MIRROR option is provided. if TARGET is missing, or if TARGET is -, then an uncompressed tarball gets sent to standard output. and without the --format option, TARGET itself is what picks the format. more on that in FORMATS โœจ

MIRROR ๐ŸŒˆ

MIRROR has range, honestly. it can be:

  • a URI
  • apt one-line format
  • a path to a file in apt's one-line format
  • a path to a file in deb822 format
  • or -

if you don't give a MIRROR at all, then <http://deb.debian.org/debian> is the default. and if SUITE doesn't refer to unstable or testing, then SUITE-updates and SUITE-security mirrors get automatically added. you don't have to ask, it just does it ๐Ÿ’…

now the matching game, because this is where it gets fun:

  • if a MIRROR option starts with deb or deb-src , then it's used as a one-line format entry for apt's sources.list inside the chroot.
  • if a MIRROR option contains a ://, then it's interpreted as a mirror URI, and the apt line inside the chroot gets assembled as deb [arch=A] B C D where A is the host's native architecture, B is the MIRROR, C is the given SUITE, and D is the components given via --components (which defaults to main).
  • if a MIRROR option happens to be an existing file, then its contents get written into the chroot's sources.list (that's when the first MIRROR is a file in one-line format) or into the chroot's sources.list.d directory, named with the extension .list or .sources โ€” depending on whether the file is in one-line or deb822 format, respectively.
  • if MIRROR is -, then standard input gets pasted into the chroot's sources.list.

you can specify more than one mirror and they get appended to the chroot's sources.list in the order you gave them. we love a queue ๐Ÿ’–

โš ๏ธ real talk though: if you specify a https or tor MIRROR and you want the chroot to be able to update itself, don't forget to also install the ca-certificates package, the apt-transport-https package (for apt versions less than 1.5) and/or the apt-transport-tor package using the --include option, as necessary. this is not optional, bestie, this is load-bearing ๐Ÿซถ

where does all the output go? ๐Ÿ˜ญ

all status output is printed to standard error, unless:

  • --logfile is used to redirect it to a file, or
  • --quiet or --silent is used to suppress any output on standard error.

help and version info get printed to standard error too, via the --help and --version options respectively. and otherwise? an uncompressed tarball might get sent to standard output โ€” if TARGET is -, or if no TARGET was specified at all. so yeah, mind your redirects, queen ๐Ÿ“


๐Ÿ’… OPTIONS ๐Ÿ’…

house rules first, because we respect the rules in this house:

  • options are case insensitive ๐Ÿ‘‘
  • short options may be bundled
  • long options require a double dash and may be abbreviated to uniqueness
  • options can be placed anywhere on the command line โ€” even before, or mixed in with, the SUITE, TARGET, and MIRROR arguments
  • a double dash -- can be used to stop interpreting command line arguments as options, which is how you get SUITE, TARGET and MIRROR arguments that start with a single or double dash
  • option order only matters for options that can be passed multiple times (as documented below)

  • -h, --help โ€” print the synopsis and options of this man page, then exit. that's it, that's the whole job ๐ŸŽ€
  • --man โ€” show the full man page, generated from the Perl POD, in a pager. this needs the perldoc program from the perl-doc package. it's literally the same as running:
pod2man /usr/bin/mmdebstrap | man -l -
  • --version โ€” print the mmdebstrap version and exit. short, sweet, iconic โœจ
  • --variant=name โ€” pick which package set to install. valid variant names are extract, custom, essential, apt, required, minbase, buildd, important, debootstrap, -, and standard. the default variant is debootstrap. see the VARIANTS section for more ๐Ÿ’–
  • --mode=name โ€” pick how to perform the chroot operation and create a filesystem with ownership information different from the current user. valid mode names are auto, sudo, root, unshare, fakeroot, fakechroot and chrootless. the default mode is auto. see the MODES section for more ๐ŸŒธ
  • --format=name โ€” pick the output format. valid format names are auto, directory, tar, squashfs, ext2, ext4 and null. the default format is auto. see the FORMATS section for more ๐Ÿฆ‹
  • --aptopt=option|file โ€” pass arbitrary options to apt. these get permamently added to /etc/apt/apt.conf.d/99mmdebstrap inside the chroot. (want temporary config options instead? that's what hooks are for.) can be specified multiple times โ€” each option gets appended to 99mmdebstrap. a semicolon will be added at the end of the option if necessary. and if the command line argument happens to be an existing file, the content of that file gets appended to 99mmdebstrap verbatim ๐Ÿ”ฅ

example: this is necessary for allowing old timestamps from snapshot.debian.org

--aptopt='Acquire::Check-Valid-Until "false"'
--aptopt='Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"'

example: settings controlling download of package description translations

--aptopt='Acquire::Languages { "environment"; "en"; }'
--aptopt='Acquire::Languages "none"'

example: enable installing Recommends (by default mmdebstrap doesn't)

--aptopt='Apt::Install-Recommends "true"'

example: configure apt-cacher or apt-cacher-ng as an apt proxy

--aptopt='Acquire::http { Proxy "http://127.0.0.1:3142"; }'

example: for situations in which the apt sandbox user cannot access the chroot

--aptopt='APT::Sandbox::User "root"'

example: minimizing the number of packages installed from experimental

--aptopt='APT::Solver "aspcud"'
--aptopt='APT::Solver::aspcud::Preferences
   "-count(solution,APT-Release:=/a=experimental/),-removed,-changed,-new"'
  • --keyring=file|directory โ€” change the default keyring that apt uses during the initial setup. this is similar to setting Dir::Etc::Trusted and Dir::Etc::TrustedParts using --aptopt, except that the --aptopt version gets permanently stored in the chroot, while the keyrings passed via --keyring are only visible to the apt that mmdebstrap itself runs. so do not use --keyring if apt inside the chroot needs to know about your keys after the initial chroot creation by mmdebstrap. this option is mainly for people using mmdebstrap as a deboostrap drop-in replacement, and as such, it's probably not what you want if you're using mmdebstrap with more than a single mirror โ€” unless you pass it a directory containing all the keyrings you need ๐Ÿ‘‘

by default, the local setting of Dir::Etc::Trusted and Dir::Etc::TrustedParts are used to choose the keyring used by the apt that mmdebstrap runs. these two locations are set to /etc/apt/trusted.gpg and /etc/apt/trusted.gpg.d by default. depending on whether a file or a directory is passed to this option, the former and latter default can be changed, respectively. and since apt only supports a single keyring file and single keyring directory, respectively, you can not use this option to pass multiple files and/or directories. using the --keyring argument like this is equal to keeping the default:

--keyring=/etc/apt/trusted.gpg --keyring=/etc/apt/trusted.gpg.d

if you need to pass multiple keyrings, use the signed-by option when specifying the mirror, like this:

mmdebstrap mysuite out.tar "deb [signed-by=/path/to/key.gpg] http://..."

another reason to use signed-by instead of --keyring is if apt inside the chroot needs to know by what key the repository is signed even after the initial chroot creation ๐Ÿ’€

the signed-by option will automatically be added to the final sources.list if the keyring required for the selected SUITE is not yet trusted by apt. automatically adding the signed-by option in these cases requires gpg to be installed. if gpg and ubuntu-archive-keyring are installed, then you can create a Ubuntu Bionic chroot on Debian like this:

mmdebstrap bionic ubuntu-bionic.tar

the resulting chroot will have a source.list with a signed-by option pointing to /usr/share/keyrings/ubuntu-archive-keyring.gpg ๐ŸŒˆ

you do not need to use --keyring or signed-by if you placed the keys that apt needs to know about into /etc/apt/trusted.gpg.d in the --setup-hook (which is before apt update runs), for example by using the copy-in special hook. and you also need to copy your keys into the chroot explicitly if the key you passed via signed-by points to a location that is not otherwise populated during chroot creation (for example by installing a keyring package) ๐Ÿงš

...and that's only part 1 bestie, there are 8 more parts coming, don't go anywhere ๐Ÿ’œ

  • --dpkgopt=option|file โ€” bestie, this one just yeets arbitrary options straight at dpkg. They get permanently added to /etc/dpkg/dpkg.cfg.d/99mmdebstrap inside the chroot, so likeโ€ฆ forever, no takebacks. If you want temporary config options instead, use hooks for that (iykyk). You can specify this multiple times btw โ€” each option just gets appended to 99mmdebstrap. And plot twist โœจ: if the command line argument is a file that actually exists, the content of that file gets appended to 99mmdebstrap verbatim. Iconic.

Example โ€” exclude paths to reduce chroot size (we love a slim queen ๐Ÿ‘‘):

--dpkgopt='path-exclude=/usr/share/man/*'
--dpkgopt='path-include=/usr/share/man/man[1-9]/*'
--dpkgopt='path-exclude=/usr/share/locale/*'
--dpkgopt='path-include=/usr/share/locale/locale.alias'
--dpkgopt='path-exclude=/usr/share/doc/*'
--dpkgopt='path-include=/usr/share/doc/*/copyright'
--dpkgopt='path-include=/usr/share/doc/*/changelog.Debian.*'
  • --include=pkg1[,pkg2,...] โ€” a comma or whitespace separated list of packages that get installed on top of whatever the variant you picked installs. Their direct and indirect hard dependencies come along too, obviously, they're a package's entourage. The behaviour here depends on which variant you chose, so pay attention bestie:
    • The extract and custom variants install no packages by default. So for those, the packages you name here are literally the only ones that get extracted or installed by dpkg, respectively.
    • For all other variants, apt is the one doing the installing of your extra packages.

Package names go straight to apt, untouched, which means you get all the fun apt features: pkg/suite, pkg=version, pkg-, globs or regexes for pkg, apt patterns, or even a path to a .deb package file (peep the notes further down about passing a .deb path in unshare mode, it's giving โœจcomplicationsโœจ). Check apt(8) for all the supported syntax.

You can specify this option multiple times, and the packages get concatenated in the order you gave them on the command line. If later list items are repeats, they get dropped so the final list is duplicate-free. So all of these are the same vibe:

--include="pkg1/stable pkg2=1.0 pkg3-"
--include=pkg1/stable,pkg2=1.0,pkg3-,,,
--incl=pkg1/stable --incl="pkg2=1.0 pkg3-" --incl=pkg2=1.0,pkg3-

Now here's the catch ๐Ÿšจ: since the list is separated by comma or whitespace, you cannot mix apt patterns or .deb package file paths that contain commas or whitespace with normal package names. If you try it, your patterns and paths get split by comma and whitespace too and become completely useless ๐Ÿ’€. To pass one of those, put it in its own --include option. If the argument to --include starts with an apt pattern or a file path, then it won't be split:

--include="?or(?priority(required), ?priority(important))"
--include="./path/to/deb with spaces/and,commas/foo.deb"

Specifically โ€” and this is the exact list, don't @ me โ€” all arguments to --include that start with a "?", "!", "~", "(", "/", "./" or "../" are not split and get treated as single arguments to apt. Wanna add more packages? Just use multiple --include options. And if you want to disable this whole pattern-and-path detection thing, start the argument to --include with a comma or whitespace.

If you pass the path to a .deb package file via --include, mmdebstrap will make sure that path exists. If it's a relative path, it gets internally converted to an absolute path. BUT โ€” and this is important โ€” since apt (outside the chroot) passes paths to dpkg (on the inside) verbatim, you have to make that .deb package available under the same path inside the chroot as well, otherwise dpkg inside the chroot simply cannot access it. You can pull this off with a setup-hook ๐Ÿช. There's actually a hook provided by mmdebstrap that automatically makes the contents of "file://" mirrors and .deb packages given with --include available inside the chroot:

--hook-dir=/usr/share/mmdebstrap/hooks/file-mirror-automount

This hook handles copying all the relevant files to their correct locations and cleans them up at the end. Very demure, very mindful. In unshare mode though, those .deb package paths have to be accessible by the unshared user too, which means the package itself likely has to be world-readable and every directory component on the path to it world-executable. No cap.

  • --components=comp1[,comp2,...] โ€” a comma or whitespace separated list of components like main, contrib, non-free and non-free-firmware, used for all URI-only MIRROR arguments. You can specify it multiple times and the components concatenate in command-line order. Repeats in later list items get dropped so the final component list has zero duplicates. So these are all equivalent, bestie:
--components="main contrib non-free non-free-firmware"
--components=main,contrib,non-free,non-free-firmware
--comp=main --comp="contrib non-free" --comp="main,non-free-firmware"
  • --architectures=native[,foreign1,...] โ€” comma or whitespace separated list of architectures. The first architecture is the native architecture inside the chroot. The rest get added to the foreign dpkg architectures. If you don't use this option, the native architecture of the chroot just defaults to the native architecture of the system running mmdebstrap. Can be specified multiple times and the values concatenate. Repeats in later list items get dropped so the list is duplicate-free. So, again, all equivalent:
--architectures="amd64 armhf mipsel"
--architectures=amd64,armhf,mipsel
--arch=amd64 --arch="armhf mipsel" --arch=armhf,mipsel
  • --simulate, --dry-run โ€” runs apt-get with --simulate. Only the package cache gets initialized โ€” no binary packages are downloaded or installed. Use this to quickly vibe-check whether a package selection within a certain suite and variant can in principle be installed, as far as their dependencies go. Heads up though: if the output is a tarball, then no output is produced at all ๐Ÿ˜ญ. If the output is a directory, that directory gets left populated with the skeleton files and directories necessary for apt to run in it. And no hooks are executed with --simulate or --dry-run. Periodt.
  • --setup-hook=command โ€” runs arbitrary commands right after initial setup (directory creation, configuration of apt and dpkg, ...) but before any packages are downloaded or installed. At that point the chroot directory contains no executables, so you can't chroot into it yet. Peep the section HOOKS for the full lore.

Example โ€” add extra apt sources entries on top of the default ones:

--setup-hook='echo "deb http..." > "$1"/etc/apt/sources.list.d/custom.list'

Example โ€” setup a chroot for installing a sub-essential busybox-based chroot with --variant=custom --include=dpkg,busybox,libc-bin,base-files,base-passwd,debianutils:

--setup-hook='mkdir -p "$1/bin"'
--setup-hook='for p in awk cat chmod chown cp diff echo env grep less ln
    mkdir mount rm rmdir sed sh sleep sort touch uname mktemp; do
    ln -s busybox "$1/bin/$p"; done'
--setup-hook='echo root:x:0:0:root:/root:/bin/sh > "$1/etc/passwd"'
--setup-hook='printf "root:x:0:\nmail:x:8:\nutmp:x:43:\n" > "$1/etc/group"'

For a much more elegant way of setting up a sub-essential busybox-based chroot, look at the --hook-dir option below. So much cleaner, we stan โœจ.

  • --extract-hook=command โ€” runs arbitrary commands after the Essential:yes packages have been extracted but before they're installed. Check the HOOKS section for more.

Example โ€” install busybox symlinks:

--extract-hook='chroot "$1" /bin/busybox --install -s'
  • --essential-hook=command โ€” runs arbitrary commands after the Essential:yes packages have been installed but before installing the remaining packages. Note that this hook is not executed for the extract and custom variants, so don't be surprised bestie. See HOOKS for more.

Example โ€” enable unattended upgrades:

--essential-hook='echo unattended-upgrades
    unattended-upgrades/enable_auto_updates boolean true
    | chroot "$1" debconf-set-selections'

Example โ€” select Europe/Berlin as the timezone:

--essential-hook='echo tzdata tzdata/Areas select Europe
    | chroot "$1" debconf-set-selections'
--essential-hook='echo tzdata tzdata/Zones/Europe select Berlin
    | chroot "$1" debconf-set-selections'
  • --customize-hook=command โ€” runs arbitrary commands after the chroot is set up and all packages got installed, but before final cleanup actions happen. Again, HOOKS section has the details.

Example โ€” add a user with no password:

--customize-hook='chroot "$1" useradd --home-dir /home/user
    --create-home user'
--customize-hook='chroot "$1" passwd --delete user'

Example โ€” set up /etc/hostname and /etc/hosts:

--customize-hook='echo host > "$1/etc/hostname"'
--customize-hook='echo "127.0.0.1 localhost host" > "$1/etc/hosts"'

Example โ€” to mimic debootstrap behaviour, mmdebstrap copies from the host ๐Ÿซถ. Remove them in a --customize-hook to make the chroot reproducible across multiple hosts:

--customize-hook='rm "$1"/etc/resolv.conf'
--customize-hook='rm "$1"/etc/hostname'
  • --hook-directory=directory โ€” okay so this one is giving automation queen โœจ it runs every script in directory whose filename starts with "setup", "extract", "essential" or "customize", and it fires them off at the matching stage of your mmdebstrap run. Those files gotta be marked executable though, no exceptions bestie. Their extension? totally ignored. Subdirectories? not traversed, we do NOT go digging ๐Ÿ™ˆ

Basically this option is just a cute little short-hand for passing the remaining four hook options individually for each file sitting in that directory. If a stage has more than one script, they get added alphabetically. And like โ€” that's exactly the vibe when you find yourself running the same hooks over and over again, iykyk. ๐Ÿ’…

So for example, say you've got a directory ./hooks holding two little scripts, setup01-foo.sh and setup02-bar.sh. This call right here:

    mmdebstrap --customize=./scriptA --hook-dir=./hooks --setup=./scriptB

is literally equivalent to this call:

    mmdebstrap --customize=./scriptA --setup=./hooks/setup01-foo.sh \
        --setup=./hooks/setup02-bar.sh --setup=./scriptB

Obsessed, honestly. ๐Ÿซถ

You can specify this option multiple times and the scripts get added to the respective hooks in the order the options show up on the command line. So if the scripts in two directories depend on each other? ๐Ÿ˜ญ You gotta put them in a common directory and name them so they get added in the correct order. No cap, order matters.

Example 1: Run mmdebstrap with eatmydata

    --hook-dir=/usr/share/mmdebstrap/hooks/eatmydata

Example 2: Setup chroot for installing a sub-essential busybox-based chroot

    --hook-dir=/usr/share/mmdebstrap/hooks/busybox

Example 3: Automatically mount all directories referenced by "file://" mirrors into the chroot

    --hook-dir=/usr/share/mmdebstrap/hooks/file-mirror-automount

  • --skip=stage[,stage,...] โ€” bestie, mmdebstrap tries so hard to implement sensible defaults and it will genuinely try to stop you before you shoot yourself in the foot ๐Ÿซฃ This option exists for when you are sure you know what you're doing, and it lets you skip certain actions and safety checks. Head over to the OPERATION section for the list of possible arguments and their context. You can specify it multiple times, or separate the multiple values by comma or whitespace. Slay responsibly. โš ๏ธ
  • -q,--quiet, -s,--silent โ€” do not write anything to standard error. ๐Ÿคซ And if you use this together with --verbose or --debug? Only the last option takes effect. Last one wins, periodt.
  • -v,--verbose โ€” instead of progress bars, write the dpkg and apt output directly to standard error. Real ones wanna see the raw tea โ˜• If used together with --quiet or --debug, only the last option takes effect.
  • -d,--debug โ€” on top of everything --verbose already gives you, this writes detailed debugging info to standard error ๐Ÿ’€ Errors will print a backtrace. If used together with --quiet or --verbose, only the last option takes effect.
  • --logfile=filename โ€” instead of writing status info to standard error, it writes it into the file given by filename. Perfect for when you want receipts of the whole journey ๐ŸŒธ

๐Ÿ‘‘ MODES โœจ

Okay bestie, listen ๐Ÿ’– making a Debian chroot isn't just "permission to run chroot", you also gotta be able to create files owned by the superuser. The mode is literally the vibe check for how we pull that off. ๐Ÿซถ

  • auto โ€” this one's the โœจmain characterโœจ that picks a fitting mode all by itself. If your effective user id is the superuser's, it goes sudo mode. Otherwise it picks unshare mode if /etc/subuid and /etc/subgid are set up correctly ๐Ÿงš. And if that's not the case and the fakechroot binary exists? Boom, fakechroot mode it is. ๐Ÿ’…
  • sudo, root โ€” this mode just directly executes chroot and is the exact same mode of operation debootstrap uses. It is the only mode that can directly create a directory chroot with the right permissions, no cap. If the chroot directory isn't accessible by the _apt user, then apt sandboxing gets automatically disabled ๐Ÿ™ˆ. This mode needs to be able to mount and thus requires "CAP_SYS_ADMIN". ๐Ÿ”ฅ
  • unshare โ€” when you use it as a normal (not root) user, this mode uses Linux user namespaces so unprivileged peeps can use chroot and create files that appear to be owned by the superuser inside the unshared namespace โœจ. A tarball created this way is gonna be bit-by-bit identical to one created with the root mode, no cap. With this mode, the only binaries that run as root are newuidmap(1) and newgidmap(1) via their setuid bit. Running those successfully requires /etc/subuid and /etc/subgid to have an entry for your username โ€” and that entry was usually already created by adduser(8) for you, bestie. ๐ŸŽ€

The unshared user will not automatically have access to the same files as you do. This is intentional โ€” it's an extra security layer against unintended changes to your files that could theoretically result from running mmdebstrap and package maintainer scripts. To copy files in and out of the chroot, either use globally readable or writable directories, or use special hooks like copy-in and copy-out. ๐Ÿฆ‹

Besides the user namespace, the mount, pid (process ids), uts (hostname) and ipc namespaces get unshared as well. Go read namespaces(7) and unshare(2) plus all the man pages they link to, iykyk. ๐Ÿ“š

A directory chroot created with this mode will end up with wrong ownership info (seen from outside the unshared user namespace). For correct ownership info, the directory must be accessed from a user namespace with the right subuid/subgid offset, like so:

$ lxc-usernsexec -- lxc-unshare -s 'MOUNT|PID|UTSNAME|IPC' -- \
> /usr/sbin/chroot ./debian-rootfs /bin/bash

Or without LXC:

$ mmdebstrap --unshare-helper /usr/sbin/chroot ./debian-rootfs /bin/bash

Or without mmdebstrap:

$ unshare --map-auto --map-user=65536 --map-group=65536 --keep-caps -- \
> /usr/sbin/chroot ./debian-rootfs /bin/bash

The above uses "--map-auto" to map the block of user/group ids for the effective user/group to a block starting at user/group ID 0. We also wanna map the current effective user/group ID into the subuid/subgid range using "--map-user" and "--map-group", respectively. But if that uid/gid overlaps with the respective range, a "hole" will get removed from the mapping by the unshare(1) utility and the remaining uid/gid values will get shifted ๐Ÿ˜ญ. So, we map the current effective user/group ID to the highest possible uid/gid, putting them at the end. Since that means the user/group will be "nobody" and not "root" inside the namespace, "--keep-caps" propagates permitted capabilities into the ambient set and thus gives the user "CAP_DAC_OVERRIDE" and other capabilities that it would've had. The following does NOT work:

$ unshare --map-root-user --map-auto ...            # or equavalient:
$ unshare --map-user=0 --map-group=0 --map-users=auto --map-groups=auto ...

Using the format of lxc-usernsexec(1), these options will result in the following mapping:

0:$UID:1 + 1:$SUBUIDBASE:65535

So a hole is punched in the automatically mapped range at the very beginning to make space for the user id of the current user as root inside the namespace, and the remaining uids are shifted. With "--map-user=65536" the mapping is as follows and places the current user at the end of the range, preventing any shifting:

0:$SUBUIDBASE:65536 + 65536:$UID:1

Lastly, if you don't mind using superuser privileges and have systemd-nspawn available and you know your subuid/subgid offset (100000 in this example):

$ sudo systemd-nspawn --private-users=100000 \
> --directory=./debian-rootfs /bin/bash

A directory created in unshare mode cannot be removed the normal way ๐Ÿ’€. Instead, use something like this:

$ unshare --map-root-user --map-auto rm -rf ./debian-rootfs

The above unshare(1) command will map user and group ids into different ranges compared to the mapping used by mmdebstrap (effectively shifting them one up) but it will provide the required capabilities for the removal operation. ๐Ÿ‘‘

If this mode is used as the root user, the user namespace is not unshared (but the mount namespace and others still are) and created directories will have correct ownership info. This is also useful in cases where the root user wants the benefits of an unshared mount namespace to prevent accidentally messing up the system. ๐ŸŒธ

  • fakeroot, fakechroot โ€” this mode will exec mmdebstrap again under "fakechroot fakeroot". A directory chroot created with this mode will end up with wrong permissions ๐Ÿ˜ฌ. If you need a directory then run mmdebstrap under "fakechroot fakeroot -s fakeroot.env" and use "fakeroot.env" later when entering the chroot with "fakechroot fakeroot -i fakeroot.env chroot ...". This mode will not work if maintainer scripts are unable to handle "LD_PRELOAD" correctly, like the package initramfs-tools until version 0.132. This mode will also not work with a different libc inside the chroot than on the outside. See the section LIMITATIONS in fakechroot(1). ๐Ÿ™ˆ
  • chrootless โ€” uses the dpkg option "--force-script-chrootless" to install packages into TARGET without dpkg and apt inside TARGET, but using apt and dpkg from the machine running mmdebstrap instead. Maintainer scripts are run without chrooting into TARGET and rely on their dependencies being installed on the machine running mmdebstrap. Only very few packages support this mode โ€” like, as of 2022, not all essential packages support it, fr fr. See https://wiki.debian.org/Teams/Dpkg/Spec/InstallBootstrap or the dpkg-root-support usertag of debian-dpkg@lists.debian.org in the Debian bug tracking system.
WARNING ๐Ÿšจ: if this option is used carelessly with packages that do not support "DPKG_ROOT", this mode can result in undesired changes to the system running mmdebstrap because maintainer-scripts will be run without chroot(1). Make sure to run this mode without superuser privileges and/or inside a throw-away chroot environment like so:
mmdebstrap --variant=apt --include=mmdebstrap \
    --customize-hook='chroot "$1" mmdebstrap --mode=chrootless
                      --variant=apt unstable chrootless.tar' \
    --customize-hook='copy-out chrootless.tar .' unstable /dev/null

๐ŸŒˆ VARIANTS ๐Ÿ’–

Every package set also includes the direct and indirect hard dependencies (but not recommends) of the selected package sets, periodt. The variants minbase, buildd and - resemble the package sets that debootstrap would install with the same --variant argument โœจ. The release with a name matching the SUITE argument as well as the native architecture will be used to determine the "Essential:yes" and priority values. To select packages with matching priority from any suite, specify the empty string for SUITE. The default variant is debootstrap. ๐ŸŽ€

  • extract โ€” installs nothing by default (not even "Essential:yes" packages, wild right ๐Ÿ˜ญ). Packages given by the "--include" option are extracted but will not be installed.
  • custom โ€” installs nothing by default (again, not even "Essential:yes" packages). Packages given by the "--include" option will be installed. If another mode than chrootless was selected and dpkg was not part of the included package set, then this variant will fail because it cannot configure the packages ๐Ÿ’€.
  • essential โ€” "Essential:yes" packages. If SUITE is a non-empty string, then only packages from the archive with suite or codename matching SUITE will be considered for selection of "Essential:yes" packages.
  • apt โ€” the essential set plus apt. It's roughly equivalent to running mmdebstrap with
--variant=essential --include="apt"
  • buildd โ€” the essential set plus apt and build-essential. It's roughly equivalent to running mmdebstrap with
--variant=essential --include="apt,build-essential"
  • required, minbase โ€” the essential set plus all packages with Priority:required. It's roughly equivalent to running mmdebstrap with
--variant=essential --include="?priority(required)"
  • important, debootstrap, - โ€” the required set plus all packages with Priority:important. This is the default of debootstrap ๐Ÿ”ฅ. It's roughly equivalent to running mmdebstrap with
--variant=essential --include="~prequired|~pimportant"
  • standard โ€” the important set plus all packages with Priority:standard. It's roughly equivalent to running mmdebstrap with
--variant=essential --include="~prequired|~pimportant|~pstandard"

โœจ FORMATS โœจ

So you wanna know what shape your brand new rootfs is gonna take? ๐Ÿ’… That's the <italic>format</italic>, and you pick it with the --format option. Don't pick one? No worries bestie, the default is auto and it'll figure it out for you. Here's the whole menu:

๐ŸŒˆ auto

When you go with auto (aka the default, the chill option, the "you decide for me" option), mmdebstrap peeks at the TARGET positional argument and infers the real format from it. The vibes-check logic goes like this:

  • If TARGET wasn't specified at all โ†’ congrats, it's tar format.
  • If TARGET happens to be /dev/null, or if standard output is /dev/null โ†’ it's null format.
  • If TARGET is an existing directory, and it does not equal "-" โ†’ it's directory format.
  • If TARGET ends with .tar or with any of the filename extensions listed in the COMPRESSION section, or if TARGET equals "-", or if TARGET is a named pipe (fifo), or if TARGET is a character special file โ†’ it's tar format.
  • If TARGET ends with .squashfs or .sqfs โ†’ it's squashfs format.
  • If TARGET ends with .ext2 โ†’ it's ext2 format.
  • If TARGET ends with .ext4 โ†’ it's ext4 format.
  • If literally none of those conditions apply โ†’ it falls back to directory format.

๐Ÿ“ directory, dir

Real ones know: this makes an actual chroot directory in TARGET. ๐Ÿซถ If that directory already exists, it's gotta be either empty, or contain only an empty lost+found directory. That's it, that's the whole rule.

The special TARGET of "-" does not work with this format, because likeโ€ฆ you cannot write a directory to standard output bestie. If you genuinely need your directory to be named -, just explicitly pass the relative path to it like ./-. The more you know โœจ.

Heads up: if a directory gets chosen as output in any mode other than sudo, then its contents will have wrong ownership information and special device files will be missing. That's not a vibe. Go read the MODES section for the full tea. ๐Ÿ˜ญ

๐ŸŽ€ tar

A temporary chroot directory gets created in $TMPDIR, or in /tmp if $TMPDIR isn't set. Then a tarball of that directory gets stored in TARGET, or sent to standard output if TARGET was omitted or if TARGET equals "-".

If TARGET ends with one of the filename extensions listed in the COMPRESSION section, then you get a compressed tarball, periodt. ๐Ÿ”ฅ

The tarball will be in POSIX 1003.1-2001 (pax) format and will contain extended attributes. To preserve those extended attributes, you have to pass --xattrs --xattrs-include='*' to tar when extracting the tarball. Don't forget, or they're gone girl ๐Ÿ’€


๐Ÿงš squashfs, sqfs

A temporary chroot directory gets created in $TMPDIR, or in /tmp if $TMPDIR isn't set. Then a tarball of that directory gets piped to the tar2sqfs utility, which will create an xz compressed squashfs image with a blocksize of 1048576 bytes in TARGET. Iconic.

The special TARGET of "-" does not work with this format, because tar2sqfs can only write to a regular file. If you need your squashfs image to be named -, just explicitly pass the relative path to it like ./-.

Also, the tar2sqfs tool only supports a limited set of extended attribute prefixes. Therefore, extended attributes are disabled in the resulting image. If you need them, create a tarball first and remove the extended attributes from its pax headers. Refer to the EXAMPLES section for how to achieve this. ๐ŸŒธ


๐Ÿฆ‹ ext2

A temporary chroot directory gets created in $TMPDIR, or in /tmp if $TMPDIR isn't set. Then a tarball of that directory gets piped to the genext2fs utility, which will create an ext2 image that will be approximately 90% full in TARGET.

The special TARGET of "-" does not work with this format, because genext2fs can only write to a regular file. If you need your ext2 image to be named -, just explicitly pass the relative path to it like ./-.

Wanna glow it up into something else? To convert the result to an ext3 image, use:

tune2fs -O has_journal TARGET

and to convert it to ext4, use:

tune2fs -O extents,uninit_bg,dir_index,has_journal TARGET
CAUTION: the ext2 format does not support timestamps beyond 2038 January 19, does not support sub-second precision timestamps, and does not support extended attributes. Its inode size of 128 prevents adding these features with tune2fs later on. So likeโ€ฆ know what you're getting into. ๐Ÿ˜ญ No cap.

๐Ÿ’œ ext4

A temporary chroot directory gets created in $TMPDIR, or in /tmp if $TMPDIR isn't set. Then a tarball of that directory gets piped to the mke2fs utility, which will create an ext4 image that will be approximately 90% full in TARGET.

The special TARGET of "-" does not work with this format, because mke2fs can only write to a regular file. If you need your ext4 image to be named -, just explicitly pass the relative path to it like ./-.

Now here's the cute part: if SOURCE_DATE_EPOCH is set, the filesystem UUID and hash_seed will be set to a UUID derived from SOURCE_DATE_EPOCH, so you get reproducible images. โญ Reproducibility is the moment, honestly.


๐Ÿซฅ null

A temporary chroot directory gets created in $TMPDIR, or in /tmp if $TMPDIR isn't set. After the bootstrap is complete, the temporary chroot gets deleted without being part of the output. Poof. โœจ

This is most useful when the desired artifact is generated inside the chroot and it's transferred using special hooks such as sync-out. It's also useful in situations where only the exit code, or stdout, or stderr of a process run in a hook is of interest. Main character energy: it's not about the rootfs, it's about what happens inside it. ๐Ÿ‘‘

โœจ HOOKS โœจ

okay bestie, this bit is all about the โœจ shared lore โœจ of the hook options --setup-hook, --extract-hook, --essential-hook and --customize-hook โ€” the four of them, common ground, we love to see it. anything that's specific to just one hook lives under that hook's own option in the OPTIONS section, iykyk. ๐Ÿซถ

You can specify these options multiple times, and the commands run in the exact order you gave them on the command line. no reordering, no chaos, just vibes in sequence. ๐Ÿ’…

There are four different types of hook option arguments:

  • if the argument you pass starts with "copy-in", "copy-out", "tar-in", "tar-out", "upload" or "download" followed by a space โ†’ congrats, it's a special hook ๐Ÿ‘‘
  • otherwise, if command is an existing executable file from $PATH, or if command doesn't contain any shell metacharacters โ†’ command is directly exec-ed, with the path to the chroot directory passed as the first argument
  • otherwise โ†’ command gets executed under sh, and you can reach the chroot directory via $1

Background (daemon) processes spawned in a hook are not guaranteed to persist beyond the hook that created them. so likeโ€ฆ don't get attached, they might ghost you ๐Ÿ˜ญ


Most environment variables set by mmdebstrap (like DEBIAN_FRONTEND, LC_ALL and PATH) are preserved. Most notably, APT_CONFIG is being unset. If you need the path to APT_CONFIG as written by mmdebstrap, it's chillin' in the MMDEBSTRAP_APT_CONFIG environment variable. All environment variables set by you, the user, are preserved โ€” except for TMPDIR, which gets cleared. see the TMPDIR section for the tea. ๐Ÿ“

Furthermore:

  • MMDEBSTRAP_MODE stores the mode set by --mode
  • MMDEBSTRAP_FORMAT stores the format chosen by --format
  • MMDEBSTRAP_HOOK stores which hook is currently running (setup, extract, essential, customize)
  • MMDEBSTRAP_ARGV0 stores the name of the binary with which mmdebstrap was executed
  • MMDEBSTRAP_VERBOSITY stores the numerical verbosity level (0 for no output, 1 for normal, 2 for verbose and 3 for debug output)
  • MMDEBSTRAP_INCLUDE stores the list of packages, apt patterns or file paths given by the --include option, separated by a comma and with commas and percent signs in the option values urlencoded โœจ
  • if a SUITE name was supplied, it's stored in MMDEBSTRAP_SUITE

In special hooks, the paths inside the chroot are relative to the root directory of the chroot. The path on the outside is relative to the current directory of the original mmdebstrap invocation. The path inside the chroot must already exist. Paths outside the chroot are created as necessary. ๐Ÿงš

In fakechroot mode, tar, or sh and cat, have to be run inside the chroot โ€” otherwise symlinks will be wrongly resolved and/or permissions will be off. This means the special hooks might fail in fakechroot mode for the setup hook or for the extract and custom variants if no tar or sh and cat is available inside the chroot. ๐Ÿ’€

copy-out pathinside [pathinside ...] pathoutside

Recursively copies one or more files and directories recursively from pathinside inside the chroot to pathoutside outside of the chroot. go off, copy queen ๐Ÿ‘‘

copy-in pathoutside [pathoutside ...] pathinside

Recursively copies one or more files and directories into the chroot, placing them into pathinside inside of the chroot. the reverse slay ๐Ÿ”

sync-out pathinside pathoutside

Recursively copy everything inside pathinside inside the chroot into pathoutside. In contrast to copy-out, this command synchronizes the content of pathinside with the content of pathoutside without deleting anything from pathoutside, but overwriting content as necessary. Use this command over copy-out if you don't want to create a new directory outside the chroot but only update the content of an existing directory. โœจ lowkey the upgrade โœจ

sync-in pathoutside pathinside

Recursively copy everything inside pathoutside into pathinside inside the chroot. In contrast to copy-in, this command synchronizes the content of pathoutside with the content of pathinside without deleting anything from pathinside, but overwriting content as necessary. Use this command over copy-in if you don't want to create a new directory inside the chroot but only update the content of an existing directory. ๐Ÿ’–

tar-in outside.tar pathinside

Unpacks a tarball outside.tar from outside the chroot into a certain location pathinside inside the chroot.

In unshare mode, device nodes cannot be created. To ignore device nodes in tarballs, use --skip=tar-in/mknod. ๐ŸŽ€

tar-out pathinside outside.tar

Packs the path pathinside from inside the chroot into a tarball, placing it into a certain location outside.tar outside the chroot. To emulate behaviour of cp and to provide control over the path which gets put into the tarball, a chdir() is performed to the dirname() of pathinside and then the basename() of pathinside is packaged as a tarball. For example, if pathinside is "/boot/." then first a chdir() into "/boot" will be performed before packing up the contents of "." inside "/boot". ๐Ÿฆ‹

download fileinside fileoutside

Copy the file given by fileinside from inside the chroot to outside the chroot as fileoutside. In contrast to copy-out, this command only handles files and not directories. To copy a directory recursively out of the chroot, use copy-out or tar-out. Its advantage is, that by being able to specify the full path on the outside, including the filename, the file on the outside can have a different name from the file on the inside. In contrast to copy-out and tar-out, this command follows symlinks. ๐Ÿ”ฅ

upload fileoutside fileinside

Copy the file given by fileoutside from outside the chroot to inside the chroot as fileinside. In contrast to copy-in, this command only handles files and not directories. To copy a directory recursively into the chroot, use copy-in or tar-in. Its advantage is, that by being able to specify the full path on the inside, including the filename, the file on the inside can have a different name from the file on the outside. In contrast to copy-in and tar-in, permission and ownership information will not be retained. ๐Ÿ˜ญ the permissions said "not today"

โœจ OPERATION โœจ

Bestie, this whole section is basically the glow-up tour of how a chroot gets built from absolutely nothing. At its core, what mmdebstrap does can be squished into a 14 line shell script, no cap:

mkdir -p "$2/etc/apt" "$2/var/cache" "$2/var/lib"
cat << END > "$2/apt.conf"
Apt::Architecture "$(dpkg --print-architecture)";
Apt::Architectures "$(dpkg --print-architecture)";
Dir "$(cd "$2" && pwd)";
Dir::Etc::Trusted "$(eval "$(apt-config shell v Dir::Etc::Trusted/f)"; printf %s "$v")";
Dir::Etc::TrustedParts "$(eval "$(apt-config shell v Dir::Etc::TrustedParts/d)"; printf %s "$v")";
END
echo "deb http://deb.debian.org/debian/ $1 main" > "$2/etc/apt/sources.list"
APT_CONFIG="$2/apt.conf" apt-get update
APT_CONFIG="$2/apt.conf" apt-get --yes --download-only install '?essential'
for f in "$2"/var/cache/apt/archives/*.deb; do dpkg-deb --extract "$f" "$2"; done
chroot "$2" sh -c "dpkg --install --force-depends /var/cache/apt/archives/*.deb"

So like, why does mmdebstrap even exist if that's the whole thing? Because it adds extra complexity for the good stuff: running with no superuser privileges, bit-by-bit reproducible output, hooks, and foreign architecture support. That's the whole plot ๐Ÿ’…

The rest of this section breaks down what mmdebstrap actually does, step by step, like a little backstage tour.


๐Ÿ‘€ check

Upon startup, several checks are carried out, like:

  • whether required utilities (apt, dpkg, tar) are installed
  • which mode to use and whether prerequisites are met
  • do not allow chrootless mode as root (without fakeroot) unless inside a chroot. This check can be disabled using --skip=check/chrootless
  • whether the requested architecture can be executed (requires arch-test) using qemu binfmt_misc support. This requires arch-test and can be disabled using --skip=check/qemu
  • how the apt sources can be assembled from SUITE, MIRROR and --components and/or from standard input as deb822 or one-line format and whether the required GPG keys exist.
  • which output format to pick depending on the --format argument or name of TARGET or its type.
  • whether the output directory is empty. This check can be disabled using --skip=check/empty
  • whether adding a "signed-by" to "apt/sources.list" is necessary. This requires gpg and can be disabled using --skip=check/signed-by

๐Ÿ› ๏ธ setup

The following tasks are carried out unless --skip=setup is used:

  • create required directories
  • write out the temporary apt config file
  • populates /etc/apt/apt.conf.d/99mmdebstrap and /etc/dpkg/dpkg.cfg.d/99mmdebstrap with config options from --aptopt and --dpkgopt, respectively
  • write out /etc/apt/sources.list
  • copy over /etc/resolv.conf and /etc/hostname
  • populate /dev if mknod is possible

๐Ÿช setup-hook

Run --setup-hook options and all setup* scripts in --hook-dir.

๐Ÿ”„ update

Runs apt-get update using the temporary apt configuration file created in the setup step. This can be disabled using --skip=update.

๐Ÿ“ฅ download

In the extract and custom variants, apt-get install is used to download all the packages requested via the --include option. In the remaining variants, apt patterns are used to find the Essential:yes packages from the native architecture. If SUITE is a non-empty string, then only packages from the archive with suite or codename matching SUITE will be considered for selection of Essential:yes packages.

๐Ÿ—œ๏ธ mount

Mount relevant device nodes, /proc and /sys into the chroot and unmount them afterwards. This can be disabled using --skip=chroot/mount or specifically by --skip=chroot/mount/dev, --skip=chroot/mount/proc and --skip=chroot/mount/sys, respectively. mmdebstrap will disable running services by temporarily moving /usr/sbin/policy-rc.d and /usr/sbin/start-stop-daemon if they exist. This can be disabled with --skip=chroot/policy-rc.d and --skip=chroot/start-stop-daemon, respectively.

๐Ÿ“ฆ extract

Extract the downloaded packages into the rootfs.

๐Ÿงš prepare

In fakechroot mode, environment variables LD_LIBRARY_PATH will be set up correctly. For foreign fakechroot environments, LD_LIBRARY_PATH and QEMU_LD_PREFIX are set up accordingly. This step is not carried out in extract mode and neither for the chrootless variant.

๐Ÿช extract-hook

Run --extract-hook options and all extract* scripts in --hook-dir.

๐Ÿ’– essential

Uses dpkg --install to properly install all packages that have been extracted before. Removes all packages downloaded in the download step, except those which were present in /var/cache/apt/archives/ before (if any). This can be disabled using --skip=essential/unlink. This step is not carried out in extract mode.

๐Ÿช essential-hook

Run --essential-hook options and all essential* scripts in --hook-dir. This step is not carried out in extract mode.

โญ install

Install the apt package into the chroot, if necessary and then run apt from inside the chroot to install all remaining packages. This step is not carried out in extract mode.

๐Ÿช customize-hook

Run --customize-hook options and all customize* scripts in --hook-dir. This step is not carried out in extract mode.

๐Ÿ™ˆ unmount

Unmount everything that was mounted during the mount stage and restores /usr/sbin/policy-rc.d and /usr/sbin/start-stop-daemon if necessary.

๐ŸงŸ zombie-reaping

Wait for (reap) still running processes (background processes or zombie processes), unless --skip=zombie-reaping is used.

๐Ÿงผ cleanup

Performs cleanup tasks, unless --skip=cleanup is used:

  • Removes the package lists (unless --skip=cleanup/apt/lists) and apt cache (unless --skip=cleanup/apt/cache). Both removals can be disabled by using --skip=cleanup/apt.
  • Remove all files that were put into the chroot for setup purposes, like /etc/apt/apt.conf.d/00mmdebstrap and the temporary apt config. This can be disabled using --skip=cleanup/mmdebstrap.
  • Remove files that make the result unreproducible and write the empty string to /etc/machine-id if it exists. This can be disabled using --skip=cleanup/reproducible. Note that this will not remove files that make the result unreproducible on machines with differing /etc/resolv.conf or /etc/hostname. Use a --customize-hook to make those two files reproducible across multiple hosts. See section "SOURCE_DATE_EPOCH" for more information. The following files will be removed:
    • /var/log/dpkg.log
    • /var/log/apt/history.log
    • /var/log/apt/term.log
    • /var/log/alternatives.log
    • /var/cache/ldconfig/aux-cache
    • /var/log/apt/eipp.log.xz
    • /var/lib/dbus/machine-id
  • Remove everything in /run inside the chroot. This can be disabled using --skip=cleanup/run.
  • Remove everything in /tmp inside the chroot. This can be disabled using --skip=cleanup/tmp.

๐ŸŽ€ output

For formats other than directory, pack up the temporary chroot directory into a tarball, ext2 image, ext4 image or squashfs image and delete the temporary chroot directory.

If --skip=output/dev is added, the resulting chroot will not contain the device nodes, directories and symlinks that debootstrap creates but just an empty /dev as created by base-files.

If --skip=output/mknod is added, the resulting chroot will not contain device nodes (neither block nor character special devices). This is useful if the chroot tarball is to be exatracted in environments where mknod does not function like in unshared user namespaces.


๐Ÿ’ซ EXAMPLES ๐Ÿ’ซ

Use like debootstrap:

$ sudo mmdebstrap unstable ./unstable-chroot

Without superuser privileges:

$ mmdebstrap unstable unstable-chroot.tar

With no command line arguments at all. The chroot content is entirely defined by a sources.list file on standard input.

$ mmdebstrap < /etc/apt/sources.list > unstable-chroot.tar

Since the tarball is output on stdout, members of it can be excluded using tar on-the-fly. For example the /dev directory can be removed from the final tarbal in cases where it is to be extracted by a non-root user who cannot create device nodes:

$ mmdebstrap unstable | tar --delete ./dev > unstable-chroot.tar

Create a tarball for use with sbuild --chroot-mode=unshare:

$ mmdebstrap --variant=buildd unstable ~/.cache/sbuild/unstable-amd64.tar

Create a buildd tarball for EOL distro:

$ mmdebstrap --include=apt --variant=buildd --aptopt \
    'Apt::Key::gpgvcommand "/usr/libexec/mmdebstrap/gpgvnoexpkeysig"' \
    --aptopt='Acquire::Check-Valid-Until "false"' \
    --keyring=/usr/share/keyrings/debian-archive-removed-keys.gpg \
    --hook-dir=/usr/share/mmdebstrap/hooks/maybe-jessie-or-older \
    jessie ~/.cache/sbuild/jessie-arm64.tar \
    http://archive.debian.org/debian

Instead of a tarball, a squashfs image can be created:

$ mmdebstrap unstable unstable-chroot.squashfs

By default, mmdebstrap runs tar2sqfs with --no-skip --exportable --compressor xz --block-size 1048576. To choose a different set of options, and to filter out all extended attributes not supported by tar2sqfs, pipe the output of mmdebstrap into tar2sqfs manually like so:

$ mmdebstrap unstable \
    | mmtarfilter --pax-exclude='*' \
        --pax-include='SCHILY.xattr.user.*' \
        --pax-include='SCHILY.xattr.trusted.*' \
        --pax-include='SCHILY.xattr.security.*' \
    | tar2sqfs --quiet --no-skip --force --exportable --compressor xz \
        --block-size 1048576 unstable-chroot.squashfs

By default, debootstrapping a stable distribution will add mirrors for security and updates to the sources.list.

$ mmdebstrap stable stable-chroot.tar

If you don't want this behaviour, you can override it by manually specifying a mirror in various different ways:

...

$ mmdebstrap stable stable-chroot.tar http://deb.debian.org/debian $ mmdebstrap stable stable-chroot.tar "deb http://deb.debian.org/debian stable main" $ mmdebstrap stable stable-chroot.tar /path/to/sources.list $ mmdebstrap stable stable-chroot.tar - < /path/to/sources.list

okay so this one is about โœ‚๏ธ trimming the fat โ€” you're dropping the locales (but bestie, keep the symlink to the locale name alias database, she stays), the translated manual packages (but the untranslated ones stay, they're fine), and the docs (but copyright and Debian changelog are coming with us, obviously ๐Ÿ’…):

$ mmdebstrap --variant=essential \
    --dpkgopt='path-exclude=/usr/share/man/*' \
    --dpkgopt='path-include=/usr/share/man/man[1-9]/*' \
    --dpkgopt='path-exclude=/usr/share/locale/*' \
    --dpkgopt='path-include=/usr/share/locale/locale.alias' \
    --dpkgopt='path-exclude=/usr/share/doc/*' \
    --dpkgopt='path-include=/usr/share/doc/*/copyright' \
    --dpkgopt='path-include=/usr/share/doc/*/changelog.Debian.*' \
    unstable debian-unstable.tar

Next up: build a whole bootable USB stick that boots straight into a full Debian desktop like it's nothing. main character energy ๐Ÿซถ

$ mmdebstrap --aptopt='Apt::Install-Recommends "true"' --customize-hook \
    'chroot "$1" adduser --comment user --disabled-password user' \
    --customize-hook='echo 'user:live' | chroot "$1" chpasswd' \
    --customize-hook='echo host > "$1/etc/hostname"' \
    --customize-hook='echo "127.0.0.1 localhost host" > "$1/etc/hosts"' \
    --include=linux-image-amd64,task-desktop unstable debian-unstable.tar
$ cat << END > extlinux.conf
> default linux
> timeout 0
>
> label linux
> kernel /vmlinuz
> append initrd=/initrd.img root=LABEL=rootfs
END
# You can use $(sudo blockdev --getsize64 /dev/sdXXX) to get the right
# image size for the target medium in bytes
$ guestfish -N debian-unstable.img=disk:8G -- \
    part-disk /dev/sda mbr : \
    part-set-bootable /dev/sda 1 true : \
    mkfs ext4 /dev/sda1 : \
    set-label /dev/sda1 rootfs : \
    mount /dev/sda1 / : \
    tar-in debian-unstable.tar / xattrs:true : \
    upload /usr/lib/EXTLINUX/mbr.bin /boot/mbr.bin : \
    copy-file-to-device /boot/mbr.bin /dev/sda size:440 : \
    extlinux / : copy-in extlinux.conf / : sync : umount / : shutdown
$ qemu-system-x86_64 -m 1G -enable-kvm debian-unstable.img
$ sudo dd if=debian-unstable.img of=/dev/sdXXX status=progress

and on architectures that don't have extlinux, you can still boot with grub2 โ€” the vibes are identical, the bootloader is just different ๐ŸŒˆ:

$ mmdebstrap --include=linux-image-amd64,grub2,systemd-sysv unstable fs.tar
$ guestfish -N debian-unstable.img=disk:2G -- \
    part-disk /dev/sda mbr : \
    part-set-bootable /dev/sda 1 true : \
    mkfs ext4 /dev/sda1 : \
    set-label /dev/sda1 rootfs : \
    mount /dev/sda1 / : \
    tar-in fs.tar / xattrs:true : \
    command "grub-install /dev/sda" : \
    command update-grub : \
    sync : umount / : shutdown
$ qemu-system-x86_64 -m 1G -enable-kvm debian-unstable.img

Wanna build libdvdcss2.deb without installing anything and without touching your current system's apt sources? Obsessed, actually โœจ (yes the original really does say "installing installing", we keep that, it's part of the lore):

$ mmdebstrap --variant=apt --components=main,contrib --include=libdvd-pkg \
    --customize-hook='chroot $1 /usr/lib/libdvd-pkg/b-i_libdvdcss.sh' \
    | tar --extract --verbose --strip-components=4 \
        --wildcards './usr/src/libdvd-pkg/libdvdcss2_*_*.deb'
$ ls libdvdcss2_*_*.deb

Using mmdebstrap as a drop-in replacement for autopkgtest-build-qemu and vmdb2, on every architecture that supports EFI booting (amd64, arm64, armhf, i386, riscv64)? There's a cute little convenience wrapper for that, so slay ๐ŸŽ€:

$ mmdebstrap-autopkgtest-build-qemu unstable ./autopkgtest.img

Same glow-up, but for the architectures that support extlinux (amd64 and i386):

$ mmdebstrap --variant=important --include=linux-image-amd64 \
    --customize-hook='chroot "$1" passwd --delete root' \
    --customize-hook='chroot "$1" useradd --home-dir /home/user --create-home user' \
    --customize-hook='chroot "$1" passwd --delete user' \
    --customize-hook='echo host > "$1/etc/hostname"' \
    --customize-hook='echo "127.0.0.1 localhost host" > "$1/etc/hosts"' \
    --customize-hook=/usr/share/autopkgtest/setup-commands/setup-testbed \
    unstable debian-unstable.tar
$ cat << END > extlinux.conf
> default linux
> timeout 0
>
> label linux
> kernel /vmlinuz
> append initrd=/initrd.img root=/dev/vda1 rw console=ttyS0
END
$ guestfish -N debian-unstable.img=disk:8G -- \
    part-disk /dev/sda mbr : \
    part-set-bootable /dev/sda 1 true : \
    mkfs ext4 /dev/sda1 : mount /dev/sda1 / : \
    tar-in debian-unstable.tar / xattrs:true : \
    upload /usr/lib/EXTLINUX/mbr.bin /boot/mbr.bin : \
    copy-file-to-device /boot/mbr.bin /dev/sda size:440 : \
    extlinux / : copy-in extlinux.conf / : sync : umount / : shutdown
$ qemu-img convert -O qcow2 debian-unstable.img debian-unstable.qcow2

Now the debootstrap-wrapper arc ๐Ÿฆ‹ โ€” you get to run debootstrap without superuser privileges by using Linux user namespaces instead. This fixes Debian bug #829134, no cap:

$ mmdebstrap --variant=custom --mode=unshare \
    --setup-hook='debootstrap unstable "$1"' \
    - debian-debootstrap.tar

Building a non-Debian chroot, like Ubuntu bionic? We love range ๐Ÿ’œ (fair warning, that --aptopt value is one logical line that's just wrapped prettier in the manual, so don't you dare add a newline):

$ mmdebstrap --aptopt='Dir::Etc::Trusted
   "/usr/share/keyrings/ubuntu-keyring-2012-archive.gpg"' bionic bionic.tar

okay so if for some reason you literally cannot use a caching proxy like apt-cacher or apt-cacher-ng, the sync-in and sync-out special hooks have your back: they sync a directory outside the chroot with /var/cache/apt/archives inside it ๐Ÿ’–

$ mmdebstrap --variant=apt --skip=essential/unlink \
    --setup-hook='mkdir -p ./cache "$1"/var/cache/apt/archives/' \
    --setup-hook='sync-in ./cache /var/cache/apt/archives/' \
    --customize-hook='sync-out /var/cache/apt/archives ./cache' \
    unstable /dev/null

and instead of copying what could be a ridiculous amount of data with sync-in, you can use a bind-mount together with a file:// mirror so the packages from outside are just... there, inside the chroot. it's giving efficiency โญ

$ mmdebstrap --variant=apt --skip=essential/unlink \
    --setup-hook='mkdir "$1/tmp/mirror"' \
    --setup-hook='mount -o ro,bind /tmp/mirror "$1/tmp/mirror"' \
    --customize-hook='sync-out /var/cache/apt/archives ./cache' \
    --customize-hook='umount "$1/tmp/mirror"; rmdir "$1/tmp/mirror";' \
    unstable /dev/null file:///tmp/mirror http://deb.debian.org/debian

and if you want all the directories referenced by file:// mirrors automatically mounted into the chroot? there's a hook directory for exactly that ๐Ÿ™ˆ

$ mmdebstrap --variant=apt \
    --hook-dir=/usr/share/mmdebstrap/hooks/file-mirror-automount \
    unstable /dev/null file:///tmp/mirror1 file:///tmp/mirror2

Create a system that plays nice with docker ๐Ÿณ:

$ mmdebstrap unstable | sudo docker import - debian
[...]
$ sudo docker run -it --rm debian whoami
root
$ sudo docker rmi debian

Create and boot a qemu virtual machine for literally any architecture, using the debvm-create wrapper script around mmdebstrap ๐Ÿ‘‘

$ debvm-create -r stable -- --architecture=riscv64
$ debvm-run

Create a system that can be used with podman ๐ŸŒธ:

$ mmdebstrap unstable | podman import - debian
[...]
$ podman run --network=none -it --rm debian whoami
root
$ podman rmi debian

Using mmdebstrap as a docker/podman replacement? iconic behavior:

$ mmdebstrap unstable chroot.tar
[...]
$ mmdebstrap --variant=custom --skip=update,tar-in/mknod \
    --setup-hook='tar-in chroot.tar /' \
    --customize-hook='chroot "$1" whoami' unstable /dev/null
[...]
root
$ rm chroot.tar

and here's the tip that saves you a whole download: you can re-use a chroot tarball you made with mmdebstrap to refine it further. Say you want a minimal chroot and a chroot with more packages โ€” instead of pulling and installing the essential packages twice, you just build on top of the minimal chroot you already have. so smart ๐Ÿง โœจ

$ mmdebstrap --variant=apt unstable chroot.tar
$ mmdebstrap --variant=custom --skip=update,setup,cleanup,tar-in/mknod \
    --setup-hook='tar-in chroot.tar /' \
    --customize-hook='chroot "$1" apt-get install --yes pkg1 pkg2' \
    '' chroot-full.tar

๐ŸŒฟ ENVIRONMENT VARIABLES ๐ŸŒฟ

  • SOURCE_DATE_EPOCH โ€” set this and your result becomes reproducible across multiple runs, as long as the options and the mirror content are the same ๐Ÿ”
Heads up though, bestie: for debootstrap compatibility, mmdebstrap copies the host's /etc/resolv.conf and /etc/hostname into the chroot. Which means the mmdebstrap output will differ if you run it on machines whose /etc/resolv.conf and /etc/hostname contents are different. ๐Ÿ˜ญ To get reproducibility across different hosts, you have to manually either delete both files from the output:
$ mmdebstrap --customize-hook='rm "$1"/etc/resolv.conf' \
             --customize-hook='rm "$1"/etc/hostname' ...

or fill them with reproducible content:

$ mmdebstrap --customize-hook='echo nameserver X > "$1"/etc/resolv.conf' \
             --customize-hook='echo host > "$1"/etc/hostname' ...
  • TMPDIR โ€” when you're creating a tarball, a temporary directory gets populated with the rootfs before the tarball is packed ๐Ÿ“ฆ. That temp directory lives in /tmp โ€” or wherever TMPDIR points, if that environment variable is set. Setting TMPDIR somewhere other than /tmp is genuinely useful if your /tmp is on a tmpfs that's too small for your rootfs. small-tmpfs girlies, this one's for you ๐Ÿ’…

If you set TMPDIR in unshare mode, then the unshared user has to be able to access that directory. Which means the directory itself must be world-writable, and every single one of its ancestors must be at least world-executable. no exceptions ๐Ÿซถ

And since TMPDIR is only valid outside the chroot, the variable gets unset while hook scripts are running. So if you need a valid temporary directory inside a hook, consider using /tmp inside your target directory instead. iykyk ๐Ÿ˜Œ

๐Ÿ‘ฏ DEBOOTSTRAP โ€” the comparison arc ๐Ÿ’…

okay so if you're coming from debootstrap and wondering what's different, here's the tea bestie ๐Ÿ“ โ€” this section is literally just the glow-up list.

โœจ stuff mmdebstrap does that debootstrap doesn't โœจ

  • More than one mirror possible โ€” yes queen, stack those mirrors ๐Ÿ‘‘
  • Default mirrors for stable releases include updates and security mirror
  • Multiple ways to operate as non-root: fakechroot and unshare
  • twice as fast โ€” not me flexing but ok ๐Ÿ”ฅ
  • Can create a chroot with only "Essential:yes" packages and their deps
  • Reproducible output by default if $SOURCE_DATE_EPOCH is set
  • Can create output on filesystems with nodev set
  • apt cache and lists are cleaned at the end ๐Ÿงน
  • foreign architecture chroots using qemu-user

๐Ÿ˜ญ the limitations though (ngl, keeping it real) ๐Ÿ˜ญ

  • Only runs on systems with apt installed (Debian and derivatives)
  • No SCRIPT argument (use hooks instead)
  • Some debootstrap options don't exist, namely:

--second-stage, --exclude, --resolve-deps, --force-check-gpg, --merged-usr, --no-merged-usr and --cache-dir.


๐ŸŒธ MERGED-/USR ๐ŸŒธ

So here's the vibe: mmdebstrap will create a merged-/usr chroot or not depending on whether packages setting up merged-/usr (i.e. the usrmerge package) are installed or not. In Debian, the essential package init-system-helpers depends on the usrmerge package, starting with Debian 12 (Bookworm). periodt ๐Ÿ’œ

Before Debian 12 (Bookworm), to force mmdebstrap to create a chroot with merged-/usr using symlinks, you have two options โ€” either explicitly install the usrmerge package:

--include=usrmerge

or setup merged-/usr using the debootstrap-method which takes care of the architecture specific symlinks and installs the usr-is-merged package:

--hook-dir=/usr/share/mmdebstrap/hooks/merged-usr

To force mmdebstrap to create a chroot without merged-/usr even after the Debian 12 (Bookworm) release, you can use the following hook:

--hook-dir=/usr/share/mmdebstrap/hooks/no-merged-usr

This will write "this system will not be supported in the future" into /etc/unsupported-skip-usrmerge-conversion inside the chroot and install the usr-is-merged package to avoid the installation of the usrmerge package and its dependencies. it's giving "blocked and ghosted" energy honestly ๐Ÿ™ˆ

If you are using mmdebstrap in a setup where you do not know upfront whether the chroot you are creating should be merged-/usr or not and you want to avoid installation of the usrmerge package and it's dependencies, you can use:

--hook-dir=/usr/share/mmdebstrap/hooks/maybe-merged-usr

That hook will use the availability of the usr-is-merged package to decide whether to call the merged-usr hook or not. she's flexible, she's indecisive, she's iconic โœจ


๐ŸŽ€ COMPRESSION ๐ŸŽ€

mmdebstrap will choose a suitable compressor for the output tarball depending on the filename extension. The following mapping from filename extension to compressor applies:

extension compressor
--------------------
.tar      none
.gz       gzip
.tgz      gzip
.taz      gzip
.Z        compress
.taZ      compress
.bz2      bzip2
.tbz      bzip2
.tbz2     bzip2
.tz2      bzip2
.lz       lzip
.lzma     lzma
.tlz      lzma
.lzo      lzop
.lz4      lz4
.xz       xz
.txz      xz
.zst      zstd

To change compression specific options, either use the respecitve environment variables like XZ_OPT or send mmdebstrap output to your compressor of choice with a pipe. slay ๐Ÿฆ‹


๐Ÿซถ WRAPPERS ๐Ÿซถ

debvm

debvm helps create and run virtual machines for various Debian releases and architectures. The tool debvm-create can be used to create a virtual machine image and the tool debvm-run can be used to run such a machine image. Their purpose primarily is testing software using qemu as a containment technology. These are relatively thin wrappers around mmdebstrap and qemu. cute little delegation moment ๐Ÿ’–

bdebstrap

bdebstrap is a YAML config based multi-mirror Debian chroot creation tool. bdebstrap is an alternative to debootstrap and a wrapper around mmdebstrap to support YAML based configuration files. It inherits all benefits from mmdebstrap. The support for configuration allows storing all customization in a YAML file instead of having to use a very long one-liner call to mmdebstrap. It also layering multiple customizations on top of each other, e.g. to support flavors of an image. layered looks, main character energy ๐Ÿ‘‘


๐Ÿ’€ BUGS ๐Ÿ’€

As of version 1.20.9, dpkg does not provide facilities preventing it from reading the dpkg configuration of the machine running mmdebstrap. Therefore, until this dpkg limitation is fixed, a default dpkg configuration is recommended on machines running mmdebstrap. If you are using mmdebstrap as the non-root user, then as a workaround you could run:

chmod 600 /etc/dpkg/dpkg.cfg.d/*

so that the config files are only accessible by the root user. See Debian bug #808203.

โš ๏ธ With apt versions before 2.1.16, setting "[trusted=yes]" or "Acquire::AllowInsecureRepositories "1"" to allow signed archives without a known public key or unsigned archives will fail because of a gpg warning in the apt output. Since apt does not communicate its status via any other means than human readable strings, and because mmdebstrap wants to treat transient network errors as errors, mmdebstrap treats any warning from "apt-get update" as an error. no cap, it will genuinely just give up ๐Ÿ’€

๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

debootstrap(8), debvm(1), bdebstrap(1)


and that's a wrap on the whole manpage bestie ๐Ÿ˜ญ๐Ÿ’– this one's perl v5.40.1, dated 2025-08-13, and she's signed MMDEBSTRAP(1). go build a chroot, you absolute legend ๐Ÿ‘‘โœจ๐ŸŒˆ

๐Ÿ“ฆ where this page comes from