--- /dev/null
+(use-modules (gnu)
+ (gnu packages ssh)
+ (gnu services networking)
+ (gnu services ssh)
+ (infiniteadaptability services cloud-init)
+ (guix gexp))
+
+(define %ssh-key "[INSERT_SSH_KEY]")
+
+;; use: blkid -s UUID -o value /dev/vda1
+(define %blkid "[INSERT_UUID]")
+
+(operating-system
+ (host-name "guix-test")
+ (timezone "Etc/UTC")
+ (locale "en_US.utf8")
+
+ ;; The supplied layout booted in legacy BIOS mode.
+ ;; Keep Debian's GRUB; installation below uses --no-bootloader.
+ (bootloader (bootloader-configuration
+ (bootloader grub-bootloader)
+ (targets '("/dev/vda"))))
+
+ ;; Guix's root is the @guix Btrfs subvolume on /dev/vda1.
+ (file-systems (cons* (file-system
+ (device (uuid %blkid))
+ (mount-point "/")
+ (type "btrfs")
+ (options "subvol=@guix")) %base-file-systems))
+
+ (services
+ (append (list (service cloud-init-service-type
+ ;; An example of extra configuration files. This specific
+ ;; file is required for properly running cloud-init on DigitalOcean
+ (cloud-init-configuration (extra-configuration-files `
+ (("99-digitalocean.cfg" ,
+ (plain-file
+ "99-digitalocean.cfg"
+ "datasource_list: [ ConfigDrive, DigitalOcean, NoCloud, None ]"))))))
+
+ (service network-manager-service-type)
+ (service wpa-supplicant-service-type)
+ (service openssh-service-type
+ (openssh-configuration (openssh openssh-sans-x)
+ (permit-root-login 'prohibit-password)
+ (password-authentication? #f)
+ (authorized-keys `(("root" ,(plain-file
+ "auth-key"
+ %ssh-key)))))))
+ %base-services)))
--- /dev/null
+# Debian VPS → Guix System on a Btrfs subvolume (full flow)
+
+This replaces a Debian VPS with Guix System in place. It needs no custom image, no installer and
+no recovery console of your own. The only part that needs offline access to the disk (the
+ext4 → Btrfs conversion) is done by the provider's operator. Everything else runs over SSH.
+
+Tested end to end on a DigitalOcean droplet (Debian 13, 10 GB disk, GPT, legacy BIOS boot).
+
+Assumed disk layout (check with `lsblk -f`):
+
+| Partition | Type | Role |
+|--------------|----------------------|-----------------------------------|
+| `/dev/vda1` | Linux root | ext4 → Btrfs, holds `@guix` later |
+| `/dev/vda14` | BIOS boot (~3 MB) | GRUB core image (BIOS on GPT) |
+| `/dev/vda15` | EFI System (vfat) | unused, left alone |
+
+The flow: **operator converts root to Btrfs, updates `/etc/fstab` and reinstalls GRUB → you install Guix on Debian → create `@guix`
+subvolume → `guix system init` into it → boot Guix → delete the old Debian files.**
+
+---
+
+## Phase 1 — Operator, in recovery mode
+
+See `sporestack-btrfs-convert-request.md` for the version to send to support. The operator
+makes **three required changes**:
+
+1. Convert the filesystem.
+2. Update the root entry in `/etc/fstab`.
+3. Reinstall GRUB.
+
+```sh
+lsblk -f
+e2fsck -f /dev/vda1
+btrfs-convert /dev/vda1 # keeps ext4 copy in ext2_saved (rollback)
+
+mount -o subvolid=5 /dev/vda1 /mnt
+for d in dev proc sys run; do mount --rbind /$d /mnt/$d; mount --make-rslave /mnt/$d; done
+chroot /mnt
+
+# fstab: the root line must point at the new Btrfs filesystem
+blkid -s UUID -o value /dev/vda1 # filesystem UUID (not PARTUUID / UUID_SUB)
+cp -a /etc/fstab /etc/fstab.bak
+vi /etc/fstab
+findmnt --verify
+
+grub-install --target=i386-pc --recheck --modules="part_gpt btrfs" /dev/vda
+update-grub
+exit
+umount -R /mnt
+reboot
+```
+
+The `/` line in `/etc/fstab` changes from
+`PARTUUID=… / ext4 rw,discard,errors=remount-ro,x-systemd.growfs 0 1` to:
+
+```fstab
+UUID=<uuid-from-blkid> / btrfs defaults 0 0
+```
+
+The changes are: type `btrfs`, no `errors=remount-ro` (ext4-only), and fsck pass `0`.
+Leave `/boot/efi` alone.
+
+Why each change matters:
+- **fstab:** after conversion, the root line still says `ext4` with the old identifier, which no
+ longer matches the filesystem.
+- **GRUB:** without the reinstall, the VM stops at `grub rescue>` with `error: unknown filesystem`.
+If anything goes wrong, rollback from recovery is `btrfs-convert -r /dev/vda1` (only while
+`ext2_saved` still exists).
+
+---
+
+## Phase 2 — You, on Debian (root is now Btrfs)
+
+### 2.1 Verify the operator's changes and make root writable
+
+```sh
+findmnt -no SOURCE,FSTYPE,OPTIONS / # expect /dev/vda1 btrfs … (may be ro, see below)
+grep ' / ' /etc/fstab # expect UUID=… / btrfs defaults 0 0
+blkid -s UUID -o value /dev/vda1 # write this down; config.scm needs it
+```
+
+**Known open issue:** after the conversion, root came up read-only on every boot of the Debian
+system, and the cause is still unknown. Logging in still worked. Remount it read-write by device,
+which works and is needed before any of the steps below:
+
+```sh
+mount -o remount,rw /dev/vda1 /
+findmnt -no OPTIONS / # now rw
+```
+
+Do this again after any reboot while still on Debian. Once you're running Guix (Phase 3), it
+no longer applies.
+
+### 2.2 Install the Guix package manager on Debian
+
+```sh
+apt update
+apt install uidmap btrfs-progs git # uidmap provides newgidmap, needed by the installer
+wget https://guix.gnu.org/guix-install.sh
+chmod +x guix-install.sh
+./guix-install.sh
+exit # log in again so the Guix profile is loaded
+```
+
+### 2.3 Add swap (`guix pull` gets OOM-killed on a small VPS)
+
+```sh
+btrfs filesystem mkswapfile --size 500M /swapfile
+chmod 600 /swapfile
+swapon /swapfile
+free -h
+guix pull
+```
+
+500 MB was enough. This swapfile is deleted with the Debian files in Phase 4.
+
+### 2.4 Get the cloud-init channel and the system config
+
+```sh
+git clone https://git.infiniteadaptability.org/git/channel ~/channel
+vi config.scm # see section 2.6
+```
+
+### 2.5 Create the `@guix` subvolume and mount it
+
+```sh
+mkdir -p /mnt/btrfs-top
+mount -o subvolid=5 /dev/vda1 /mnt/btrfs-top # Btrfs top level
+btrfs subvolume create /mnt/btrfs-top/@guix
+btrfs subvolume list /mnt/btrfs-top # expect ext2_saved and @guix
+umount /mnt/btrfs-top
+
+mkdir -p /mnt/guix
+mount -o subvol=@guix /dev/vda1 /mnt/guix
+```
+
+Don't point Debian's fstab at `@guix`. Debian keeps running from the top level until the reboot.
+
+### 2.6 `config.scm` — things to check before installing
+
+Start from `config.scm` in this directory and change:
+
+- **`file-system` UUID**: the UUID from step 2.1, with `(options "subvol=@guix")`.
+- **Bootloader**: `grub-bootloader` (BIOS) with `(targets '("/dev/vda"))`, the whole disk.
+- **SSH key**: put your public key directly into `openssh-configuration`:
+ `(authorized-keys `(("root" ,(plain-file "auth-key" "ssh-ed25519 AAAA… you@host"))))`.
+ The `openssh-authorized-keys` build failed several times before the key was given this way
+ as one exact public-key line. If it fails, the build log is in
+ `/var/log/guix/drvs/…-openssh-authorized-keys.drv.gz`.
+- **SSH policy**: `permit-root-login 'prohibit-password` and `password-authentication? #f`.
+ Key login is the only way in after reboot, so get the key right.
+- **Networking**: `network-manager-service-type` (DHCP).
+- **cloud-init**: the `99-digitalocean.cfg` datasource list is specific to DigitalOcean.
+ Change or remove it to suit the provider.
+- **host-name**, **timezone**, **locale** as you like.
+
+### 2.7 Install Guix System into the subvolume
+
+```sh
+df -h # check free space (~8 GB was enough)
+guix system -L ~/channel/ init config.scm /mnt/guix
+```
+
+This builds the system into `/mnt/guix` and installs Guix's GRUB to `/dev/vda`, which replaces
+Debian's. If it fails, fix `config.scm` and run it again. Each run reuses what is already built.
+
+---
+
+## Phase 3 — Reboot into Guix System
+
+```sh
+reboot
+```
+
+Then log in over SSH as root with your key and check:
+
+```sh
+guix system describe
+ip addr
+ping -c3 gnu.org
+df -h
+```
+
+---
+
+## Phase 4 — Remove the old Debian data
+
+Guix runs from `@guix`, but the old Debian files are still at the Btrfs top level (subvolid 5),
+along with the `ext2_saved` ext4 image (a single `image` file the size of the disk). Neither
+shows up from Guix's `/`.
+
+```sh
+mkdir -p /mnt/btrfs-top
+mount -o subvolid=5 /dev/vda1 /mnt/btrfs-top
+btrfs subvolume list /mnt/btrfs-top
+ls -lha /mnt/btrfs-top # Debian dirs + @guix + ext2_saved
+
+# (optional) look at the old system first
+mkdir -p /mnt/old-debian
+mount -o ro,subvol=ext2_saved /dev/vda1 /mnt/old-debian && ls -lha /mnt/old-debian
+umount /mnt/old-debian
+
+# point of no return: removes the ext4 rollback copy
+btrfs subvolume delete /mnt/btrfs-top/ext2_saved
+
+# delete everything at the top level except @guix (includes the old /swapfile)
+find /mnt/btrfs-top -mindepth 1 -maxdepth 1 ! -name '@guix' ! -name 'ext2_saved' \
+ -exec rm -rf -- {} +
+
+ls -lha /mnt/btrfs-top # only @guix should remain
+umount /mnt/btrfs-top
+df -h # space reclaimed
+reboot
+```
+
+Once it comes back up, you're done: Guix System running from `@guix`, with Debian gone.
+
+---
+
+## Troubleshooting from the test run
+
+| Symptom | Cause / fix |
+|---------------------------------------------------------|-------------|
+| `grub rescue>` / `error: unknown filesystem` | GRUB wasn't reinstalled with Btrfs. Rerun Phase 1 `grub-install` + `update-grub`. |
+| `grub-install: cannot backup … Read-only file system` | `/mnt` was mounted read-only. Run `mount -o remount,rw /mnt` before the chroot. |
+| `grub-install: More than one install device?` | The disk was given twice / stray argument. Give `/dev/vda` exactly once. |
+| Root always mounts read-only on Debian after conversion | **Unresolved; cause unknown.** Workaround: `mount -o remount,rw /dev/vda1 /` (step 2.1). |
+| `mount: /: can't find PARTUUID=` | Plain `mount -o remount,rw /` looks up a bad fstab entry. Name the device: `mount -o remount,rw /dev/vda1 /`. |
+| Guix installer complains about `newgidmap` | `apt install uidmap`. |
+| `guix pull` / `guix system init` gets killed | Out of memory. Add the swapfile (step 2.3). |
+| `openssh-authorized-keys.drv` build fails | Malformed key string in `config.scm`. Check the drv log and use one exact public-key line. |
+| Disk still full after booting Guix | Debian + `ext2_saved` are still at subvolid 5. See Phase 4. |