cd /news/developer-tools/four-debian-13-boxes-one-brief-1923-… · home topics developer-tools article
[ARTICLE · art-125149] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Four Debian 13 Boxes, One Brief: 1,923 Packages on Metal, 328 in the Cloud

A developer benchmarked Debian 13 on four machines—one laptop and three cloud instances from AWS, GCE, and Azure—revealing that cloud images ship with far fewer packages (328-350) than a full install (1,923) and often lack tools like nvme-cli and dmidecode, leaving hardware health unreadable. The test also found that AWS and Azure use the same upstream image, while GCE builds its own, and highlighted a pitfall where cloud-init scripts can skew measurements.

by read14 min views2 publishedSep 9, 2026

This article covers one afternoon of running the same Debian 13 on four machines — one laptop and three clouds — and asking each of them the same question about its own hardware.

A cloud image is not a small Debian. It is a different bet about what you will need. The laptop install carries 1,923 packages. The three cloud images carry 328, 337 and 350. That is not a rounding difference, and the interesting part is not the count — it is that all four are booting from NVMe, and only two of them ship anything that can read an NVMe health log.

What that creates is a gap with two halves. The hardware is present and the kernel can see it. The default toolset cannot. And because nothing fails, nothing tells you:

nvme-cli`` dmidecode and Vulnerability Retbleed: Vulnerable`` GPT:16777215 != 41943039, still wrong at the boot I measured. GCE emits the identical warning and That last pair is the whole article. Same kernel, same message, opposite correct answers.

https://github.com/xbill9/local-bench

Four hosts, all Debian 13 (trixie), all measured on 2026-09-09:

Image Shape CPU Kernel
local installed, bare metal Lenovo 83B1 i7-1360P, Raptor Lake, 16 threads 7.1.8+deb13-amd64 (backports)
AWS ami-0871da4641e8b4413 c7i.large Xeon 8488C, Sapphire Rapids, 2 vCPU 6.12.107+deb13-cloud-amd64
GCE debian-13-trixie-v20260908 c4-standard-2 Xeon 8581C, Emerald Rapids, 2 vCPU 6.12.107+deb13-cloud-amd64
Azure Debian:debian-13:13-gen2:0.20260831.2587 Standard_D2s_v7 Xeon 6973P-C, Granite Rapids, 2 vCPU 6.12.107+deb13-cloud-amd64

One detail decides how to read everything below. The AWS and Azure images are the same upstream Debian build2587, dated 2026-08-31 — running the same kernel. GCE builds its own, a week newer. So where AWS and Azure differ, the distro is not the variable. The platform is.

ssh, and a key you are willing to put on a throwaway host Steps 0 through 2 are reads and launches. Step 3 installs software, and Step 3 is the point of no return for the baseline — see Step 1 for why that ordering is not optional.

An image family is a moving target, and "Debian 13" is not a measurement. Resolve it to an ID before you launch anything, and write the ID down.

aws ec2 describe-images --owners 136693071363 \
  --filters "Name=name,Values=debian-13-amd64-*" "Name=state,Values=available" \
  --query 'sort_by(Images,&CreationDate)[-1].{Name:Name,ID:ImageId,Date:CreationDate}'

gcloud compute images describe-from-family debian-13 \
  --project debian-cloud --format="value(name)"

az vm image list --publisher Debian --offer debian-13 --sku 13-gen2 --all \
  --query "[?offer=='debian-13'] | sort_by(@,&version)[-1].urn" -o tsv

That third one has a trap in it. Asking Azure for --offer debian-13 with --all will happily return debian-13-daily URNs alongside the stable ones, because the filter matches loosely. The [?offer=='debian-13'] clause is doing real work — without it you will pin a daily build and not notice.

This is also where the AWS/Azure coincidence shows up: both resolved to build 2587, dated 2026-08-31. That is not a fluke, it is the same upstream Debian cloud image, and it is the single most useful fact for interpreting the rest.

This step exists because I got it wrong the first time.

The first EC2 instance I launched had a user-data script that installed about twenty packages before I ever logged in. Then I scanned it and wrote down what was present. Every one of those findings was worthless: I was measuring my own cloud-init, not Debian's image. That instance had to be discarded and relaunched with no user-data at all.

A default-install analysis has exactly one chance to be correct, and it is before the first apt-get install.

The scanner has to run on a machine where nothing is installed, which means it can only use what a minimal cloud image actually ships — bash, coreutils, /proc, /sys, dpkg. No jq. No lspci, because pciutils might not be there. Absence is the data, so nothing may fail:

for f in sys_vendor product_name bios_version bios_date; do
  [ -r "/sys/class/dmi/id/$f" ] && printf '%-16s %s\n' "$f" "$(cat /sys/class/dmi/id/$f)"
done

for p in pciutils usbutils dmidecode nvme-cli smartmontools lm-sensors \
         ethtool intel-microcode fwupd build-essential gcc git unzip; do
  s=$(dpkg-query -f '${db:Status-Status}' -W "$p" 2>/dev/null)
  [ "$s" = installed ] && echo "PRESENT $p" || echo "absent  $p"
done

Check packages with dpkg, not with command -v. This is the second thing I got wrong, and it is worth more than the first. My initial pass over the laptop reported dmidecode, nvme-cli, smartmontools, hdparm, ethtool, powertop and turbostat as missing. All seven were installed. My login PATH contained no /usr/sbin, no /sbin, no /usr/local/sbin, and a bare command -v cannot see a binary it has no path to. Seven false negatives from one unstated assumption, on the machine I know best.

The full scanner is evidence/baseline-scan.sh in the repo. Run it, keep the output, then install things.

No user-data, no startup script, no custom image. Lock SSH to your own address rather than the world — every one of these CLIs will happily open port 22 to 0.0.0.0/0 if you let it.

MYIP=$(curl -s https://checkip.amazonaws.com)/32

aws ec2 run-instances --image-id <ami> --instance-type c7i.large \
  --key-name <key> --security-group-ids <sg> --subnet-id <subnet> \
  --associate-public-ip-address --metadata-options 'HttpTokens=required'

gcloud compute instances create baseline --zone=us-east1-b \
  --machine-type=c4-standard-2 --image-family=debian-13 --image-project=debian-cloud

az vm create -g <rg> -n baseline --image <urn> --size Standard_D2s_v7 \
  --admin-username azureuser --ssh-key-values ~/.ssh/id_ed25519.pub --nsg-rule SSH
az network nsg rule update -g <rg> --nsg-name baselineNSG --name default-allow-ssh \
  --source-address-prefixes "$MYIP"

Azure will also refuse a size that is not offered to your subscription in that region, and the CLI reports it badly — mine crashed inside its own error handler with RuntimeError: The content for this response was already consumed, having created nothing. The real cause was that Standard_D2s_v6 does not exist for that subscription in eastus. Ask before you launch:

az vm list-skus -l eastus --size Standard_D2s_v7 \
  --query "[].{Name:name,Restrictions:restrictions[].reasonCode}" -o json

An empty array means the SKU is not available to you. That is the answer the failed deployment was trying to give.

curl -fsSL https://claude.ai/install.sh | bash

On all three clouds, on images with no node, no npm, no unzip and no compiler:

AWS GCE Azure
Elapsed 10 s 8 s 12 s
Version 2.1.266 2.1.266 2.1.266
Installed size 206 MB 206 MB 206 MB

ldd on the binary returns linux-vdso, librt, libc, libpthread, libdl, ld-linux — glibc 2.41 and nothing else. The prerequisites are curl and a writable $HOME, and every one of these images has both.

All three printed the same warning: ~/.local/bin is not in your PATH. Debian's ~/.profile adds that directory only if it exists at login, and the installer creates it mid-session. Three for three, and it resolves itself on the next login.

This is the part that changes the result. Rather than SSH-ing in and running commands from outside, install Claude Code on each box and have it examine the machine it is running on:

claude -p "$(cat brief.txt)" --allowedTools Bash

--allowedTools Bash pre-approves the one tool the job needs. The blunter --permission-mode bypassPermissions disables the permission system wholesale and is the wrong instrument here.

The brief asks for seven things in order — identity, drivers, firmware, kernel errors, failed units, system tools, gaps — and ends by asking for a verdict of at most eight lines. The full text is evidence/deep-analysis-prompt.txt.

One clause in it earned its place:

IMPORTANT: check with an explicit PATH that includes /usr/sbin, /sbin and /usr/local/sbin, and cross-check against dpkg — a tool in /usr/sbin is NOT missing just because a bare command -v under a login PATH cannot see it. This exact mistake has already been made once in this project.

Without it, four machines would have independently reproduced my Step 1 error. With it, the Azure instance flagged ethtool as "the tool a bare login-PATH command -v missed earlier in this project" — it had read the warning and applied it.

One caveat, and it is mine. The brief says "use your Bash tool" and never says read-only. The laptop's instance ran update-initramfs -u during its analysis, rebuilt the boot image, and said so at the top of its report. The result was benign — it incidentally shipped the rtl8156b-2.fw blob that had been missing from that initrd — and the three cloud instances wrote nothing. But that was the brief's luck, not its design. If you reuse this, say read-only.

dpkg-verified, on pristine images, before anything was installed:

Package local AWS GCE Azure
pciutils YES YES YES YES
ethtool YES YES YES YES
curl /wget /python3 YES YES YES YES
dmidecode YES YES
nvme-cli YES YES
smartmontools YES
lm-sensors YES
lshw /usbutils YES
linux-cpupower /powertop YES
intel-microcode /fwupd YES
build-essential /gcc /git YES
numactl /hwloc /gawk
Total installed 1,923 337 350 328

Five packages are universal. Everything else is a choice, and GCE makes the most generous one — it is the only image of the three that can inspect its own disk or decode its own SMBIOS out of the box.

The gap has a second half that no package closes. Some facilities are absent because the hypervisor does not expose them, and installing the tool achieves nothing:

local AWS GCE Azure
/sys/class/hwmon present absent absent absent
thermal_zone* 10 none none none
/sys/class/powercap (RAPL) present empty, modules loaded absent absent
thermal_throttle counters present absent absent absent
EDAC present absent absent absent
TPM present none vTPM present disabled by config
microcode revision 0x6134 0x2b000661 0xffffffff 0xffffffff

Two of these repay a second look.

AWS's RAPL is not simply missing. The instance found intel_rapl_msr and intel_rapl_common loaded and a psys PMU domain registered — KVM is exposing the MSRs — but /sys/class/powercap is empty because the msr module is not loaded and no reader is installed. On AWS, power is recoverable with linux-cpupower and a modprobe. On GCE and Azure, RAPL is not virtualised at all, and no tooling will bring it back. Those two states look identical from a distance and are not the same problem.

GCE and Azure mask the microcode revision to 0xffffffff. AWS reports a real one. If your compliance story involves auditing microcode level in-guest, it works on one of these three.

AWS and GCE both emit this during boot:

GPT:16777215 != 41943039
GPT: Use GNU Parted to correct GPT errors.

16,777,215 sectors is 8 GiB. 41,943,039 is 20 GiB. Both disks were created at 8 GiB and requested at 20; the primary GPT was updated and the backup header at the end of the disk was not moved to match. The kernel falls back to the primary and boots.

On AWS it is still wrong. The instance ranked it as action item #1 and gave the fix:

sudo sgdisk -e /dev/nvme0n1

On GCE the same warning appears at t=1.95s and t=2.23s and is then corrected inside the same boot, because Google ships gce-disk-expand in the image and AWS ships nothing equivalent. Azure never emits it at all.

Same kernel. Same message. On one machine it is a defect you must fix; on another it is a transient you must ignore. Nothing in the message says which, and no amount of reading the message from outside the machine would tell you — you need the boot timeline, the package list and the agent's behaviour together.

Azure is exposed to Retbleed. lscpu reports Vulnerability Retbleed: Vulnerable, tied to the microcode Hyper-V presents to the guest. AWS and GCE report no such thing. There is no in-guest fix; it depends on Azure host-side updates. Azure also has Secure Boot and vTPM available and both disabled on a default VM.

GCE fails a boot step every time. The hwclock binary is absent, so google_guest_agent's clock-skew-setup step fails on every boot. Impact is near zero — kvm-clock is authoritative — but it is a permanent error in a log nobody reads.

Azure has zero unbound PCI devices. It presents three PCI devices, all bound, plus thirteen VMBus devices, all bound. AWS presents about 45 with four unbound legacy stubs (440FX, PIIX3 ISA, PIIX4 ACPI, an emulated VGA); GCE presents 15 with three. Hyper-V Gen2 emulates no legacy chipset at all, and AWS still boots legacy BIOS with an unused EFI System Partition mounted at /boot/efi.

Worth stating plainly, because it is the larger part of the result. On all four machines: kernel taint 0, zero failed systemd units, is-system-running: running, and every PCI device with a real function had its driver bound. On all three clouds: zero firmware-load attempts and zero firmware packages — which is consistent rather than broken, because ena, gve, mana, nvme and virtio request no blobs.

The shared noise is shared exactly: systemd-ssh-generator fails its AF_VSOCK probe every boot on all three clouds, none of which expose a working vsock CID.

aws ec2 describe-images --owners 136693071363 --filters "Name=name,Values=debian-13-amd64-*" \
  --query 'sort_by(Images,&CreationDate)[-1].ImageId'
gcloud compute images describe-from-family debian-13 --project debian-cloud --format="value(name)"
az vm image list --publisher Debian --offer debian-13 --sku 13-gen2 --all \
  --query "[?offer=='debian-13'] | sort_by(@,&version)[-1].urn" -o tsv

az vm list-skus -l eastus --size Standard_D2s_v7 --query "[].name" -o tsv

dpkg-query -f '${db:Status-Status}\n' -W nvme-cli dmidecode smartmontools
dpkg-query -f '.\n' -W | wc -l

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin command -v nvme smartctl turbostat

ls /sys/class/hwmon /sys/class/powercap /sys/devices/system/edac 2>&1
lsmod | grep -c rapl        # loaded modules + empty powercap = recoverable
grep -m1 microcode /proc/cpuinfo   # 0xffffffff = hypervisor is masking it

sudo dmesg | grep -i 'GPT:'
sudo sgdisk -e /dev/nvme0n1        # only if the mismatch persists after boot

curl -fsSL https://claude.ai/install.sh | bash
claude -p "$(cat brief.txt)" --allowedTools Bash

The goal was to find out what a stock Debian 13 cloud image knows about the hardware underneath it. The answer is: less than the hardware is willing to say, and the shortfall is different on every platform, in ways that a package list alone does not predict.

The key to getting a usable answer was giving each machine the same brief and letting it read its own boot log, because the two most interesting findings — a broken partition table on one cloud and a self-correcting one on another — are the same kernel message, and only the machine can tell you which one it is looking at.

The method's own two mistakes are in the article on purpose. Both were unstated assumptions, both produced confident wrong answers, and one of them was on the machine I know best.

Repo: github.com/xbill9/local-benchdocs/debian-across-clouds/ carries the four self-analyses, the pristine baseline scans, the brief, and the benchmark and pricing data. Every evidence file ends with a "NOT measured" section listing which claims are inferences rather than measurements.

── more in #developer-tools 4 stories · sorted by recency
── more on @debian 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/four-debian-13-boxes…] indexed:0 read:14min 2026-09-09 ·