Turning a Raspberry Pi into a webserver for physical hardware control bridges the gap between software and the real world. While hosting a static HTML page is trivial, building a Raspberry Pi webserver that safely toggles GPIO pins, reads environmental sensors, and handles network requests requires navigating hardware interrupts, memory-mapped I/O, and Python threading quirks.

This guide targets the Raspberry Pi 5 (4GB variant), though the code and wiring are fully backward-compatible with the Raspberry Pi 4 Model B. We will build a Flask-based API that reads a DHT22 temperature/humidity sensor and toggles a 5V relay module. More importantly, we will cover the exact failure modes and debug paths that typically stall embedded web projects on the bench.

Project Spec Sheet & Parts List

Do not rely on underpowered supplies or counterfeit sensors for embedded webservers; a brownout during a GPIO state change can corrupt the SD card or leave a relay latched. Budget around $85 for reliable components.

Component Exact Variant / Model Estimated Cost Notes
Microcontroller Raspberry Pi 5 (4GB RAM) $60.00 Requires active cooling for sustained network loads.
Power Supply Official 27W USB-C PD PSU $12.00 Required to prevent peripheral brownouts on Pi 5.
Storage SanDisk Extreme 64GB microSD (A2) $14.00 A2 rating ensures fast random I/O for database/logging.
Relay Module 5V 1-Channel Relay (Optocoupler) $3.00 Must have opto-isolation to protect Pi GPIO from back-EMF.
Sensor DHT22 / AM2302 (3-pin or 4-pin) $6.00 Avoid DHT11; it lacks the precision for meaningful logging.
Wiring 22 AWG Solid Core Jumper Wires $5.00 Use solid core for breadboards, stranded for screw terminals.

Hardware Wiring & Pin Mapping

The Raspberry Pi 5 uses the new RP1 southbridge chip for GPIO routing. While the physical pinout remains identical to the Pi 4, the underlying memory addresses have changed (which we will address in the debugging section). Use the following BCM (Broadcom) pin mapping for your physical connections.

Component Pin Pi Physical Pin BCM GPIO Wire Color (Suggested)
Relay VCC Pin 2 (5V Power) N/A Red
Relay GND Pin 6 (Ground) N/A Black
Relay IN (Signal) Pin 11 GPIO 17 Yellow
DHT22 VCC (+) Pin 1 (3.3V Power) N/A Orange
DHT22 GND (-) Pin 9 (Ground) N/A Brown
DHT22 DATA (Out) Pin 7 GPIO 4 Blue
Callout Tip: DHT22 Pull-Up Resistor
If your DHT22 module does not have a built-in pull-up resistor on the PCB, you must place a 10kΩ resistor between the VCC and DATA pins. Without it, the data line will float, resulting in continuous checksum errors in your Python logs.

The Flask Webserver Code (Python)

This code uses gpiozero for relay control and adafruit-circuitpython-dht for the sensor. We use gpiozero instead of the legacy RPi.GPIO library because it natively supports the Pi 5's RP1 chip via the lgpio backend.

Prerequisites: Run sudo apt install python3-gpiozero python3-pip and pip3 install flask adafruit-circuitpython-dht --break-system-packages (or use a virtual environment).


import time
import traceback
from flask import Flask, jsonify, request
from gpiozero import OutputDevice
import adafruit_dht
import board

# --- PIN DEFINITIONS ---
# Physical Pin 11 -> BCM GPIO 17
RELAY_PIN = 17  
# Physical Pin 7 -> BCM GPIO 4
DHT_PIN = board.D4 

app = Flask(__name__)

# Hardware Initialization with Error Handling
try:
    # active_high=True means GPIO HIGH triggers the relay
    relay = OutputDevice(RELAY_PIN, active_high=True, initial_value=False)
    # use_pulseio=False is mandatory on standard Pi OS Debian builds
    dht_sensor = adafruit_dht.DHT22(DHT_PIN, use_pulseio=False)
    print('Hardware initialized successfully.')
except RuntimeError as e:
    print(f'Hardware Init Failed (Check permissions/wiring): {e}')
    exit(1)
except Exception as e:
    print(f'Unexpected hardware error: {e}')
    exit(1)

@app.route('/api/status', methods=['GET'])
def get_status():
    try:
        temperature = dht_sensor.temperature
        humidity = dht_sensor.humidity
        relay_state = relay.value
        return jsonify({
            'relay': 'ON' if relay_state else 'OFF',
            'temperature_c': temperature,
            'humidity_pct': humidity
        }), 200
    except RuntimeError as e:
        # DHT sensors frequently throw checksum errors if polled too fast
        return jsonify({'error': 'Sensor read failed', 'detail': str(e)}), 503

@app.route('/api/relay', methods=['POST'])
def toggle_relay():
    try:
        data = request.get_json()
        state = data.get('state', 'toggle').lower()
        
        if state == 'on':
            relay.on()
        elif state == 'off':
            relay.off()
        else:
            relay.toggle()
            
        return jsonify({'relay': 'ON' if relay.value else 'OFF'}), 200
    except Exception as e:
        return jsonify({'error': 'Relay toggle failed', 'detail': str(e)}), 500

if __name__ == '__main__':
    try:
        # host='0.0.0.0' exposes the server to the local network
        app.run(host='0.0.0.0', port=5000, debug=False)
    except KeyboardInterrupt:
        print('\nServer stopped by user.')
    finally:
        # Safe GPIO cleanup to prevent relay latching on reboot
        relay.close()
        print('GPIO resources released.')

Debugging: When the Server Fails to Boot

Embedded webservers fail at the intersection of OS permissions and hardware addressing. If your script crashes on boot, here is the exact diagnostic path.

The Exact Error: 'Cannot determine SOC peripheral base address'

If you see this exact string in your terminal: RuntimeError: Cannot determine SOC peripheral base address, you are attempting to use the legacy RPi.GPIO library on a Raspberry Pi 5.

  • Cause 1 (Most Likely): The Pi 5 uses the RP1 southbridge chip. The old RPi.GPIO library looks for the BCM2711 memory-mapped addresses, which no longer exist on the Pi 5.
  • Fix: Uninstall the legacy library (pip uninstall RPi.GPIO) and ensure you are using gpiozero (as in the code above) or install the drop-in replacement rpi-lgpio.
  • Cause 2: You are running the script as a standard user without /dev/mem or /dev/gpiomem access.
  • Fix: Add your user to the gpio group: sudo usermod -aG gpio $USER, then log out and back in.

The First Three Things to Check When It Fails

If the server boots but behaves erratically (e.g., relay clicks randomly, sensor returns None, or API responses take >2 seconds), check these three items immediately:

  1. Power Throttling: The Pi 5 will silently throttle CPU and disable USB/peripherals if voltage drops below 4.65V. Run vcgencmd get_throttled. If it returns anything other than throttled=0x0, your power supply or USB-C cable is inadequate.
  2. DHT Checksum Collisions: The DHT22 uses a strict timing protocol. If your Flask server is handling heavy network traffic, the OS might interrupt the Python thread reading the sensor, causing a timing mismatch. Always wrap DHT reads in a try/except RuntimeError block and implement a 2-second software cooldown between polls.
  3. Port 5000 Collision: If you get OSError: [Errno 98] Address already in use, a zombie Flask process is holding the port. Kill it with sudo fuser -k 5000/tcp.

Extending and Simplifying the Build

The Flask development server (app.run()) is single-threaded and not meant for production. How you scale this depends on your end goal.

How to Simplify: If you only need to control the relay from a single mobile app on your local network, drop Flask entirely. Use the gpiozero built-in GPIOZeroPin network features, or write a lightweight UDP listener in Python. This removes HTTP overhead and reduces RAM usage by ~40MB, which is critical if you downgrade to a Pi Zero 2 W.

How to Extend (Production Ready): To make this a robust appliance: 1. Replace the Flask dev server with Gunicorn (pip install gunicorn) using 4 worker threads. 2. Put Nginx in front of Gunicorn as a reverse proxy to handle SSL termination and static file caching. 3. Use Cloudflare Tunnels to expose the API to the internet without opening ports on your home router. This prevents your home IP from being exposed to port scanners.

Frequently Asked Questions

How do I expose my Raspberry Pi webserver to the internet safely?

Do not use traditional port forwarding on your home router; it exposes your Pi (and your network) to automated botnets scanning for open port 5000. Instead, install Cloudflare Tunnels (cloudflared). It creates an outbound-only encrypted connection to Cloudflare's edge network, allowing you to map a public URL (e.g., api.yourdomain.com) directly to localhost:5000 on your Pi without touching your router's firewall.

Why is my Flask Raspberry Pi webserver running so slow on port 5000?

The built-in Flask server (Werkzeug) is single-threaded and synchronous. If a sensor read takes 500ms, the entire server blocks and queues all other incoming HTTP requests. To fix this, run the app using Gunicorn with threaded workers: gunicorn -w 4 --threads 2 -b 0.0.0.0:5000 app:app. This allows the Pi to handle multiple API calls concurrently while waiting on hardware I/O.

Can I use a Raspberry Pi Zero 2 W for this webserver project?

Yes, but with strict caveats. The Pi Zero 2 W has only 512MB of RAM. Running a full Flask stack, the OS, and sensor polling will consume about 250MB. If you add a database (like SQLite or InfluxDB) for logging, you will trigger swap memory usage, which will rapidly degrade your microSD card. If using a Zero 2 W, simplify the build by using a lightweight framework like Flask with no database, or switch to MicroPython on an ESP32 for lower-overhead IoT tasks.