Stop wget-and-Hope Image Downloads: Practical importctl on Linux
You need a container rootfs, a portable service image, a sysext, or a confext on disk. The old habit is familiar:
wget https://example.com/jammy-root.tar.xz
tar -C /var/lib/machines/jammy -xf jammy-root.tar.xz
# ... did the checksum match? where did the qcow2 land? who owns this transfer?
That works until you want GPG-verified pulls, qcow2→raw conversion, class-aware install paths, cancellable background transfers, or the same workflow for portable/sysext/confext images instead of only nspawn machines.
importctl (systemd 256+) is the CLI in front of systemd-importd.service. It downloads, imports, and exports disk images into the right image class directory — machines, portables, sysexts, confexts — with optional checksum/signature verification and transfer listing/cancel.
This guide is operational. Commands and options below come from importctl(1), systemd-importd.service(8), and systemd-firstboot(1).
What importctl owns (and what it does not)
| Tool | Job |
|---|---|
wget / curl + tar
|
Blind download and unpack |
machinectl pull-tar / pull-raw
|
Legacy machine-only pull path (still around on many hosts) |
importctl |
Class-aware pull/import/export via systemd-importd
|
systemd-nspawn / machinectl
|
Boot and manage machine images once they are on disk |
portablectl |
Attach/detach portable service images |
systemd-sysext / systemd-confext
|
Merge extension images into the host tree |
systemd-dissect |
Inspect/mount/validate DDIs already on disk |
importctl places images. Other tools activate them.
Image classes and install directories (importctl(1)):
| Class | Short flag | Directory |
|---|---|---|
machine |
-m |
/var/lib/machines/ |
portable |
-P |
/var/lib/portables/ |
sysext |
-S |
/var/lib/extensions/ |
confext |
-C |
/var/lib/confexts/ |
Supported payload shapes:
-
Tar filesystem images (
.tar,.tar.gz,.tar.xz,.tar.bz2, plus zstd on import/export paths) -
Raw / qcow2 block images (optional
.gz/.xz/.bz2; qcow2 is converted to raw on pull) -
OCI container references via
pull-oci(systemd 260+)
Prerequisites
# Debian/Ubuntu — importd/nspawn tooling usually ships with systemd-container
sudo apt install systemd-container
# Fedora
sudo dnf install systemd-container
# Arch — typically part of the main systemd package
sudo pacman -S systemd
command -v importctl
importctl --version
systemctl status systemd-importd.service --no-pager
importctl talks to systemd-importd over D-Bus (org.freedesktop.import1). Privileged image writes need appropriate authorization (typically root or a polkit-allowed admin).
Lab 1 — List what you already have
# Images already present for the selected class (default class is machine-oriented workflows; be explicit)
importctl list-images -m
importctl list-images -P
importctl list-images -S
importctl list-images -C
# JSON for scripts
importctl list-images -m --json=pretty
Empty output is fine on a fresh host. After pulls/imports, this is your inventory.
Lab 2 — Pull a verified tar rootfs into machines/
The man page example uses Ubuntu cloud root tarballs. Verification defaults to --verify=signature: integrity via .sha256 / SHA256SUMS, plus GPG against:
-
/usr/lib/systemd/import-pubring.pgp(vendor keyring) -
/etc/systemd/import-pubring.pgp(local trust; falls back to legacy.gpgpath if needed)
# -m → class=machine → /var/lib/machines/
# -N → --keep-download=no (write straight to the local name; default keep-download is true for machines)
sudo importctl pull-tar -mN \
https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz
# Optional explicit local name
sudo importctl pull-tar -mN \
https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz \
jammy-root
Verification modes (--verify=):
| Mode | Behavior |
|---|---|
signature (default) |
Checksum and detached GPG signature |
checksum |
SHA-256 only (.sha256 or SHA256SUMS) |
no |
No download verification (use only for trusted local mirrors you already checked) |
If the remote only publishes checksums without signatures you trust in the import keyring, you will need --verify=checksum (or import a local file after verifying out-of-band).
Ctrl-C does not abort an in-flight transfer. Use transfer management instead:
importctl list-transfer
sudo importctl cancel-transfer <ID>
Lab 3 — Pull a raw disk image, set root password offline, boot it
sudo importctl pull-raw -mN \
https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-disk-kvm.img \
jammy
# Offline first-boot style setup against the image file
sudo systemd-firstboot --image=/var/lib/machines/jammy.raw \
--prompt-root-password --force
# Register/start via machined (same store nspawn uses)
sudo machinectl start jammy
sudo machinectl login jammy
Notes from the man pages:
- Downloaded qcow2 images are converted to raw before they are made available.
-
systemd-firstboot --image=operates on the disk image without booting it — ideal afterpull-raw. - Prefer
--root-password-file=/--root-password-hashed=over putting a plaintext password on the command line when you automate this.
Shell into a tar-style machine image without a full boot:
sudo systemd-nspawn -M jammy-root
# or, after import name derivation from the URL basename:
sudo systemd-nspawn -M jammy-server-cloudimg-amd64-root
Lab 4 — Import local tar/raw/directory trees (no network verification)
Local imports do not run the pull verification path. Verify checksums yourself before import-* if the file came from anywhere untrusted.
# Tar archive → unpacked directory/subvolume under the class dir
sudo importctl import-tar -m ./myroot.tar.xz myroot
# Raw or qcow2 file (compressed ok) → machine image
sudo importctl import-raw -m ./disk.qcow2 mydisk
# Existing directory tree (btrfs snapshot/subvolume when supported)
sudo importctl import-fs -m /srv/build/rootfs myfs
# Read from stdin (NAME is mandatory when FILE is '-')
xz -dc ./myroot.tar.xz | sudo importctl import-tar -m - myroot-stdin
# Force replace an existing local name
sudo importctl import-raw -m --force ./disk.raw mydisk
# Read-only image result
sudo importctl import-tar -m --read-only ./golden.tar.xz golden
Lab 5 — Class-aware pulls for portable / sysext / confext
Same tool, different install root:
# Portable service image → /var/lib/portables/
sudo importctl pull-tar -P --verify=checksum \
https://example.com/images/myapp.tar.xz myapp
# Then attach with portablectl (separate tool/workflow)
# sudo portablectl attach myapp --enable --now
# System extension → /var/lib/extensions/
sudo importctl pull-raw -S --verify=checksum \
https://example.com/images/devtools.raw devtools
# sudo systemd-sysext merge
# Configuration extension → /var/lib/confexts/
sudo importctl pull-tar -C --verify=checksum \
https://example.com/images/site-conf.tar.xz site-conf
# sudo systemd-confext merge
--keep-download= defaults to true for machine, false otherwise. For machines, a successful pull can keep a read-only URL+ETag download and snapshot/copy a writable instance. Pass -N / --keep-download=no when you want a single named image and no retained download template. To download only the read-only template and skip the writable instance, use - as the local name (see pull-tar / pull-raw in importctl(1)).
Lab 6 — Export images for backup or handoff
# Directory/subvolume machine → compressed tar (suffix selects compressor)
sudo importctl export-tar -m fedora ./fedora-backup.tar.xz
# Raw disk machine → raw export (optional compression via suffix or --format=)
sudo importctl export-raw -m jammy ./jammy.raw.xz
# Explicit format when writing to stdout or a suffix-less path
sudo importctl export-tar -m fedora --format=zst - > fedora.tar.zst
Compression formats: uncompressed, xz, gzip, zst, bzip2.
Constraint from the man page: only directory/subvolume images export as tar; only raw disk images export as raw. Match the export command to how the image lives on disk.
Lab 7 — OCI pull (systemd 260+)
importctl --version # need 260+ for pull-oci
# REF is an OCI reference such as library/nginx
sudo importctl pull-oci -m library/nginx nginx
# HTTPS cert checks apply; importctl does not apply the tar/raw GPG verify path to OCI
Use OCI pulls when your upstream already publishes container images and you want them materialized into a systemd image class directory. For classic cloud tarballs and DDIs, prefer pull-tar / pull-raw.
Transfer hygiene and remote operation
# Watch / cancel
importctl list-transfer --json=pretty
sudo importctl cancel-transfer 1234
# Quiet mode for scripts
sudo importctl pull-tar -mN -q --verify=checksum https://mirror.example/root.tar.xz app-root
# Operate against importd in a container, or via SSH host
sudo importctl -M mycontainer list-images -m
sudo importctl -H admin@bastion.example list-images -m
A minimal “pull → boot” recipe you can script
#!/usr/bin/env bash
set -euo pipefail
NAME=jammy-lab
URL=https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64-root.tar.xz
# Replace if re-running
if importctl list-images -m | grep -qw "$NAME"; then
sudo machinectl remove "$NAME" || true
fi
sudo importctl pull-tar -mN --force "$URL" "$NAME"
sudo systemd-nspawn -M "$NAME" -b
For raw KVM-style disks, swap in pull-raw, run systemd-firstboot --image=..., then machinectl start.
Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Signature verification fails | Key not in import pubring | Install vendor key, or use a mirror you verify with --verify=checksum after manual GPG |
| Name already exists | Prior image left behind |
--force, or remove via machinectl / delete under the class directory carefully |
| Ctrl-C “did nothing” | Transfer continues in importd |
importctl list-transfer + cancel-transfer
|
Export-tar rejects a .raw
|
Wrong shape for tar export | Use export-raw for raw disk images |
| Permission errors | importd needs privileges | Run authorized as root; check polkit / systemd-importd logs |
| OCI command missing | Older systemd |
pull-oci needs 260+; use tar/raw pulls or upgrade |
journalctl -u systemd-importd.service -b --no-pager
How this fits the rest of the systemd image stack
-
Build images with
mkosi/systemd-repart(or consume vendor cloud images). -
Move them with
importctl(this article). -
Inspect/mount DDIs with
systemd-dissect. -
Run machines with
systemd-nspawn/machinectlor VMs withsystemd-vmspawn. -
Attach portables with
portablectl; merge sysext/confext withsystemd-sysext/systemd-confext. -
Update fleets with
systemd-sysupdatewhen you are doing A/B image transfers rather than one-off imports.
importctl is the missing “get the bits into the right directory, verified and cancellable” layer — not a replacement for boot or extension activation.
References
-
importctl(1)— pull/import/export commands, classes, verify, keep-download (Arch man page) -
systemd-importd.service(8)— backend service and D-Bus API pointer (Arch man page) -
systemd-firstboot(1)— offline--image=initialization afterpull-raw(Arch man page) -
systemd-nspawn(1)/machinectl(1)— boot and manage machine-class images -
portablectl(1),systemd-sysext(8),systemd-confext(8)— activate non-machine classes - Ubuntu cloud images (example URLs used above): https://cloud-images.ubuntu.com/
If your workflow still starts with bare wget into /var/lib/machines, try one importctl pull-tar -mN with signature verification enabled. Same end state — better transfer control, class routing, and fewer “which tarball was this?” moments.
Top comments (0)