Created
August 23, 2025 03:56
-
-
Save Siguza/66f49fab2a966ea3074189d039387dbb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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