static.apur.in

Static addressing on a headless box without locking yourself out

2026-05-19

Network configuration is the one change where a typo costs you the connection you are using to fix the typo. The way through it is to know which layer you are editing and to always leave yourself a way back.

Which layer is actually in charge

On a current Ubuntu server the chain is: YAML in /etc/netplan/ → generated .network file → systemd-networkd. When something behaves oddly, look at the generated file rather than guessing from the YAML:

networkctl status ens18
ls /run/systemd/network/ /etc/systemd/network/
networkctl cat ens18            # the effective .network, after drop-ins

Two files defining the same interface is a real and common failure: a leftover 99-custom.yaml next to the installer's 50-cloud-init.yaml. netplan merges them, and the result is rarely what either file meant on its own.

Making the change

# /etc/netplan/60-static.yaml
network:
  version: 2
  ethernets:
    ens18:
      dhcp4: false
      addresses: [192.168.1.50/24]
      routes:
        - to: default
          via: 192.168.1.1
        - to: 192.168.1.99/32
          via: 192.168.1.1
          on-link: true
      nameservers:
        addresses: [192.168.1.1]

Drop-ins instead of editing generated files

Anything netplan generates can be extended without fighting it, because networkd reads drop-in directories next to the unit:

/etc/systemd/network/10-netplan-ens18.network.d/extra.conf

[Route]
Destination=192.168.1.99/32
Gateway=192.168.1.1

This survives netplan apply and interface re-creation, which matters more than it sounds: a route added with ip route at runtime belongs to nobody and is lost the first time the interface is rebuilt — by a reboot, by a package upgrade that restarts networkd, or by a DHCP renewal that reconfigures the link. Anything the machine needs in order to come back must be declared, not typed.

Verifying

networkctl is-ready ens18
ip -4 addr show ens18
ip route show table all | grep -v 'linkdown'
resolvectl status | sed -n '/Current DNS Server/,+2p'
ping -c2 -W2 192.168.1.1

Then, and this is the part people skip: from a second session, confirm the box is reachable by the new address before closing the first one. Keep the old session open until you have typed something in the new one.

Boot-time ordering

Services that bind to an address will fail if they start before addressing exists. systemd-networkd-wait-online is the blunt instrument; the precise version is to depend on the interface being configured:

[Unit]
After=systemd-networkd.service
Wants=network-online.target

If a service instead retries and binds late, it will look healthy while silently having skipped whatever it does at startup. Check what it logged on boot, not whether it is running.

← manuals