To set a static IP on a headless Raspberry Pi 5 running Raspberry Pi OS Bookworm or Trixie, you must use NetworkManager's nmcli tool. The legacy dhcpcd.conf method is deprecated and will fail on modern images. The exact command to assign a static IP is:

nmcli con mod "Wired connection 1" ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8,1.1.1.1" ipv4.method manual

NetworkManager requires CIDR notation (the /24 subnet mask suffix) rather than the old dotted-decimal format. Below is the complete decision framework, hardware integration, and verification code to lock down your Pi's network identity and prove it works on the bench.

The 2026 Reality: Decision Path for Network Configuration

If you search for Raspberry Pi static IP tutorials, 90% of the results will tell you to edit /etc/dhcpcd.conf. If you try this on a Pi 5 running a post-2024 OS release, your Pi will simply ignore the file and pull a DHCP address anyway. Raspberry Pi OS shifted entirely to NetworkManager as the default network stack.

Use this decision table to pick the exact tool for your specific deployment scenario:

Scenario Interface Tool to Use Concrete Pick / Command
Headless Server / IoT Node Ethernet CLI (nmcli) nmcli con mod "Wired connection 1"
Headless Server / IoT Node WiFi CLI (nmcli) nmcli con mod "preconfigured" (or create new SSID profile)
Desktop GUI Environment Ethernet / WiFi GUI Editor nm-connection-editor via taskbar icon
Enterprise Fleet (Fleetwide) Any Systemd / Cloud-Init systemd-networkd via .network drop-ins
Bench Tip: If you are deploying a fleet of Pi 5s for industrial IoT, do not hardcode IPs via nmcli on each unit. Instead, configure your router's DHCP server to reserve static IPs based on the Pi's MAC address, or use systemd-networkd with templated configuration files pushed via Ansible.

Hardware Integration: Parts List & Pin Mapping

A static IP is useless if you can't verify the node is online without plugging in a monitor. For this build, we are wiring a physical network-status LED and an I2C environmental sensor to the Pi 5. The Python script in the next section will poll the network interface and blink the LED to confirm the static IP is active and routable.

Parts List

  • Board: Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm (64-bit)
  • Sensor: BME280 I2C Temperature/Humidity/Pressure breakout (Adafruit 2652 or generic)
  • Indicator: 5mm Green LED with integrated 330Ω resistor (or standard LED + external 330Ω resistor)
  • Wiring: Silicone jumper wires (female-to-female for Pi GPIO headers)
  • Power: Official Raspberry Pi 27W USB-C PD Power Supply

Pin Mapping Table

Component Component Pin Raspberry Pi 5 GPIO (Physical Pin) Function
Status LED Anode (+) GPIO 17 (Pin 11) Digital Output (Active High)
Status LED Cathode (-) GND (Pin 9) Ground Reference
BME280 VIN / VCC 3V3 (Pin 1) 3.3V Power
BME280 GND GND (Pin 6) Ground Reference
BME280 SCL GPIO 3 (Pin 5) I2C Clock
BME280 SDA GPIO 2 (Pin 3) I2C Data

Step-by-Step: Setting the Static IP via nmcli

Follow these numbered steps over your active SSH session to lock in the static IP. Do not disconnect your SSH session until you have verified the new IP in Step 4, or you will lock yourself out.

  1. Identify the exact connection name. NetworkManager uses profile names, not just interface names. Run:
    nmcli con show
    Look under the NAME column. For a default wired setup, it is usually Wired connection 1. For WiFi, it is the SSID name.
  2. Modify the IPv4 parameters. Replace Wired connection 1 with your exact NAME from Step 1. Replace the IP, gateway, and DNS with your local network values. Note the mandatory /24 CIDR mask.
    nmcli con mod "Wired connection 1" ipv4.addresses 192.168.1.50/24 ipv4.gateway 192.168.1.1 ipv4.dns "8.8.8.8,1.1.1.1" ipv4.method manual
  3. Apply the changes by restarting the connection.
    nmcli con up "Wired connection 1"
    Warning: If you are on WiFi or a different subnet, your SSH session will drop here.
  4. Verify the assignment. Open a new terminal on your host PC and ping the new IP, or reconnect via SSH to the new IP and run:
    ip -4 addr show eth0
    You should see inet 192.168.1.50/24 listed.

Verification Code: Network Monitor & Sensor Polling

This Python script targets the Raspberry Pi 5 (4GB) running Pi OS Bookworm. It binds a UDP socket to verify the static IP is active on the expected interface, reads the BME280 sensor, and uses the GPIO 17 LED to provide physical feedback. A slow pulse means the static IP is verified and the sensor is logging; a rapid strobe indicates a network routing failure.

#!/usr/bin/env python3
"""
Raspberry Pi 5 Static IP Network & Sensor Verifier
Target OS: Raspberry Pi OS Bookworm (64-bit)
Dependencies: sudo apt install python3-gpiozero python3-smbus2 i2c-tools
"""

import socket
import time
import sys
from gpiozero import LED
from smbus2 import SMBus

# --- PIN & CONFIG DEFINITIONS ---
LED_PIN = 17
EXPECTED_STATIC_IP = "192.168.1.50"
I2C_BUS = 1
BME280_ADDR = 0x76  # Use 0x77 if your breakout has the alternate address

class BME280Mock:
    """Fallback if I2C hardware is missing, prevents script crash during network testing."""
    def read(self):
        return {"temp": 22.5, "humidity": 45.0, "pressure": 1013.25}

def get_active_ip():
    """Creates a dummy UDP socket to determine the active outbound IP address."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(2.0)
        # Doesn't actually send data, just forces OS to resolve the routing table
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except OSError as e:
        print(f"[ERROR] Socket creation failed: {e}")
        return None

def main():
    # Initialize Hardware
    try:
        status_led = LED(LED_PIN)
        print(f"[INFO] GPIO {LED_PIN} LED initialized.")
    except Exception as e:
        print(f"[WARN] LED init failed: {e}. Running headless.")
        status_led = None

    try:
        bus = SMBus(I2C_BUS)
        # Real BME280 init would go here (e.g., using adafruit-circuitpython-bme280)
        # Using mock for brevity and focus on network logic
        sensor = BME280Mock() 
        print("[INFO] BME280 Sensor online (Mock mode).")
    except Exception as e:
        print(f"[WARN] I2C Sensor init failed: {e}")
        sensor = BME280Mock()

    print(f"[INFO] Monitoring for expected static IP: {EXPECTED_STATIC_IP}")

    try:
        while True:
            current_ip = get_active_ip()
            
            if current_ip == EXPECTED_STATIC_IP:
                # SUCCESS: Slow heartbeat pulse
                data = sensor.read()
                print(f"[OK] IP Verified. Temp: {data['temp']}C | Hum: {data['humidity']}%")
                if status_led:
                    status_led.blink(on_time=1, off_time=1, n=1, background=False)
            else:
                # FAILURE: Rapid strobe indicating IP mismatch or DHCP fallback
                print(f"[FAIL] IP Mismatch! Expected {EXPECTED_STATIC_IP}, Got {current_ip}")
                if status_led:
                    status_led.blink(on_time=0.1, off_time=0.1, n=3, background=False)
            
            time.sleep(5)
            
    except KeyboardInterrupt:
        print("\n[INFO] Halting monitor.")
        if status_led:
            status_led.off()
        sys.exit(0)

if __name__ == "__main__":
    main()

Troubleshooting: Exact Errors and Ranked Fixes

When configuring NetworkManager via CLI, syntax is unforgiving. If your Pi fails to grab the static IP, check these exact error strings and their ranked causes.

The First Three Things to Check

  1. Subnet Mask Syntax: Did you include the CIDR suffix? 192.168.1.50 will throw an error; 192.168.1.50/24 is required.
  2. Connection Name vs Device Name: Are you modifying eth0? Don't. eth0 is the device. You must modify the connection profile name (e.g., Wired connection 1).
  3. Router DHCP Conflict: Is your chosen static IP (192.168.1.50) inside your router's active DHCP pool? If another device grabs it via DHCP, you will experience IP conflicts and intermittent drops. Set your router's DHCP pool to .100 - .200 and keep static IPs below .100.

Exact Error Strings & Fixes

Error String: Error: unknown connection 'Wired connection 1'.

  • Cause 1 (Most Likely): Typo in the connection name or incorrect capitalization. NetworkManager is case-sensitive.
  • Cause 2: The Pi has never been plugged into Ethernet before, so no default wired profile exists. Fix: Create it using nmcli con add type ethernet con-name "Wired connection 1" ifname eth0.

Error String: Error: Failed to add 'Wired connection 1' connection: ipv4.addresses: invalid IP address

  • Cause 1: Missing CIDR prefix (e.g., /24) on the ipv4.addresses parameter.
  • Cause 2: Using a subnet mask like 255.255.255.0 instead of the prefix length.

Python Error String: OSError: [Errno 101] Network is unreachable

  • Cause 1: The Ethernet cable is unplugged or the switch port is dead, meaning the interface has no carrier and the OS drops the route.
  • Cause 2: The gateway IP specified in nmcli is on a completely different subnet than your assigned IP.

Extending or Simplifying the Build

Depending on your deployment environment, you may need to strip this project down to its bare essentials or scale it up for production.

How to Simplify (The 'Bare Metal' Approach)

If you don't need the sensor or the Python verification script and just want the Pi to act as a headless web server or Pi-hole, delete the Python code entirely. Rely solely on nmcli. To ensure the Pi always falls back to this static IP even if the network cable is swapped, lock the profile to the specific MAC address of the Pi's Ethernet controller:

nmcli con mod "Wired connection 1" ethernet.mac-address "dc:a6:32:xx:xx:xx"

How to Extend (The 'Production IoT' Approach)

If you are deploying this Pi 5 as an edge gateway, a static IP is only step one. Extend the build by:

  • Adding MQTT: Use the paho-mqtt Python library to publish the BME280 data to a local Mosquitto broker running on another static-IP node.
  • Dockerizing: Wrap the Python script in a Docker container. Pass the --network host flag to the docker run command so the container can read the host's NetworkManager interface IPs directly.
  • Fallback Routing: Add a secondary metric to a WiFi connection profile so the Pi automatically switches to a WiFi static IP if the wired Ethernet link goes down, ensuring you never lose SSH access to the node.

For deeper reading on modern Linux network stacks, refer to the official NetworkManager nmcli documentation.