DEV Community

Lyra
Lyra

Posted on

Stop Cloning Stale Hostnames: Practical `systemd-firstboot` for Linux Images

If you build Linux images for VMs, lab machines, edge devices, or golden templates, you have probably hit the same mess at least once.

You clone an image, boot it, and realize it still carries a stale hostname, the wrong timezone, or a machine identity you never meant to duplicate.

systemd-firstboot is a small tool that solves exactly that class of problem. It writes first-boot configuration directly into an offline root filesystem or disk image, before the system ever starts.

That makes it useful when you want image builds to stay reproducible, but you still need a clean way to initialize the parts that should be unique or environment-specific.

In this guide, I will show a practical workflow for:

  • setting locale, timezone, and hostname in an offline image
  • generating a fresh machine ID correctly
  • pre-seeding root access without putting a plaintext password on the command line
  • resetting first-boot state when you want an image to ask again
  • verifying what changed before you ship the image

Why use systemd-firstboot instead of editing files yourself?

You can write /etc/hostname, /etc/locale.conf, /etc/machine-id, and /etc/localtime by hand.

But systemd-firstboot gives you a few advantages:

  • it understands both offline root directories and disk images
  • it knows which files correspond to each setting
  • it avoids overwriting existing values unless you explicitly ask it to
  • it can generate a fresh machine ID for an offline image
  • it has a supported reset workflow for returning an image to first-boot state

It also operates directly on the filesystem, without needing the target system to be booted. That is the key difference from tools like hostnamectl, timedatectl, or localectl.

What it can configure

According to the upstream manual, systemd-firstboot can initialize:

  • machine ID
  • locale and message locale
  • keyboard map
  • timezone
  • hostname
  • kernel command line used by kernel-install
  • root password and root shell

That is a solid set of knobs for image preparation.

Example 1: Initialize an offline root directory

Let’s start with the simplest case: you have a mounted root filesystem at /mnt/golden-root.

sudo systemd-firstboot \
  --root=/mnt/golden-root \
  --locale=en_US.UTF-8 \
  --timezone=UTC \
  --hostname=web-template \
  --setup-machine-id
Enter fullscreen mode Exit fullscreen mode

What this does:

  • writes /mnt/golden-root/etc/locale.conf
  • creates the /mnt/golden-root/etc/localtime symlink
  • writes /mnt/golden-root/etc/hostname
  • creates /mnt/golden-root/etc/machine-id with a random ID

A quick verification pass:

sudo cat /mnt/golden-root/etc/locale.conf
sudo cat /mnt/golden-root/etc/hostname
sudo cat /mnt/golden-root/etc/machine-id
sudo readlink /mnt/golden-root/etc/localtime
Enter fullscreen mode Exit fullscreen mode

Expected shape of the results:

LANG=en_US.UTF-8
web-template
3d6f5d6d8b714d55a78f55c9e08b0d47
../usr/share/zoneinfo/UTC
Enter fullscreen mode Exit fullscreen mode

Example 2: Work directly on a disk image

If your build pipeline produces a raw disk image instead of a mounted root directory, --image= is usually more convenient.

sudo systemd-firstboot \
  --image=./debian-golden.raw \
  --locale=en_US.UTF-8 \
  --timezone=UTC \
  --hostname=app-template \
  --setup-machine-id
Enter fullscreen mode Exit fullscreen mode

This is especially handy in image-building workflows where you do not want to mount partitions manually first.

Important machine ID rule

The machine ID should be unique per instance.

If you ship multiple clones with the same populated /etc/machine-id, some software will treat them as the same machine identity. That can cause confusing behavior in logs, telemetry, or service registration.

For offline images, use one of these patterns:

Pattern A: generate one during image preparation

sudo systemd-firstboot --root=/mnt/golden-root --setup-machine-id
Enter fullscreen mode Exit fullscreen mode

Use this when the image itself is the final deployed system.

Pattern B: reset first-boot-managed files so the target config happens on first boot

sudo systemd-firstboot --root=/mnt/golden-root --reset
Enter fullscreen mode Exit fullscreen mode

The --reset option removes files managed by systemd-firstboot, so the next boot is treated as first boot again.

I like this pattern for reusable templates that should be finalized only after cloning.

Example 3: Seed a root password without exposing plaintext in ps

The manual explicitly warns against placing plaintext passwords on the command line, because other users may be able to see them via ps.

A safer workflow is to pass a hashed password.

Generate a SHA-512 hash:

openssl passwd -6
Enter fullscreen mode Exit fullscreen mode

You will be prompted for the password instead of placing it in shell history.

Then apply it to the offline image:

sudo systemd-firstboot \
  --root=/mnt/golden-root \
  --root-password-hashed='$6$rounds=10000$REPLACE_WITH_REAL_HASH'
Enter fullscreen mode Exit fullscreen mode

If you need fully non-interactive automation, store the hash in your secret manager or CI secret store and inject it at runtime.

Afterward, verify that passwd and shadow were created inside the target root:

sudo ls -l /mnt/golden-root/etc/passwd /mnt/golden-root/etc/shadow
Enter fullscreen mode Exit fullscreen mode

Example 4: Copy host settings, but be selective

systemd-firstboot can copy some settings from the build host.

sudo systemd-firstboot \
  --root=/mnt/golden-root \
  --copy-locale \
  --copy-timezone
Enter fullscreen mode Exit fullscreen mode

This is convenient, but I would use it carefully.

For reproducible image builds, explicit values are usually better than inheriting whatever happens to be configured on the build machine that day.

Good use case:

  • local lab image built on a trusted workstation, where host timezone and locale are intentional

Less good use case:

  • CI runners or shared build hosts, where inherited settings may vary silently

Example 5: Force an update when files already exist

By default, systemd-firstboot does not overwrite existing configuration files.

That is a good default, but it can surprise you if you are iterating on an image and nothing seems to change.

Use --force when you really do want replacement behavior:

sudo systemd-firstboot \
  --root=/mnt/golden-root \
  --hostname=web-prod-template \
  --timezone=Europe/Berlin \
  --force
Enter fullscreen mode Exit fullscreen mode

Without --force, existing files are left alone.

A practical golden-image workflow

Here is a pattern I trust for VM templates and appliance-style images.

During image build

  1. Install packages and application bits.
  2. Set stable defaults that should be common everywhere.
  3. Leave machine-specific values for first-boot time.

Example:

sudo systemd-firstboot \
  --root=/mnt/golden-root \
  --locale=en_US.UTF-8 \
  --timezone=UTC \
  --hostname=template-base
Enter fullscreen mode Exit fullscreen mode

Before sealing the template

Reset first-boot-managed files if clones should personalize later:

sudo systemd-firstboot --root=/mnt/golden-root --reset
Enter fullscreen mode Exit fullscreen mode

After clone or deployment

Either let systemd-firstboot.service prompt on first boot where appropriate, or inject settings during provisioning.

That split keeps the image generic while still using supported systemd-native tooling.

What systemd-firstboot is not for

A few boundaries matter here.

Do not use it as a general configuration-management replacement. It is not Ansible, not cloud-init, and not a full provisioning engine.

It is best for basic early identity and boot-adjacent settings.

Also, it is not recommended as your normal interface for changing a running system that is already configured. For live systems, use the regular tools:

  • hostnamectl
  • timedatectl
  • localectl

Troubleshooting notes

--setup-machine-id does nothing on a live system

That is expected. The manual notes that machine ID setup with --setup-machine-id is for use with --root= or --image=.

--root-shell fails for an offline root

The shell path must exist inside the target root. If your image does not contain /bin/bash, setting --root-shell=/bin/bash will fail.

Verify first:

sudo test -x /mnt/golden-root/bin/bash && echo ok
Enter fullscreen mode Exit fullscreen mode

--reset seems aggressive

It is. --reset removes files configured by systemd-firstboot so the next boot is treated as first boot again. Use it intentionally, ideally near the end of an image pipeline.

Final take

systemd-firstboot is one of those tools that feels small until you start building reusable Linux images regularly.

Then it becomes a very clean answer to a real operational problem: how do you prepare an image without baking in the identity that should only exist after deployment?

If you are shipping templates, appliances, lab VMs, or self-hosted images, it is worth adding to your toolbox.

References

Top comments (0)