ubuntu – migrate packages from one server as a source to target server

root@source: ~/migration/packages
root@source:~# dpkg –get-selections > selections.list # the whole inventory. the precise list comes later

ubuntu – migrate packages from one server as a source to target server

Updated for 2026: the 2018 procedure, fixed and finished. The problem is real — you have a server you built up over years, and you need the same package set on a new box — but “copy the package list” has three traps in it: the list is 2,000 lines of dependencies, the sources and keys do not travel with it, and half the software on a modern Ubuntu box is not a .deb at all. This is the procedure that gets all of it right, same release to same release.

# the objective — what you are actually moving
packages only. This moves the installed package set: which packages, and which are held. It does not move configuration in /etc, data in /var, user accounts, cron jobs, or services. Those are a separate migration. If you need those too, move them deliberately, not by accident.
same release to same release. Source and target must run the same Ubuntu release (22.04 to 22.04, 24.04 to 24.04). Package names, transitions, and defaults change between releases — cross-release migration is a different procedure wearing this one’s name. If the releases differ, upgrade the source first, or install fresh on the target and migrate the manual list only.
same architecture. amd64 selections do not transfer to arm64. Check dpkg --print-architecture on both boxes before you start.
# the precise list — what you added, nothing else
the problem with the full dump: dpkg --get-selections lists every installed package — roughly 2,000 lines on a typical server, nearly all dependencies you never chose. That list tells you nothing about intent, and it is the wrong thing to review or to carry across.
the precise method: take what is installed, subtract what the installer put there, subtract what is marked automatic. What is left is what you added. That is the list worth migrating, and it is the heart of this procedure:

sudo comm -13
    <(gzip -dc /var/log/installer/initial-status.gz | sed -n ‘s/^Package: //p’ | sort)
    <(comm -23
        <(dpkg-query -W -f=’${Package}n’ | sort)
        <(apt-mark showauto | sort)
    ) > manual-added.txt

how it reads: the inner comm -23 takes installed packages and removes the automatic ones, leaving everything installed manually. The outer comm -13 removes the packages the installer shipped, leaving only what was added after install. All three inputs are sorted, which comm requires — if it complains about sort order, re-run with LC_ALL=C.
when it does not apply: /var/log/installer/initial-status.gz exists only on systems installed with the classic installer. Cloud images, containers, and VPS images built from snapshots usually do not have it. If the file is missing, use the simple list below instead — do not invent an initial list.

A full selections dump is 2,000 lines that tell you nothing about intent. This list is short enough to read before you trust it — and reading it is the point.
# the simple list — the universal fallback
on the source: apt-mark showmanual | sort > manual.txt — every package marked manually installed. Review the file before you use it: transitional packages and renamed packages between releases will show up here, and a package you do not recognize is a question, not a passenger.
on the target: xargs -a manual.txt sudo apt-get install -y — apt resolves the dependencies itself, so the manual list is enough. No selections file, no dselect step.
which list to use: the precise list when initial-status.gz exists, the manual list everywhere else. Either beats the full dump, because either one you can actually read.
# the full selections method — exact replication, holds included
on the source:
dpkg –get-selections > selections.list
apt-mark showauto > auto.list

on the target (after the sources and keys below are in place):
sudo apt-get update
sudo dpkg –set-selections < selections.list
sudo apt-get dselect-upgrade
xargs -a auto.list sudo apt-mark auto

what this buys you over the manual list: exactness. Selections carry hold states too, so a held package stays held — check them first with dpkg --get-selections | grep -w hold so nothing surprises you at upgrade time. The final apt-mark auto pass restores the automatic markings, so apt autoremove behaves on the target the way it did on the source.
verify: dpkg --get-selections > new-selections.list; diff selections.list new-selections.list — anything missing is a package the target’s repositories do not carry, and that is a decision to make, not an error to ignore.

# sources and keys travel too — the part everyone forgets
copy the files, do not cat the directory. The 2018 draft had cat /etc/apt/sources.list /etc/apt/sources.list.d > sources.list — and sources.list.d is a directory, which does not cat. Copy instead:

cp /etc/apt/sources.list /tmp/mig/ 2>/dev/null
cp /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources /tmp/mig/ 2>/dev/null

two formats, both live: older releases use .list files; Ubuntu 24.04 and later use DEB822 .sources files (usually /etc/apt/sources.list.d/ubuntu.sources). Copy whichever your source has — and on the target, check the suite names match the target release (grep -h '^Suites:' /tmp/mig/*.sources, or the deb lines in .list files). A source file naming the wrong release is how a migration becomes a FrankenDebian.
keys: copy the keyring files your .sources files name in their Signed-By lines, to the same paths on the target (usually under /usr/share/keyrings). Do not use apt-key — it is deprecated and gone on current releases. If a source is a PPA, re-adding it on the target with add-apt-repository is cleaner than copying keys by hand.
then: sudo apt-get update on the target before any install step, and read its output — a source that fails to update is a source that will fail to install from.

# the parts dpkg does not cover — snaps, flatpaks, and everything else
snaps: your package list does not know they exist. On the source: snap list. On the target, install each one again — and check --classic snaps individually, because classic confinement is per-snap, not a flag you can blanket-apply.
flatpaks: same story, if you use them: flatpak list --app on the source, reinstall on the target.
everything else: configuration, data, users, cron jobs, certificates, firewall rules — none of this is in a package list. Migrate those deliberately, and test the services on the target before you point anything at it.
Snaps are not dpkg packages. If your migration plan only mentions dpkg, it is missing the software your users actually open.
# what changed from the 2018 draft — honest corrections
the cat bug: cat ... /etc/apt/sources.list.d cannot work — that path is a directory. Replaced with copying the files.
the dead line: /usr/lib/dpkg/methods/apt/update /var/lib/dpkg/ pointed at a dpkg helper that no longer exists. Dropped — apt-get update covers it.
the stray sed: the original pipeline’s sed 1d deleted the first package in the list. dpkg-query prints no header to skip, so it is gone.
the garbled fragment: the original’s Check_diff / diff.txt block referenced a file the procedure never creates. Replaced with the complete sequences above.
the modern additions: the apt-mark showmanual fallback for systems without initial-status.gz, DEB822 .sources handling, the apt-key deprecation, snaps and flatpaks, and the same-release and same-architecture rules, which the original left unsaid.
what survived: the comm pipeline itself — the precise list is still the core of this post, and it still works on current releases. Verified against apt 2.8, Oct 26, 2026.
The 2018 draft had a line that catted a directory. Directories do not cat. That is the kind of bug a write-up exists to kill.
# quick reference — the whole post in three lines
Source: precise list (comm pipeline) or apt-mark showmanual; copy sources files + Signed-By keyrings; snap list.
Target: place sources + keyrings → apt-get update → dpkg --set-selections → apt-get dselect-upgrade → apt-mark auto pass → reinstall snaps.
Verify: diff the selections files; check holds; test services before cutover. Same release, same architecture, or stop.
Sources: the procedure’s original references — Unix & Linux Stack Exchange answers on the initial-status method and on replicating selections, and the Ask Ubuntu thread on packages installed after OS install — re-checked against current apt behavior, Oct 26, 2026.
Series: GuidesStatus: Published 26 Oct 2026

Scroll to Top