Running a Raspberry Pi without a monitor, keyboard, or mouse—known as a headless setup—is the standard deployment method for embedded IoT nodes, home automation gateways, and remote data loggers. By configuring the OS over the network via SSH, you eliminate the need for a dedicated display and reduce the physical footprint of your build.
To set up Raspberry Pi headless reliably in 2026, you should use the official Raspberry Pi Imager to pre-configure your WiFi, SSH credentials, and hostname before the first boot. This guide walks through building a headless Raspberry Pi 5 environmental monitor that reads a BME280 I2C sensor and publishes the data to an MQTT broker.
The Headless Raspberry Pi 5 Spec Sheet & Parts List
Before flashing an OS, ensure your hardware matches the requirements for a stable headless node. The Raspberry Pi 5 requires a high-quality USB-C PD power supply to prevent brownouts under load, which is a common cause of silent reboots in headless deployments.
Estimated Time: 45 minutes
Target Board Variant: Raspberry Pi 5 (4GB or 8GB RAM) running Raspberry Pi OS (64-bit, Bookworm or newer)
| Component | Exact Variant / Model | Approx. Cost | Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB) | $60.00 | 8GB variant recommended if running local Docker containers alongside the script. |
| Power Supply | Official 27W USB-C PD PSU | $12.00 | Do not use generic phone chargers; Pi 5 will throttle USB current if PD negotiation fails. |
| Storage | SanDisk Extreme 32GB microSD | $14.00 | A1/A2 rated for high random I/O. Avoid generic unbranded cards. |
| Sensor | Adafruit BME280 I2C (PID 2652) | $10.00 | Includes onboard 3.3V logic and pull-up resistors. Generic clones often lack pull-ups. |
| Wiring | Silicone Female-to-Female Jumpers | $6.00 | Silicone insulation withstands heat better than PVC and is more flexible. |
Step-by-Step: Flashing and Booting Headless
The modern Raspberry Pi OS no longer includes a default pi user for security reasons. You must inject your user credentials and WiFi settings during the flashing process.
- Install Raspberry Pi Imager: Download the latest version from the official Raspberry Pi site for your host OS (Windows, macOS, or Linux).
- Select Device and OS: Choose Raspberry Pi 5 as the device. Select Raspberry Pi OS (64-bit) under the Lite or Desktop options (Lite is preferred for headless to save RAM).
- Open OS Customization: Click the gear icon (or press
Ctrl+Shift+X) to open the advanced settings menu. - Configure Network & SSH:
- Check Enable SSH and select Use password authentication.
- Set a custom Username (e.g.,
admin) and a strong Password. - Check Configure wireless LAN, enter your exact SSID and password, and set the country code (critical for 5GHz WiFi regulatory domains).
- Flash and Boot: Write the image to your microSD card. Insert it into the Pi 5, connect the 27W PSU, and wait 60-90 seconds for the first boot and SSH daemon initialization.
- Connect via SSH: Open your terminal and type
ssh admin@raspberrypi.local(or use the IP address assigned by your router).
Wiring the BME280 Sensor (Pin Mapping)
The Raspberry Pi 5 utilizes a 40-pin GPIO header. While the Pi 5 introduces a dedicated I2C bus for the onboard RTC and EEPROM, the primary user-accessible I2C bus (I2C1) remains on physical pins 3 and 5.
| Pi 5 Physical Pin | GPIO / Function | BME280 Breakout Pin | Wire Color (Suggested) |
|---|---|---|---|
| Pin 1 | 3.3V Power | VIN / VCC | Red |
| Pin 6 | Ground | GND | Black |
| Pin 3 | GPIO 2 (SDA1) | SDA | Blue |
| Pin 5 | GPIO 3 (SCL1) | SCL | Yellow |
Python MQTT Code for Headless Monitoring
Before running the code, enable the I2C interface on the Pi. Run sudo raspi-config, navigate to Interface Options > I2C, and enable it. Alternatively, add dtparam=i2c_arm=on to your /boot/firmware/config.txt and reboot.
Install the required Python libraries via the system package manager or pip (using a virtual environment is recommended in Bookworm):
sudo apt install python3-smbus2 python3-paho-mqtt i2c-tools
The following script targets the Raspberry Pi 5, reads the BME280 via I2C1, and publishes the payload to an MQTT broker. It includes explicit pin/bus definitions and robust error handling.
#!/usr/bin/env python3
"""
Headless Raspberry Pi 5 BME280 MQTT Publisher
Target: Raspberry Pi 5 (4GB/8GB) running Raspberry Pi OS 64-bit
Dependencies: smbus2, paho-mqtt (v2.0+)
"""
import time
import json
import smbus2
from paho.mqtt import client as mqtt_client
import paho.mqtt.enums as mqtt_enums
# --- Hardware & Pin Definitions ---
# Physical Pin 3 = GPIO 2 (SDA1), Physical Pin 5 = GPIO 3 (SCL1)
# On the Pi, these map to I2C Bus 1.
I2C_BUS_NUMBER = 1
BME280_I2C_ADDR = 0x76 # Use 0x77 if the breakout has the address jumper bridged
# --- MQTT Configuration ---
MQTT_BROKER = "192.168.1.50"
MQTT_PORT = 1883
MQTT_TOPIC = "homeassistant/sensor/pi5_bme280/state"
# BME280 Registers (Simplified for reading compensated data)
REG_CHIP_ID = 0xD0
REG_CONTROL = 0xF4
REG_DATA = 0xF7
def init_bme280(bus):
"""Verify chip ID and set oversampling."""
chip_id = bus.read_byte_data(BME280_I2C_ADDR, REG_CHIP_ID)
if chip_id != 0x60:
raise ValueError(f"Invalid BME280 Chip ID: {hex(chip_id)}. Check wiring.")
# Set oversampling: temp x2, press x16, humidity x1, normal mode
bus.write_byte_data(BME280_I2C_ADDR, 0xF2, 0x01) # Humidity
bus.write_byte_data(BME280_I2C_ADDR, REG_CONTROL, 0x6F) # Temp/Press/Mode
time.sleep(0.1)
def read_raw_data(bus):
"""Read raw bytes from the sensor (simplified mock for brevity)."""
# In production, use the adafruit-circuitpython-bme280 library for full compensation.
# Here we read raw registers to demonstrate I2C bus communication.
data = bus.read_i2c_block_data(BME280_I2C_ADDR, REG_DATA, 8)
# Mocking compensated values for the MQTT payload structure
return {"temperature_c": 22.5, "humidity_pct": 45.2, "pressure_hpa": 1013.25}
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print("Connected to MQTT Broker!")
else:
print(f"Failed to connect, return code {reason_code}")
def main():
# Initialize I2C Bus
try:
bus = smbus2.SMBus(I2C_BUS_NUMBER)
init_bme280(bus)
print("BME280 initialized successfully.")
except Exception as e:
print(f"I2C Initialization Failed: {e}")
return
# Initialize MQTT Client (Paho v2.0 API)
client = mqtt_client.Client(mqtt_enums.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
except Exception as e:
print(f"MQTT Connection Failed: {e}")
return
# Main Loop
try:
while True:
sensor_data = read_raw_data(bus)
payload = json.dumps(sensor_data)
client.publish(MQTT_TOPIC, payload, qos=1, retain=True)
print(f"Published: {payload}")
time.sleep(60)
except KeyboardInterrupt:
print("\nStopping...")
finally:
client.loop_stop()
client.disconnect()
bus.close()
if __name__ == "__main__":
main()
Debugging: Fixing "OSError: [Errno 121] Remote I/O error"
When working with I2C on a headless Pi, the most common failure mode is the OSError: [Errno 121] Remote I/O error. This is a low-level kernel error indicating the Pi sent a clock signal but received no acknowledgment (NACK) from the slave device.
The first three things to check when it fails:
- Verify the I2C Interface is Enabled: Run
ls /dev/i2c*. If/dev/i2c-1is missing, the kernel module isn't loaded. Re-runsudo raspi-configand reboot. - Scan the Bus for the Address: Run
i2cdetect -y 1. If the output grid is entirely empty (only dashes), the Pi cannot see the sensor at all. If you see76or77, the hardware connection is good, and the error is in your Python code's register calls. - Check Physical Wiring and Pull-ups: Ensure SDA and SCL are not swapped. If using a cheap generic BME280 breakout, it may lack the required 4.7kΩ pull-up resistors on the SDA/SCL lines. The Pi has internal pull-ups, but they are often too weak for reliable I2C at higher speeds.
For deeper I2C configuration details, consult the official Raspberry Pi documentation.
Extending and Simplifying the Build
How to simplify: If you don't want to write raw I2C register commands, simplify the build by installing the adafruit-circuitpython-bme280 library via pip. It handles all the complex temperature and pressure compensation math internally, reducing the Python script to about 15 lines of code.
How to extend: To make this headless node production-ready, extend it by creating a systemd service file. This ensures the script automatically restarts if it crashes or if the Pi loses power. Create a file at /etc/systemd/system/bme280-mqtt.service, define the ExecStart path to your Python script, and enable it with sudo systemctl enable --now bme280-mqtt. You can also extend the hardware by adding an OLED display on a secondary I2C bus using the Pi 5's GPIO 4 and 5 pins.
Headless Raspberry Pi Setup FAQ
How do I find my headless Raspberry Pi IP address on the network?
If raspberrypi.local (mDNS) isn't resolving on your Windows PC, log into your router's admin panel and check the DHCP client list for the hostname you assigned in the Imager. Alternatively, use a network scanner app like Fing on your smartphone, or run an ARP scan from another Linux machine on the same subnet using arp-scan -l.
Can I set up a Raspberry Pi headless using a smartphone instead of a PC?
Yes, but it requires specific hardware. You can use a USB-C to USB-A OTG adapter to connect a standard microSD card reader to an Android phone. Use an app like "EtchDroid" to flash the OS image. However, you cannot easily inject the SSH and WiFi configuration files via Android without a root file explorer. It is highly recommended to use a PC or Mac for the initial Imager step to ensure the userconf and wpa_supplicant files are written correctly to the boot partition.
Why does my headless Pi disconnect from WiFi after a reboot?
This usually happens because the WiFi power management feature is putting the adapter to sleep, or the 5GHz regulatory domain is misconfigured. To fix this, disable WiFi power management by creating a udev rule or running sudo iwconfig wlan0 power off. Additionally, ensure the country=XX line in your /boot/firmware/syscfg.txt (or wpa_supplicant.conf on older OS versions) exactly matches your local 2-letter ISO country code, otherwise the 5GHz radio will remain disabled by the kernel.






