To build a reliable, production-ready web server on a Raspberry Pi 5 that interacts with physical hardware, use the Raspberry Pi 5 (8GB) running Raspberry Pi OS (64-bit) Bookworm, and deploy a Python Flask application served by Gunicorn. The Pi 5’s new RP1 southbridge chip fundamentally changes how GPIO is accessed, rendering older libraries like RPi.GPIO obsolete. This guide provides the exact hardware BOM, the updated gpiozero pin mapping, complete compilable code with error handling, and the specific debugging paths for Pi 5 architecture.
The Pi 5 Web Server Decision Matrix
Before writing code, you must choose the right web framework for an embedded environment. The Pi 5 has ample RAM (up to 8GB), but thermal throttling and SD card I/O bottlenecks still dictate a lightweight footprint. Here is the decision path for selecting your stack:
| Framework | Async Support | Learning Curve | Pi 5 Idle RAM | Verdict |
|---|---|---|---|---|
| Flask + Gunicorn | No (Sync/WSGI) | Low | ~35 MB | Default Pick: Best balance of simplicity, low resource use, and vast documentation for hardware APIs. |
| FastAPI + Uvicorn | Yes (ASGI) | Medium | ~50 MB | Choose when you need high-concurrency WebSockets for real-time sensor streaming. |
| Node-RED | Yes (Event-loop) | Low (Visual) | ~180 MB | Choose only if the project is purely IoT data routing with zero custom UI requirements. |
Hardware BOM and GPIO Pin Mapping
The Raspberry Pi 5 requires a 27W USB-C PD power supply to prevent brownouts when driving GPIO pins. Do not use a standard 15W Pi 4 brick; the Pi 5 will throttle and drop USB/GPIO power under load.
Parts List
- Board: Raspberry Pi 5 (8GB variant) — ~$80
- Thermal: Official Raspberry Pi 5 Active Cooler — ~$5 (Mandatory; passive cooling fails under web server load)
- Power: Official 27W USB-C PD Power Supply — ~$12
- Actuator: 5V Single-Channel Relay Module (Optocoupler isolated, SRD-05VDC-SL-C) — ~$3
- Input: 6x6mm Tactile Pushbutton Switch — ~$1
- Wiring: 22 AWG Female-to-Female Jumper Wires
Spec-Sheet: Pin Mapping Table
The Pi 5 maintains the standard 40-pin header layout, but the underlying BCM GPIO numbers map to the RP1 chip. Always reference the BCM GPIO number in your code, not the physical pin number.
| Physical Pin | BCM GPIO | Function | Connected Component |
|---|---|---|---|
| Pin 2 | 5V | Power | Relay Module VCC |
| Pin 6 | GND | Ground | Relay GND & Button GND |
| Pin 11 | GPIO 17 | Digital Output | Relay IN (Signal) |
| Pin 13 | GPIO 27 | Digital Input | Button Signal (Internal Pull-up) |
Build Sequence: Environment and Code
This build targets Raspberry Pi OS (64-bit) Bookworm. Bookworm uses lgpio as the default pin factory for gpiozero, which is critical for Pi 5 compatibility.
Step 1: OS and Dependency Setup
- Flash Raspberry Pi OS (64-bit) Bookworm using Raspberry Pi Imager. Enable SSH and set your username/password in the advanced settings.
- Boot the Pi 5, connect via SSH, and update the system:
sudo apt update && sudo apt upgrade -y - Install the required Python packages. We use a virtual environment to comply with PEP 668 (externally managed environment rules in Bookworm):
mkdir ~/pi-server && cd ~/pi-server python3 -m venv venv source venv/bin/activate pip install flask gunicorn gpiozero psutil lgpio
Step 2: The Application Code
Save the following complete, compilable code as app.py. It includes pin definitions, hardware fallbacks for headless testing, and proper error handling.
import os
import logging
from flask import Flask, jsonify, request
from gpiozero import LED, Button
from gpiozero.exc import GPIODeviceError
import psutil
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app = Flask(__name__)
# --- PIN DEFINITIONS (BCM Numbering) ---
RELAY_GPIO = 17 # Physical Pin 11
BUTTON_GPIO = 27 # Physical Pin 13
# --- HARDWARE INITIALIZATION ---
relay = None
button = None
try:
# Pi 5 uses lgpio backend automatically via gpiozero >= 2.0
relay = LED(RELAY_GPIO, active_high=True, initial_value=False)
button = Button(BUTTON_GPIO, pull_up=True, bounce_time=0.05)
logging.info("GPIO initialized successfully via lgpio backend.")
except GPIODeviceError as e:
logging.critical(f"GPIO Initialization failed: {e}. Running in headless/simulated mode.")
# Fallback allows the web server to run for API testing even if GPIO fails
# --- WEB ROUTES ---
@app.route('/')
def index():
return "<h1>Pi 5 Hardware Server</h1><p>Use /api/status or /api/relay/on</p>"
@app.route('/api/status', methods=['GET'])
def get_status():
cpu_temp = psutil.sensors_temperatures().get('cpu_thermal', [None])[0]
temp_val = cpu_temp.current if cpu_temp else 'N/A'
return jsonify({
"relay_state": relay.value if relay else "simulated_off",
"button_pressed": button.is_pressed if button else "simulated_false",
"cpu_temp_c": temp_val
})
@app.route('/api/relay/<state>', methods=['POST'])
def control_relay(state):
if relay is None:
return jsonify({"error": "Hardware not initialized"}), 503
if state == 'on':
relay.on()
logging.info("Relay engaged.")
elif state == 'off':
relay.off()
logging.info("Relay disengaged.")
else:
return jsonify({"error": "Invalid state. Use 'on' or 'off'."}), 400
return jsonify({"relay_state": relay.value})
if __name__ == '__main__':
# Dev server only. Use Gunicorn for production.
app.run(host='0.0.0.0', port=5000, debug=True)
Step 3: Production Deployment with Gunicorn
Never use the Flask development server in production. It lacks concurrency and will drop requests. Run the app using Gunicorn, binding to all interfaces so you can access it from your local network:
gunicorn --bind 0.0.0.0:8000 --workers 2 --threads 2 app:app
Debugging: Exact Errors and the "First Three" Checklist
When migrating code from a Pi 4 to a Pi 5, or deploying headless, you will hit specific architectural walls. Here are the exact error strings and their ranked fixes.
Ranked Error Causes
Error 1: RuntimeError: This module can only be run on a Raspberry Pi!
- Cause: Your code or a dependency is importing the legacy
RPi.GPIOlibrary. The Pi 5’s RP1 chip does not map to the old Broadcom memory addresses. - Fix: Remove
RPi.GPIOfrom your requirements. Refactor to usegpiozero(which useslgpioon Bookworm) or uselgpiodirectly.
Error 2: lgpio.error: 'GPIO busy' or gpiozero.exc.GPIOPinInUse
- Cause: A previous instance of your script crashed without releasing the pin, or another service (like a background MQTT script) holds the file lock on the GPIO chip.
- Fix: Run
sudo lsof | grep gpiochipto find the PID holding the pin, thenkill -9 [PID]. If stuck, a hard reboot clears the RP1 state.
Error 3: ConnectionRefusedError: [Errno 111] Connection refused (from external PC)
- Cause: You started the server with
flask runorapp.run()without specifying the host, causing it to bind only to127.0.0.1(localhost). - Fix: Always use Gunicorn with
--bind 0.0.0.0:8000or passhost='0.0.0.0'to the Flask dev server.
1. Verify Pin Factory: Run
python3 -c "import gpiozero; print(gpiozero.Device.pin_factory)". It must output <gpiozero.pins.lgpio.LGPIOFactory object>. If it says RPiGPIO, your environment is misconfigured.2. Check Power Supply: Run
vcgencmd get_throttled. If it returns anything other than 0x0, your power supply is inadequate, causing the RP1 chip to drop GPIO connections.3. Confirm Bind Address: Run
sudo ss -tulpn | grep 8000 to ensure Gunicorn is listening on 0.0.0.0:8000, not 127.0.0.1.
Scaling the Architecture
Once the baseline server is stable, you must decide how to scale based on your deployment environment.
How to Simplify (The Static Route)
If you only need to serve a single HTML dashboard and don't require dynamic Python API endpoints, strip out Flask entirely. Use lighttpd or Nginx to serve static HTML/JS files, and use a lightweight C-based daemon (like pigpiod) to handle GPIO via socket commands. This reduces RAM usage to under 10 MB and eliminates Python dependency rot.
How to Extend (The Production Route)
For a robust, 24/7 home automation server, extend the build with these three layers:
- Reverse Proxy: Install Nginx (
sudo apt install nginx) to sit in front of Gunicorn. Nginx handles SSL termination (via Let's Encrypt) and static asset caching, passing only API calls to Python. - Systemd Service: Create a
/etc/systemd/system/piserver.servicefile to auto-start Gunicorn on boot and restart it on crash. Use theRestart=alwaysandWatchdogSec=30directives. - Containerization: If you plan to replicate this across multiple Pi 5 nodes, wrap the Flask app and Gunicorn in a Docker container. Note: You must pass
--device /dev/gpiochip0to thedocker runcommand to grant the container access to the Pi 5's RP1 GPIO headers.
For deeper reading on the Pi 5's RP1 architecture and GPIO changes, consult the official Raspberry Pi hardware documentation. For production WSGI deployment patterns, reference the Gunicorn deployment guide.






