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 |
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.GPIOlibrary 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 usinggpiozero(as in the code above) or install the drop-in replacementrpi-lgpio. - Cause 2: You are running the script as a standard user without
/dev/memor/dev/gpiomemaccess. - 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:
- 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 thanthrottled=0x0, your power supply or USB-C cable is inadequate. - 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 RuntimeErrorblock and implement a 2-second software cooldown between polls. - Port 5000 Collision: If you get
OSError: [Errno 98] Address already in use, a zombie Flask process is holding the port. Kill it withsudo 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.






