Debian's official upgrade procedure is essentially "swap the codename in your apt sources, then apt full-upgrade." Below are the one-liners I actually run, ordered newest to oldest. All commands are run as root (or under sudo).

Before you start, on any release:

apt update
apt upgrade -y            # fully up-to-date on the current release first
apt autoremove --purge -y # clean out old kernels / unused libs

And run the upgrade itself inside screen or tmux — if SSH drops mid-full-upgrade, you want the process to keep going.

Debian 12 → 13 (bookworm → trixie)

sed -i 's/bookworm/trixie/g' /etc/apt/sources.list
sed -i 's/bookworm/trixie/g' /etc/apt/sources.list.d/*

apt update
apt full-upgrade -y

bookworm-security and bookworm-updates get rewritten to trixie-security / trixie-updates by the same sed — the codename change covers everything.

Post-upgrade: `Missing Signed-By` notices on trixie

After the jump to trixie, apt update starts printing notices like this:

Notice: Missing Signed-By in the sources.list(5) entry for 'mirror+file:/etc/apt/mirrors/debian.list'
Notice: Missing Signed-By in the sources.list(5) entry for 'mirror+file:/etc/apt/mirrors/debian.list'
Notice: Missing Signed-By in the sources.list(5) entry for 'mirror+file:/etc/apt/mirrors/debian.list'
Notice: Missing Signed-By in the sources.list(5) entry for 'mirror+file:/etc/apt/mirrors/debian-security.list'

Nothing is broken — apt is still verifying packages against the trusted Debian keyring. trixie ships apt 3.0, which now warns whenever a deb822 source stanza doesn't pin its signing key explicitly with a Signed-By: field. The same stanza on bookworm (apt 2.6) was silent, so the upgrade is what surfaces it. The cloud-image debian.sources uses mirror+file: URIs and omits the key, so it's the one that complains.

apt modernize-sources does not fix this — the file is already deb822, so that command is a no-op. The Signed-By: field has to be added. The correct key for stock Debian is /usr/share/keyrings/debian-archive-keyring.gpg (already present on the system):

grep -q '^Signed-By:' /etc/apt/sources.list.d/debian.sources || \
  sudo sed -i '/^Components:/a Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg' \
    /etc/apt/sources.list.d/debian.sources
apt update

The grep guard makes it idempotent and the sed appends the key line after every Components: line, so both the main and -security stanzas get pinned in one shot. Re-run apt update and the notices are gone.

Debian 11 → 12 (bullseye → bookworm)

sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list.d/*

apt update
apt full-upgrade -y

Same shape as 12 → 13 — bullseye → bookworm is a straight codename swap, no security-repo URL surgery needed.

Debian 10 → 11 (buster → bullseye)

This is the one with extra steps. Between Debian 10 and 11 the security suite URL and the suite name both changed:

  • Old (buster): http://security.debian.org/debian-security buster/updates main
  • New (bullseye): https://deb.debian.org/debian-security bullseye-security main

So a naive sed buster→bullseye leaves you with a broken security line. Replace it explicitly:

sed -i 's/buster/bullseye/g' /etc/apt/sources.list
sed -i 's/buster/bullseye/g' /etc/apt/sources.list.d/*

# Append the new-style security suite
echo "deb https://deb.debian.org/debian-security bullseye-security main contrib" >> /etc/apt/sources.list
echo "deb-src https://deb.debian.org/debian-security bullseye-security main contrib" >> /etc/apt/sources.list

# Strip the stale buster-style security lines that sed left behind
sed -i 's|deb-src http://security.debian.org/debian-security bullseye/updates main||g' /etc/apt/sources.list
sed -i 's|deb http://security.debian.org/debian-security bullseye/updates main||g' /etc/apt/sources.list

apt update
apt full-upgrade -y

After the upgrade settles, open /etc/apt/sources.list and tidy up — the strip-step above leaves blank lines where the old security entries were.

After every upgrade

apt autoremove --purge -y    # drop transitional / orphaned packages
apt clean
reboot                       # new kernel needs it

Check uname -r and cat /etc/os-release after reboot to confirm the new release is actually running.

Worth knowing

  • Back up first. A VM snapshot or filesystem-level backup before kicking off full-upgrade is cheap and saves you from a botched dist-upgrade. Drop it once the system is verified post-reboot.
  • Read the release notes — really. Each Debian release has breaking changes worth skimming before you upgrade: 10 → 11, 11 → 12, 12 → 13.
  • Skipping releases isn't supported. Officially you upgrade one major release at a time (10 → 11 → 12 → 13). It often works end-to-end, but if something breaks, "I skipped a version" is the first thing anyone will ask.
  • Watch for dpkg config prompts. full-upgrade will pause and ask whether to keep your local config or take the maintainer's version for modified files (Y, N, Diff, Zshell). Keep your local copy unless you have a reason not to — diffing first is free.
  • apt full-upgrade, not apt upgrade. A plain apt upgrade won't remove or replace packages, which is exactly what a major-release jump needs to do. full-upgrade (the modern name for dist-upgrade) is correct here.
  • Third-party repos break. Anything in /etc/apt/sources.list.d/ pinned to the previous codename — Docker, PostgreSQL, Node, etc. — gets rewritten by the sed too, which might point you at a suite the vendor hasn't published yet. Comment those out before the upgrade, re-enable them with the right suite afterwards.
  • /etc/apt/sources.list may not exist on newer installs. Debian 12+ installers can emit deb822 format files in /etc/apt/sources.list.d/debian.sources instead of the legacy sources.list. The sed against sources.list.d/* covers both, but sources.list itself may be empty or absent — that's fine.

References