Skip to content

Instantly share code, notes, and snippets.

@Siguza
Created August 23, 2025 03:56
Show Gist options
  • Save Siguza/66f49fab2a966ea3074189d039387dbb to your computer and use it in GitHub Desktop.
Save Siguza/66f49fab2a966ea3074189d039387dbb to your computer and use it in GitHub Desktop.
# debianize
# My notes of migrating a Ubuntu system to Debian, incrementally and (mostly) live.
# The way this works is:
# 1. Bootstrap a Debian install into /debian.
# 2. Boot that as a container with systemd-nspawn.
# 3. Set up kernel, bootloader and sshd inside the container.
# 4. Boot into recovery, move all root-level folders under /ubuntu and move
# everything under /debian to the root folder, then reboot.
# (This should be the only downtime.)
# 5. Run /ubuntu as a container, gradually move one service after another out
# of the container and onto the host.
# Should work on various distros, as long as they're systemd-based.
# ALL OF THIS WAS FIRST DONE IN A LOCAL VM, I'm not mad enough to test in prod!
# Also these are NOTES, not a script that you can run. Some commands will run on
# the "host", others in the systemd container. You'll have to pay attention.
# Be root
sudo su -
# Gonna make extensive use of this
apt-get install systemd-container
# Debootstrap
cd /tmp
wget https://ftp.debian.org/debian/pool/main/d/debootstrap/debootstrap_1.0.141~bpo12+1_all.deb
dpkg -i debootstrap_1.0.141~bpo12+1_all.deb
# Let's go
mkdir /debian
debootstrap --arch amd64 trixie /debian https://deb.debian.org/debian/
# Set a root pw so we can log in on the console
systemd-nspawn -D /debian /usr/bin/passwd
# Properly boot the container (and log in)
systemd-nspawn -D /debian /sbin/init
# Put proper APT sources in place
cat >/etc/apt/sources.list.d/debian.sources <<EOF
Types: deb deb-src
URIs: https://deb.debian.org/debian
Suites: trixie trixie-updates trixie-backports
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb deb-src
URIs: https://security.debian.org/debian-security
Suites: trixie-security
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
echo '# See /etc/apt/sources.list.d/debian.sources' >/etc/apt/sources.list
# Keep the root dir clean
echo 'do_symlinks = no' >/etc/kernel-img.conf
# We don't have a kernel or bootloader yet...
apt-get install linux-image-cloud-amd64 grub-cloud-amd64
# NOTE: You might need linux-image-amd64 here instead of the cloud variant, depending on what your VM exposes.
# For real HW, you will likely also not want the cloud grub, and you might need some firmware-linux*.
# Before we install an SSH server, we *really* want to get rid of the root password.
# But we don't want to lock ourselves out of getting a shell if need be. Systemd's
# tools allow that, but we need dbus for that - OpenSSH will need it as well anyway.
apt-get install dbus
systemctl start dbus
passwd -d root
# From now on, you can use the following command to get a shell in the container from the host:
# machinectl shell [container-name]
# For me, the container name is "debian", check "machinectl list" to see if yours is different.
# Install minimal environment for SSH, so this can later serve as host
apt-get install man-db sudo openssh-server
# Default RSA key settings are kinda weak, override them with something stronger
ssh-keygen -q -N '' -t rsa -b 8192 -f /etc/ssh/ssh_host_rsa_key
# If sshd on your host runs on the default port, then the one in the container will
# now have failed to start. Otherwise, it will be running now. Whatever your setup,
# ensure that the two are configured to different ports.
echo 'Port 23' >/etc/ssh/sshd_config.d/bind.conf
# I need this for my backup, you may or may not...
echo 'StreamLocalBindUnlink yes' >/etc/ssh/sshd_config.d/unix.conf
# Those days are over...
echo 'PasswordAuthentication no' >/etc/ssh/sshd_config.d/passwd.conf
# Personal choice:
# Remove "LANG" and "LC_*" from AcceptEnv in sshd_config.
# Apply
systemctl restart sshd
# Add user and set up SSH keys
adduser --disabled-password user
mkdir -p /home/user/.ssh
nano /home/user/.ssh/authorized_keys
chown -R user:user /home/user/.ssh
# sudo
echo 'user ALL=(ALL) ALL' >/etc/sudoers.d/user
chmod 0440 /etc/sudoers.d/user
# At this point, you should have full SSH access with sudo ability.
# Now we need a network config, for when we become the host.
# For static IP address (substitute your own, obviously):
cat >/etc/network/interfaces.d/static <<EOF
# The loopback network interface
auto lo
iface lo inet loopback
iface lo inet6 loopback
# The primary network interface
auto /en*=eth
iface eth inet static
address 198.51.100.41/32
gateway 198.51.100.1
iface eth inet6 static
address 3fff::babe/128
gateway fe80::1
EOF
# Alternatively, for DHCP:
cat >/etc/network/interfaces.d/dhcp <<EOF
# The loopback network interface
auto lo
iface lo inet loopback
iface lo inet6 loopback
# The primary network interface
auto /en*=eth
iface eth inet dhcp
iface eth inet6 auto
EOF
# And a DNS setup... we lowkey wanna add this config _before_ we start and install the resolver.
mkdir -p /etc/systemd/resolved.conf.d
# Assuming you want to run this on the public internet:
# Tell systemd-resolved to not open any ports.
cat >/etc/systemd/resolved.conf.d/cloud.conf <<EOF
[Resolve]
LLMNR=no
MulticastDNS=no
EOF
# And assuming you have no local DNS server:
cat >/etc/systemd/resolved.conf.d/quad9.conf <<EOF
[Resolve]
DNS=9.9.9.9:853#dns.quad9.net 149.112.112.112:853#dns.quad9.net [2620:fe::9]:853#dns.quad9.net [2620:fe::fe]:853#dns.quad9.net
DNSOverTLS=yes
EOF
# Now install
apt-get install systemd-resolved
# We also need to set up an fstab entry, so out file system gets mounted rw.
# You can use either /dev/... here or UUID=...
echo 'UUID=... / ext4 rw,errors=remount-ro 1 1' >/etc/fstab
# Edit grub config, if necessary. In my case, I had to disable serial and gfxterm
# and make sure only "console" is used.
nano /etc/default/grub
mkdir -p /boot/grub
# Drop a service file and install the tools necessary to run Ubuntu in a container later
cat >/etc/systemd/system/ubuntu-container.service <<EOF
[Unit]
Description=Ubuntu Container
PartOf=machines.target
Before=machines.target
After=network.target
[Service]
ExecStart=systemd-nspawn --quiet --keep-unit -D /ubuntu /sbin/init
KillMode=mixed
Type=notify
RestartForceExitStatus=133
SuccessExitStatus=133
Slice=machine.slice
Delegate=yes
DelegateSubgroup=supervisor
CoredumpReceive=yes
WatchdogSec=3min
[Install]
WantedBy=machines.target
EOF
apt-get install systemd-container
# Now shut the container down again. Ideally, open a second session and run:
# machinectl poweroff [container-name]
# We need less isolation to set up grub...
mount --rbind /dev /debian/dev
mount --rbind /proc /debian/proc
chroot /debian update-grub
# Now boot into recovery, whatever that looks like for your system.
# I will assume that /dev/sda1 is your EFI partition and /dev/sda2
# your main OS partition, and that the latter is mounted to /mnt.
cd /mnt
# Switcheroo
mkdir ubuntu
mv !(debian|ubuntu|lost+found) ubuntu/
mv debian/* ./
# And IF you have an EFI partition, it's now time to install the new grub
mkdir -p /mnt/efi
mount /dev/sda1 /mnt/efi
mount --rbind /dev /mnt/dev
mount --rbind /proc /mnt/proc
mount --rbind /sys /mnt/sys
chroot /mnt
grub-install --target=x86_64-efi --boot-directory=/boot --efi-directory=/efi
exit
# Reboot our new main OS
reboot
# Log in again, and spin up the new container
systemctl start ubuntu-container
systemctl enable ubuntu-container
# If everything worked out, then you should now be ready to move services out
# of the container and onto the host, one by one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment