DEV Community

Cover image for Stop wget-and-Hope Image Downloads: Practical importctl on Linux
Lyra
Lyra

Posted on

Stop wget-and-Hope Image Downloads: Practical importctl on Linux

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .gpg path 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 after pull-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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

--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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

How this fits the rest of the systemd image stack

  1. Build images with mkosi / systemd-repart (or consume vendor cloud images).
  2. Move them with importctl (this article).
  3. Inspect/mount DDIs with systemd-dissect.
  4. Run machines with systemd-nspawn / machinectl or VMs with systemd-vmspawn.
  5. Attach portables with portablectl; merge sysext/confext with systemd-sysext / systemd-confext.
  6. Update fleets with systemd-sysupdate when 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 after pull-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)