Skip to content

fix(ansible): install systemd-resolved on x86 to fix cold-boot DNS race - #3231

Merged
vpetersson merged 2 commits into
Screenly:masterfrom
vpetersson-bot:fix/dns-boot-race-x86-3230
Aug 5, 2026
Merged

fix(ansible): install systemd-resolved on x86 to fix cold-boot DNS race#3231
vpetersson merged 2 commits into
Screenly:masterfrom
vpetersson-bot:fix/dns-boot-race-x86-3230

Conversation

@vpetersson-bot

@vpetersson-bot vpetersson-bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #3230

Problem

On x86, URL / web-page assets can fail to load after a cold boot and only recover after a manual docker restart.

Root cause

dockerd reads the host resolver config when it creates a network and bakes those nameservers into the container-side embedded DNS (127.0.0.11); it never re-derives them, so if it captures an empty/stale /etc/resolv.conf the containers stay stuck with a dead upstream for the life of that network. A docker restart recreates the network after DNS is up, which is why that workaround appears to fix it.

Docker is ordered After=network-online.target, so in principle it should not start until the network (and DNS) is ready. The problem is that on the stock x86 image nothing actually holds network-online.target back until DNS is written:

  • The primary NIC is configured via ifupdown as allow-hotplug enp2s0 (brought up by dhcpcd).
  • NetworkManager-wait-online.service returns immediately, because NM does not manage that interface (ifupdown owns it — NM reports it unmanaged).
  • ifupdowns networking.service only blocks on auto interfaces; allow-hotplug ones come up asynchronously via udev, so the service completes without waiting for DHCP.

So network-online.target is declared reached while DHCP/DNS is still pending, dockerd starts against an empty /etc/resolv.conf, and the race fires.

(For contrast, the Raspberry Pi images use NetworkManager to manage the link, so NetworkManager-wait-online genuinely blocks network-online.target until NM has written the nameserver — Docker then reads a populated resolv.conf. That is why this is x86-specific.)

Fix

Install and enable systemd-resolved on x86. This removes the dependence on boot ordering entirely:

  • /etc/resolv.conf becomes a static symlink to the 127.0.0.53 stub, which exists from install time and never goes empty — so Docker can never capture an empty resolver list.
  • systemd-resolved tracks the live upstream behind that fixed address, fed by whatever manages the link. This does not require NetworkManager: resolved ships a resolvconf compatibility shim (/sbin/resolvconfresolvectl), so dhcpcds 20-resolv.conf hook (and dhclient, and systemd-networkd natively) push the leased DNS servers into resolved automatically.
  • Docker, seeing a loopback-only resolv.conf, forwards to the real upstreams from /run/systemd/resolve/resolv.conf.

Scoped to x86, where the issue is reported and where the boot ordering demonstrably fails to gate Docker on DNS.

Validation

Reproduced and validated on the x86 testbed (Debian 13, ifupdown + dhcpcd, NetworkManager present but not managing the NIC — i.e. the no-NM DNS path). After a cold reboot with systemd-resolved enabled:

  • host /etc/resolv.conf127.0.0.53 stub; resolved active with the correct upstream, populated via dhcpcd → the resolvconf/resolvectl shim (no NetworkManager involved);
  • name resolution works on the host (getent hosts github.com);
  • name resolution works inside the Anthias containers (127.0.0.11 embedded DNS), and a real HTTPS asset fetch returns 200.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ

URL/web assets could fail to load on x86 until a manual `docker
restart` (GH issue 3230). On a headless x86 box the DHCP client
writes upstream nameservers directly into /etc/resolv.conf, and that
file can still be empty or hold a not-yet-reachable server at the
moment dockerd first creates the Anthias bridge network. Docker bakes
the host resolver config into its embedded DNS (127.0.0.11) at network
creation and never re-derives it, so containers stay stuck with a dead
upstream for the life of that network. The asset revalidation loop
then marks every URL asset unreachable and the viewer skips them; a
`docker restart` recreates the network after DNS is up, which is why
the manual workaround appears to fix it.

Installing and enabling systemd-resolved removes the race: it makes
/etc/resolv.conf a static symlink to the 127.0.0.53 stub that exists
from install time and never goes empty, while resolved tracks the live
upstream behind that fixed address. Docker forwards to the real
upstreams from the resolved run directory. The ordering is already
correct in Docker's packaged unit (After network-online.target and
nss-lookup.target) paired with resolved's Before nss-lookup.target, so
enabling resolved makes dockerd genuinely wait for name resolution.

Scoped to x86, where the issue is reported; the Pi images have a
different, working network/boot stack. Validated on the x86 testbed:
after a cold reboot with resolved enabled, host and in-container name
resolution and a real HTTPS asset fetch all succeed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (master@7acda8c). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3231   +/-   ##
=========================================
  Coverage          ?   90.82%           
=========================================
  Files             ?       76           
  Lines             ?     8361           
  Branches          ?      885           
=========================================
  Hits              ?     7594           
  Misses            ?      549           
  Partials          ?      218           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

vpetersson
vpetersson previously approved these changes Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses an x86-only cold-boot DNS race where Docker can snapshot an empty/stale host resolver configuration, leaving Anthias containers unable to resolve external URLs until Docker/containers are restarted. The fix ensures a stable local resolver is present before Docker is installed/started on x86 hosts.

Changes:

  • Add an x86-gated system task to configure DNS stability prior to Docker setup.
  • Install/enable systemd-resolved on x86 and repoint /etc/resolv.conf to the resolved stub.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
ansible/roles/system/tasks/main.yml Adds an x86-only include to run DNS setup before Docker tasks.
ansible/roles/system/tasks/dns.yml New task file that installs/enables systemd-resolved and symlinks /etc/resolv.conf to the stub resolver.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ansible/roles/system/tasks/dns.yml Outdated
The comment claimed the stub resolv.conf 'exists from install time and
never goes empty', but the symlink target
(/run/systemd/resolve/stub-resolv.conf) is generated by resolved at boot
and is briefly absent until the service starts — as the task's own
comment notes. Reword to state the actual invariant: the nameserver is a
fixed loopback address (127.0.0.53) and unit ordering (resolved
Before=nss-lookup.target, dockerd After=nss-lookup.target) guarantees the
stub is answering before dockerd creates the network.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HJ3ucEkn62cbgPoisAZ5LQ
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

ansible/roles/system/tasks/dns.yml:59

  • The comment says "expected and harmless. force overwrites…"; the sentence starts mid-word and reads like a typo. Capitalizing it improves readability without changing meaning.
    # The stub file is created by resolved at runtime, so the link is
    # briefly dangling on first boot before the service starts; that is
    # expected and harmless. force overwrites a static resolv.conf left
    # behind by the DHCP client / installer.

ansible/roles/system/tasks/dns.yml:55

  • Linking /etc/resolv.conf to a file under /run means it will be a dangling symlink early in boot (before systemd-resolved has started and created stub-resolv.conf). That can cause DNS lookups to fail for any unit that starts before resolved. Consider linking to the static stub file shipped by systemd-resolved (commonly /usr/lib/systemd/resolv.conf), or otherwise ensuring resolved is started before any DNS-consuming services on boot.
- name: Point /etc/resolv.conf at the systemd-resolved stub
  ansible.builtin.file:
    src: /run/systemd/resolve/stub-resolv.conf
    dest: /etc/resolv.conf
    state: link

@vpetersson
vpetersson merged commit ac27579 into Screenly:master Aug 5, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants