When building network projects with a Raspberry Pi, the difference between a weekend toy and a reliable 24/7 infrastructure node comes down to hardware selection, power delivery, and explicit software configuration. This guide walks through building a dual-purpose network appliance: a Pi-hole DNS sinkhole paired with a Mosquitto MQTT broker for local IoT telemetry, complete with an I2C OLED status dashboard.

The code and hardware configurations in this guide specifically target the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Lite (64-bit, Bookworm). We will bypass the common pitfalls of headless network nodes by adding a physical status display and addressing the strict security defaults introduced in Mosquitto v2.0+.

Project Spec Sheet & Parts List

Difficulty: Intermediate | Estimated Time: 2 Hours | Cost: ~$115 USD

Component Exact Model / Variant Why This Specific Part
Compute Board Raspberry Pi 5 (8GB) 8GB RAM prevents OOM kills when Pi-hole FTL and Mosquitto spike during heavy DNS queries or MQTT message bursts.
Power Supply Official Raspberry Pi 27W USB-C PD The Pi 5 requires 5V/5A via USB-C PD to enable full peripheral current limits. Third-party 5V/3A supplies will throttle USB ports and cause brownouts.
Cooling Official Active Cooler Network daemons cause sustained CPU loads. The Active Cooler keeps the BCM2712 under 60°C without the bulk of a tower cooler.
Storage Samsung PRO Endurance 64GB (A2) Network nodes write constant logs. Endurance-rated microSD cards prevent filesystem corruption from write-wear.
Status Display SSD1306 128x64 I2C OLED (0.96") Provides headless IP and daemon status without needing to SSH in or plug in an HDMI monitor.
Wiring Female-to-Female Dupont (20cm) Standard 2.54mm pitch jumper wires for the I2C bus connection.

Hardware Assembly & Pin Mapping

The Raspberry Pi 5 retains the standard 40-pin GPIO header layout, but the underlying I2C bus routing remains on I2C1 (BCM GPIO 2 and 3). Wire the SSD1306 OLED display to the Pi 5 using the following pin mapping.

Pi 5 Pin Name BCM GPIO Physical Pin OLED Pin Function
3V3 Power N/A 1 VCC 3.3V logic power for the OLED controller
Ground N/A 6 GND Common ground reference
GPIO 2 (SDA1) 2 3 SDA I2C Data line
GPIO 3 (SCL1) 3 5 SCL I2C Clock line
  1. Apply the Active Cooler: Peel the adhesive backing off the thermal pads and press the Active Cooler firmly onto the BCM2712 SoC and PMIC. Plug the 4-pin PWM fan connector into the dedicated JST fan header on the Pi 5 board.
  2. Connect the OLED: Using the Dupont wires, connect the display to the physical pins 1, 3, 5, and 6 as mapped above. Ensure the SDA and SCL lines are not swapped; doing so won't damage the board, but the display will not initialize.
  3. Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit). In the OS Customisation menu, enable SSH, set your hostname to netnode, and configure your WiFi or rely on Ethernet.
  4. Boot and Verify I2C: SSH into the Pi and run sudo raspi-config to enable the I2C interface under Interface Options. Reboot, then run i2cdetect -y 1. You should see 3c in the grid, confirming the SSD1306 is responding at its default hex address.

Software Configuration & Python Status Monitor

Install Pi-hole using the official automated installer, which handles the DNSmasq configuration and web server setup. For Mosquitto, the Bookworm repository includes v2.0+, which defaults to a highly restrictive security model that blocks external connections unless explicitly configured.

Configuration Note: After installing Mosquitto (sudo apt install mosquitto mosquitto-clients), you must edit /etc/mosquitto/mosquitto.conf and add listener 1883 0.0.0.0 and allow_anonymous true (or configure ACLs/passwords for production). Without the listener directive, Mosquitto v2.0+ only binds to localhost.

To monitor the node without SSH, we use a Python script leveraging the luma.oled library for the display and paho.mqtt.client to verify the broker is accepting local connections. Install the dependencies:

sudo apt install python3-pip python3-smbus i2c-tools
pip3 install luma.oled paho-mqtt --break-system-packages

Save the following code as network_monitor.py. This script includes explicit error handling for I2C bus faults and MQTT connection refusals.

import time
import socket
import sys
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import ImageFont
import paho.mqtt.client as mqtt

# --- Pin & Hardware Definitions ---
# I2C Port 1 is standard for Pi 5 SDA1/SCL1 (Pins 3 & 5)
I2C_PORT = 1
OLED_ADDRESS = 0x3C
MQTT_BROKER = "localhost"
MQTT_PORT = 1883

def get_ip_address():
    """Gets the primary IP address by opening a dummy UDP socket."""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        ip = s.getsockname()[0]
    except Exception:
        ip = "No Network"
    finally:
        s.close()
    return ip

def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        client.connected_flag = True
    else:
        client.bad_connection_flag = True

# Initialize MQTT Client (Paho v2.0+ requires CallbackAPIVersion)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect

try:
    # Initialize I2C OLED
    serial = i2c(port=I2C_PORT, address=OLED_ADDRESS)
    device = ssd1306(serial)
    font = ImageFont.load_default()
except Exception as e:
    print(f"Fatal: Cannot initialize OLED. Check wiring. Error: {e}")
    sys.exit(1)

while True:
    ip_addr = get_ip_address()
    mqtt_status = "Disconnected"
    
    try:
        if not client.is_connected():
            client.connect(MQTT_BROKER, MQTT_PORT, 60)
            client.loop_start()
            time.sleep(1) # Allow callback to execute
        if client.is_connected():
            mqtt_status = "Broker OK"
    except ConnectionRefusedError:
        mqtt_status = "Err: Refused"
    except Exception as e:
        mqtt_status = f"Err: {type(e).__name__}"

    # Render to OLED
    try:
        with canvas(device) as draw:
            draw.text((0, 0), f"IP: {ip_addr}", font=font, fill="white")
            draw.text((0, 20), f"MQTT: {mqtt_status}", font=font, fill="white")
            draw.text((0, 40), "Pi-hole: Active", font=font, fill="white")
    except OSError as e:
        print(f"I2C Write Error: {e}")
        
    time.sleep(5)

Debugging Network & MQTT Failures

When your Raspberry Pi network projects fail, the issue is rarely the code itself; it is almost always power, IP conflicts, or daemon security policies. If the node drops off the network or the OLED shows an error, check these first three things:

  1. Power Supply Brownout: Run dmesg | grep -i voltage. If you see "Under-voltage detected", your power supply is failing under the combined load of the CPU, WiFi/Ethernet PHY, and I2C bus. Swap to the official 27W PSU.
  2. DHCP Lease Conflicts: If Pi-hole is acting as your DHCP server, ensure its IP pool does not overlap with your primary router's pool, or disable DHCP on the router entirely. Duplicate IPs will cause intermittent packet loss.
  3. Mosquitto Listener Binding: If remote IoT devices cannot connect to the broker, verify /etc/mosquitto/mosquitto.conf contains listener 1883 0.0.0.0. Restart the service with sudo systemctl restart mosquitto.

Exact Error Strings and Ranked Causes

Error 1: ConnectionRefusedError: [Errno 111] Connection refused

  • Cause A (Most Likely): Mosquitto is not running, or crashed due to a malformed config file. Check sudo systemctl status mosquitto.
  • Cause B: Mosquitto v2.0+ default security is blocking the connection because the listener directive is missing from the config.
  • Cause C: UFW or iptables is actively blocking port 1883. Run sudo ufw allow 1883/tcp.

Error 2: OSError: [Errno 121] Remote I/O error (in Python script)

  • Cause A: Loose Dupont wire on the SDA or SCL line. The I2C bus lacks pull-up resistors on the Pi; rely on the OLED module's onboard pull-ups and ensure a tight physical connection.
  • Cause B: The I2C address is incorrect. Some SSD1306 clones use 0x3D instead of 0x3C. Verify with i2cdetect -y 1.

Error 3: NXDOMAIN or DNS Timeout on client devices

  • Cause A: The Pi-hole FTL service crashed. Check pihole status.
  • Cause B: The Pi lost its upstream DNS configuration. Verify /etc/resolv.conf or the Pi-hole web UI upstream DNS settings.

Extending and Simplifying the Build

To Simplify: If you don't want to wire an I2C OLED, you can strip the hardware down to just the Pi and Ethernet. Replace the Python script with a simple cron job that emails or pushes a notification via Pushover if the Pi-hole API (/admin/api.php?summary) fails to respond. You can also use the official Pi-hole documentation to configure Telegram alerts natively.

To Extend: Transform this node into a full smart-home hub by adding a Zigbee coordinator. Plug a Sonoff Zigbee 3.0 USB Dongle Plus-P into a USB 2.0 port (using a short USB extension cable to avoid 2.4GHz WiFi interference). Install Zigbee2MQTT via Docker. The MQTT broker we configured will seamlessly bridge the Zigbee telemetry to your home automation platform like Home Assistant. For advanced storage, the Raspberry Pi 5 PCIe lane allows you to add an NVMe HAT, eliminating microSD card wear entirely.

FAQ: Common Raspberry Pi Network Project Questions

Which Raspberry Pi board variant is best for network projects in 2026?

For dedicated network infrastructure (routers, NAS, MQTT brokers), the Raspberry Pi 5 (4GB or 8GB) is the current standard due to its true Gigabit Ethernet PHY and PCIe Gen 2 support for NVMe storage. However, if your project involves distributed, low-power sensor nodes rather than a central server, the Raspberry Pi Zero 2 W remains the most cost-effective choice, provided you can tolerate its 100Mbps USB-tied Ethernet limitations and 512MB RAM constraint.

Why does my MQTT broker drop connections when the Pi reboots?

By default, Mosquitto does not persist session states or retained messages across reboots unless configured to do so. To fix this, you must enable persistence in /etc/mosquitto/mosquitto.conf by adding persistence true and persistence_location /var/lib/mosquitto/. Ensure the mosquitto user has write permissions to that directory, otherwise, the daemon will silently fail to save the database and drop all retained topics on reboot.

Can I run Pi-hole and a DHCP server on the same Raspberry Pi?

Yes, Pi-hole includes a built-in DHCP server (dnsmasq). However, you must disable the DHCP server on your primary ISP router first. Running two DHCP servers on the same VLAN will result in a race condition where clients randomly receive IP addresses with conflicting gateways, leading to intermittent internet outages. If your router's DHCP cannot be disabled, you must isolate the Pi-hole on a separate VLAN or use it strictly for DNS forwarding without enabling its DHCP module.