Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/939232cc4463b0bc2dc750dda786908a to your computer and use it in GitHub Desktop.

Select an option

Save aont/939232cc4463b0bc2dc750dda786908a to your computer and use it in GitHub Desktop.

Create a Debian rootfs as a non-root user using unshare --user --map-root-user and debootstrap

This short article explains a simple, practical one-shot workflow that lets a non-root user create a root filesystem (rootfs) with debootstrap and install packages inside it using apt, by running the operations inside an unprivileged user namespace where the user appears as root. It includes a ready-to-run script, key explanations, and troubleshooting hints.


What this does (short)

Run the provided script as a regular user. It uses unshare --user --map-root-user to create a user/mount/pid namespace where your UID is mapped to UID 0 inside that namespace. Inside that namespace the script mounts necessary pseudo-filesystems, runs debootstrap to populate a target directory with a minimal Debian rootfs, then chroots into that rootfs and runs apt-get to install packages (e.g. vim). When finished, it unmounts the bind mounts and leaves the populated rootfs at the path you chose.


Prerequisites (check these first)

  • Kernel must allow unprivileged user namespaces. Check with:
sysctl kernel.unprivileged_userns_clone

Value 1 → OK. If it’s 0, ask the system administrator (or enable it if you control the host).

  • Host must have unshare (util-linux) and debootstrap installed. Some systems may require newuidmap/newgidmap and proper /etc/subuid /etc/subgid entries for UID/GID mapping to work correctly.

One-shot script (run as non-root on host)

Replace ROOT=/path/to/root with the desired target directory.

#!/bin/bash
set -euo pipefail

ROOT=/path/to/root            # ← change this
DEBIAN_SUITE=stable          # e.g. stable, bookworm, focal
MIRROR="http://deb.debian.org/debian"
ARCH=$(dpkg --print-architecture)

# 1) create directories
mkdir -p "$ROOT"
for d in dev proc sys run tmp etc/apt/; do
  mkdir -p "$ROOT/$d"
done

# 2) copy resolv.conf so apt can resolve names (or bind mount if preferred)
cp -L /etc/resolv.conf "$ROOT/etc/resolv.conf" || true

# 3) run inside a new user/mount/pid namespace where we map our user to root
unshare --user --map-root-user --mount --pid --fork /bin/bash - <<'INNER'
set -euo pipefail

ROOT=''"$ROOT"''
ARCH=''"$ARCH"''
DEBIAN_SUITE=''"$DEBIAN_SUITE"''
MIRROR=''"$MIRROR"''

# mount pseudo-filesystems into the target (namespace-local mounts)
mount -t proc proc "$ROOT/proc"
mount --rbind /dev "$ROOT/dev"
mount --rbind /sys "$ROOT/sys" || true
mount --rbind /run "$ROOT/run" || true

mkdir -p "$ROOT/var/cache/apt/archives"
mount --bind /tmp "$ROOT/tmp" || true

# 4) ensure debootstrap exists and run it
if ! command -v debootstrap >/dev/null 2>&1; then
  echo "debootstrap not found on host; install it first." >&2
  exit 1
fi

debootstrap --variant=minbase --arch="$ARCH" "$DEBIAN_SUITE" "$ROOT" "$MIRROR"

# 5) chroot and install packages (example: vim)
chroot "$ROOT" /bin/bash -c "apt-get update && apt-get -y install --no-install-recommends vim"

# 6) cleanup (unshare-internal unmounts)
umount -l "$ROOT/proc" || true
umount -l "$ROOT/sys" || true
umount -l "$ROOT/dev" || true
umount -l "$ROOT/run" || true
umount -l "$ROOT/tmp" || true

INNER

echo "done. rootfs is in $ROOT"

Key points and explanations

  • Why --user --map-root-user works --user creates a user namespace; --map-root-user maps your (host) non-privileged UID to 0 inside that namespace. Inside that namespace you effectively act as root for namespace-local operations (mount, chroot, etc.), while remaining unprivileged on the host.

  • debootstrap can run from the host debootstrap unpacks a minimal Debian filesystem into a target directory. If you run it after you set up the mounts inside the namespace, it runs fine — debootstrap itself expects root, and the namespace-root satisfies that.

  • Why chroot is recommended for apt While APT offers -o Dir::Root=... style options, the reliable and simple method is to chroot into the freshly created rootfs and run apt-get update && apt-get install .... Make sure /proc, /dev, /sys, and DNS are available inside the chroot.

  • Networking and name resolution Put a working /etc/resolv.conf into the target (copy or bind-mount) so apt can resolve package mirrors.

  • Cleanup Unmount the bind mounts when finished. Although the user namespace is destroyed after unshare exits, mounts may persist against the target directory until explicitly unmounted — use umount -l where appropriate.


Troubleshooting (common issues)

  • unshare: unshare failed: Operation not permitted → kernel unprivileged user namespaces likely disabled. Check sysctl kernel.unprivileged_userns_clone and consult the admin.
  • UID/GID mapping errors or failure to create the namespace → the system may require newuidmap/newgidmap and entries in /etc/subuid and /etc/subgid. Install the uidmap tools and configure subuid/subgid per your distribution’s docs.
  • debootstrap not found → install it on the host (Debian/Ubuntu: sudo apt install debootstrap, but you need admin for that).
  • apt failing inside chroot → verify /proc, /dev, /sys, /run and /etc/resolv.conf are present in the chroot and that the mirror URL and suite are correct.

Alternative: using APT options (Dir::*)

APT supports -o overrides (e.g. Dir::Root and other Dir::* options), but making APT behave correctly entirely from the host often requires multiple Dir:: overrides and is more fiddly. For clarity and compatibility, chroot inside a mapped user namespace is simpler and more robust for general use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment