Stop Cloning Stale Hostnames: Practical `systemd-firstboot` for Linux Images
Lyra
·
2026-05-01
·
via DEV Community
<p>If you build Linux images for VMs, lab machines, edge devices, or golden templates, you have probably hit the same mess at least once.</p> <p>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.</p> <p><code>systemd-firstboot</code> 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.</p> <p>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.</p> <p>In this guide, I will show a practical workflow for:</p> <ul> <li>setting locale, timezone, and hostname in an offline image</li> <li>generating a fresh machine ID correctly</li> <li>pre-seeding root access without putting a plaintext password on the command line</li> <li>resetting first-boot state when you want an image to ask again</li> <li>verifying what changed before you ship the image</li> </ul> <h2> Why use <code>systemd-firstboot</code> instead of editing files yourself? </h2> <p>You <em>can</em> write <code>/etc/hostname</code>, <code>/etc/locale.conf</code>, <code>/etc/machine-id</code>, and <code>/etc/localtime</code> by hand.</p> <p>But <code>systemd-firstboot</code> gives you a few advantages:</p> <ul> <li>it understands both offline root directories and disk images</li> <li>it knows which files correspond to each setting</li> <li>it avoids overwriting existing values unless you explicitly ask it to</li> <li>it can generate a fresh machine ID for an offline image</li> <li>it has a supported reset workflow for returning an image to first-boot state</li> </ul> <p>It also operates directly on the filesystem, without needing the target system to be booted. That is the key difference from tools like <code>hostnamectl</code>, <code>timedatectl</code>, or <code>localectl</code>.</p> <h2> What it can configure </h2> <p>According to the upstream manual, <code>systemd-firstboot</code> can initialize:</p> <ul> <li>machine ID</li> <li>locale and message locale</li> <li>keyboard map</li> <li>timezone</li> <li>hostname</li> <li>kernel command line used by <code>kernel-install</code> </li> <li>root password and root shell</li> </ul> <p>That is a solid set of knobs for image preparation.</p> <h2> Example 1: Initialize an offline root directory </h2> <p>Let’s start with the simplest case: you have a mounted root filesystem at <code>/mnt/golden-root</code>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="se">\</span> <span class="nt">--locale</span><span class="o">=</span>en_US.UTF-8 <span class="se">\</span> <span class="nt">--timezone</span><span class="o">=</span>UTC <span class="se">\</span> <span class="nt">--hostname</span><span class="o">=</span>web-template <span class="se">\</span> <span class="nt">--setup-machine-id</span> </code></pre> </div> <p>What this does:</p> <ul> <li>writes <code>/mnt/golden-root/etc/locale.conf</code> </li> <li>creates the <code>/mnt/golden-root/etc/localtime</code> symlink</li> <li>writes <code>/mnt/golden-root/etc/hostname</code> </li> <li>creates <code>/mnt/golden-root/etc/machine-id</code> with a random ID</li> </ul> <p>A quick verification pass:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo cat</span> /mnt/golden-root/etc/locale.conf <span class="nb">sudo cat</span> /mnt/golden-root/etc/hostname <span class="nb">sudo cat</span> /mnt/golden-root/etc/machine-id <span class="nb">sudo readlink</span> /mnt/golden-root/etc/localtime </code></pre> </div> <p>Expected shape of the results:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>LANG=en_US.UTF-8 web-template 3d6f5d6d8b714d55a78f55c9e08b0d47 ../usr/share/zoneinfo/UTC </code></pre> </div> <h2> Example 2: Work directly on a disk image </h2> <p>If your build pipeline produces a raw disk image instead of a mounted root directory, <code>--image=</code> is usually more convenient.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--image</span><span class="o">=</span>./debian-golden.raw <span class="se">\</span> <span class="nt">--locale</span><span class="o">=</span>en_US.UTF-8 <span class="se">\</span> <span class="nt">--timezone</span><span class="o">=</span>UTC <span class="se">\</span> <span class="nt">--hostname</span><span class="o">=</span>app-template <span class="se">\</span> <span class="nt">--setup-machine-id</span> </code></pre> </div> <p>This is especially handy in image-building workflows where you do not want to mount partitions manually first.</p> <h2> Important machine ID rule </h2> <p>The machine ID should be unique per instance.</p> <p>If you ship multiple clones with the same populated <code>/etc/machine-id</code>, some software will treat them as the same machine identity. That can cause confusing behavior in logs, telemetry, or service registration.</p> <p>For offline images, use one of these patterns:</p> <h3> Pattern A: generate one during image preparation </h3> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="nt">--setup-machine-id</span> </code></pre> </div> <p>Use this when the image itself is the final deployed system.</p> <h3> Pattern B: reset first-boot-managed files so the target config happens on first boot </h3> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="nt">--reset</span> </code></pre> </div> <p>The <code>--reset</code> option removes files managed by <code>systemd-firstboot</code>, so the next boot is treated as first boot again.</p> <p>I like this pattern for reusable templates that should be finalized only after cloning.</p> <h2> Example 3: Seed a root password without exposing plaintext in <code>ps</code> </h2> <p>The manual explicitly warns against placing plaintext passwords on the command line, because other users may be able to see them via <code>ps</code>.</p> <p>A safer workflow is to pass a hashed password.</p> <p>Generate a SHA-512 hash:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code>openssl passwd <span class="nt">-6</span> </code></pre> </div> <p>You will be prompted for the password instead of placing it in shell history.</p> <p>Then apply it to the offline image:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="se">\</span> <span class="nt">--root-password-hashed</span><span class="o">=</span><span class="s1">'$6$rounds=10000$REPLACE_WITH_REAL_HASH'</span> </code></pre> </div> <p>If you need fully non-interactive automation, store the hash in your secret manager or CI secret store and inject it at runtime.</p> <p>Afterward, verify that <code>passwd</code> and <code>shadow</code> were created inside the target root:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo ls</span> <span class="nt">-l</span> /mnt/golden-root/etc/passwd /mnt/golden-root/etc/shadow </code></pre> </div> <h2> Example 4: Copy host settings, but be selective </h2> <p><code>systemd-firstboot</code> can copy some settings from the build host.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="se">\</span> <span class="nt">--copy-locale</span> <span class="se">\</span> <span class="nt">--copy-timezone</span> </code></pre> </div> <p>This is convenient, but I would use it carefully.</p> <p>For reproducible image builds, explicit values are usually better than inheriting whatever happens to be configured on the build machine that day.</p> <p>Good use case:</p> <ul> <li>local lab image built on a trusted workstation, where host timezone and locale are intentional</li> </ul> <p>Less good use case:</p> <ul> <li>CI runners or shared build hosts, where inherited settings may vary silently</li> </ul> <h2> Example 5: Force an update when files already exist </h2> <p>By default, <code>systemd-firstboot</code> does <strong>not</strong> overwrite existing configuration files.</p> <p>That is a good default, but it can surprise you if you are iterating on an image and nothing seems to change.</p> <p>Use <code>--force</code> when you really do want replacement behavior:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="se">\</span> <span class="nt">--hostname</span><span class="o">=</span>web-prod-template <span class="se">\</span> <span class="nt">--timezone</span><span class="o">=</span>Europe/Berlin <span class="se">\</span> <span class="nt">--force</span> </code></pre> </div> <p>Without <code>--force</code>, existing files are left alone.</p> <h2> A practical golden-image workflow </h2> <p>Here is a pattern I trust for VM templates and appliance-style images.</p> <h3> During image build </h3> <ol> <li>Install packages and application bits.</li> <li>Set stable defaults that should be common everywhere.</li> <li>Leave machine-specific values for first-boot time.</li> </ol> <p>Example:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="se">\</span> <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="se">\</span> <span class="nt">--locale</span><span class="o">=</span>en_US.UTF-8 <span class="se">\</span> <span class="nt">--timezone</span><span class="o">=</span>UTC <span class="se">\</span> <span class="nt">--hostname</span><span class="o">=</span>template-base </code></pre> </div> <h3> Before sealing the template </h3> <p>Reset first-boot-managed files if clones should personalize later:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo </span>systemd-firstboot <span class="nt">--root</span><span class="o">=</span>/mnt/golden-root <span class="nt">--reset</span> </code></pre> </div> <h3> After clone or deployment </h3> <p>Either let <code>systemd-firstboot.service</code> prompt on first boot where appropriate, or inject settings during provisioning.</p> <p>That split keeps the image generic while still using supported systemd-native tooling.</p> <h2> What <code>systemd-firstboot</code> is not for </h2> <p>A few boundaries matter here.</p> <p>Do <strong>not</strong> use it as a general configuration-management replacement. It is not Ansible, not cloud-init, and not a full provisioning engine.</p> <p>It is best for basic early identity and boot-adjacent settings.</p> <p>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:</p> <ul> <li><code>hostnamectl</code></li> <li><code>timedatectl</code></li> <li><code>localectl</code></li> </ul> <h2> Troubleshooting notes </h2> <h3> <code>--setup-machine-id</code> does nothing on a live system </h3> <p>That is expected. The manual notes that machine ID setup with <code>--setup-machine-id</code> is for use with <code>--root=</code> or <code>--image=</code>.</p> <h3> <code>--root-shell</code> fails for an offline root </h3> <p>The shell path must exist inside the target root. If your image does not contain <code>/bin/bash</code>, setting <code>--root-shell=/bin/bash</code> will fail.</p> <p>Verify first:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight shell"><code><span class="nb">sudo test</span> <span class="nt">-x</span> /mnt/golden-root/bin/bash <span class="o">&&</span> <span class="nb">echo </span>ok </code></pre> </div> <h3> <code>--reset</code> seems aggressive </h3> <p>It is. <code>--reset</code> removes files configured by <code>systemd-firstboot</code> so the next boot is treated as first boot again. Use it intentionally, ideally near the end of an image pipeline.</p> <h2> Final take </h2> <p><code>systemd-firstboot</code> is one of those tools that feels small until you start building reusable Linux images regularly.</p> <p>Then it becomes a very clean answer to a real operational problem: how do you prepare an image <em>without</em> baking in the identity that should only exist after deployment?</p> <p>If you are shipping templates, appliances, lab VMs, or self-hosted images, it is worth adding to your toolbox.</p> <h2> References </h2> <ul> <li>systemd upstream manual, <code>systemd-firstboot(1)</code>: <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html" rel="noopener noreferrer">https://www.freedesktop.org/software/systemd/man/latest/systemd-firstboot.html</a> </li> <li>Debian manpage mirror for <code>systemd-firstboot(1)</code>: <a href="https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html" rel="noopener noreferrer">https://manpages.debian.org/bookworm/systemd/systemd-firstboot.1.en.html</a> </li> <li>ArchWiki overview: <a href="https://wiki.archlinux.org/title/Systemd-firstboot" rel="noopener noreferrer">https://wiki.archlinux.org/title/Systemd-firstboot</a> </li> </ul>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。