Using a Raspberry Pi as web server is one of the most practical ways to bridge physical hardware with network-accessible dashboards. Unlike microcontrollers (ESP32/Arduino) that require RTOS task management and C++ memory tuning, a Pi running Linux allows you to deploy robust Python REST APIs in minutes. The direct answer for a modern, reliable build: use a Raspberry Pi 5 (4GB) running Raspberry Pi OS Lite (64-bit), paired with Python Flask and the gpiozero library to serve real-time sensor states and control outputs over HTTP.
This guide targets the Raspberry Pi 5 (4GB RAM) variant. The Pi 5's PCIe Gen 2.0 interface and 2.4GHz Cortex-A76 cores eliminate the I/O bottlenecks that plagued earlier models when handling concurrent web requests alongside GPIO polling. Below is the complete hardware breakdown, pin mapping, production-ready code, and the exact debugging steps for common failure modes.
Hardware Bill of Materials & Web Server Suitability
Before wiring anything, you need to select the right board. Not every Pi variant is suited for concurrent web traffic. The table below compares the real-world web-serving capabilities of current models based on network throughput, RAM overhead for Flask/Gunicorn, and thermal throttling thresholds.
| Board Variant | CPU / Cores | RAM | Network Interface | Max Concurrent Flask Requests | Approx. Price (USD) |
|---|---|---|---|---|---|
| Pi 5 (4GB) | 2.4GHz Quad A76 | 4GB LPDDR4X | Gigabit Ethernet | ~150 req/sec (Gunicorn) | $60 |
| Pi 4 Model B (4GB) | 1.8GHz Quad A72 | 4GB LPDDR4 | Gigabit Ethernet | ~85 req/sec (Gunicorn) | $55 |
| Pi 3 Model B+ | 1.4GHz Quad A53 | 1GB DDR2 | Gigabit (USB 2.0 bus) | ~30 req/sec (Bottlenecked) | $35 (Used) |
| Pi Zero 2 W | 1.0GHz Quad A53 | 512MB DDR2 | WiFi only (No Ethernet) | ~20 req/sec (RAM limited) | $15 |
For this project, we are reading a physical pushbutton (simulating a doorbell or limit switch) and toggling a status LED, alongside reading the internal CPU temperature to monitor thermal health under load.
GPIO Pin Mapping Table
Wire the components to the Pi's 40-pin header as follows. Always use the 3.3V rail for logic inputs to avoid frying the Pi 5's SoC.
| Component | Pi GPIO (BCM) | Physical Pin # | Wiring Notes |
|---|---|---|---|
| Status LED (Anode) | GPIO 17 | Pin 11 | Use a 220Ω current-limiting resistor in series. |
| Status LED (Cathode) | GND | Pin 9 | Common ground rail. |
| Pushbutton (Leg 1) | GPIO 27 | Pin 13 | Enable internal pull-up in software; no external resistor needed. |
| Pushbutton (Leg 2) | GND | Pin 14 | Closes circuit to ground when pressed. |
Step-by-Step: Configuring the OS and Dependencies
Do not use the desktop version of Raspberry Pi OS for a dedicated web server. The X11 window manager consumes 400MB+ of RAM and introduces unnecessary attack surfaces.
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit) to a high-endurance microSD card (e.g., SanDisk High Endurance 32GB). Enable SSH and set your username/password in the imager's advanced settings.
- Boot and SSH: Connect via SSH. Run
sudo apt update && sudo apt upgrade -yto patch the kernel. - Install Python Dependencies: The Pi 5 requires the
lgpiobackend for GPIO access. Install Flask, GPIO Zero, and the lgpio bridge:sudo apt install python3-flask python3-gpiozero rpi-lgpio python3-smbus2 -y - Verify GPIO Permissions: Ensure your user is in the
gpiogroup. Runsudo usermod -aG gpio $USER, then log out and log back in.
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/python3.11. Otherwise, stick to port 5000 and use Nginx as a reverse proxy later.
Complete Python Flask Code with Error Handling
Create a file named server.py. This script initializes the GPIO pins, reads the CPU temperature, and exposes three REST endpoints. It includes explicit try/except blocks to catch I/O and pin factory errors that frequently crash headless Pi deployments.
from flask import Flask, jsonify, request
from gpiozero import LED, Button
from signal import pause
import os
import json
app = Flask(__name__)
# --- PIN DEFINITIONS ---
LED_PIN = 17
BTN_PIN = 27
# --- HARDWARE INITIALIZATION WITH ERROR HANDLING ---
try:
# pull_up=None relies on gpiozero's default internal pull-up for Buttons
status_led = LED(LED_PIN)
trigger_btn = Button(BTN_PIN, pull_up=True, bounce_time=0.05)
print(f"[INFO] GPIO initialized: LED on {LED_PIN}, Button on {BTN_PIN}")
except Exception as e:
print(f"[FATAL] GPIO initialization failed: {e}")
print("Ensure rpi-lgpio is installed and user is in the 'gpio' group.")
exit(1)
def get_cpu_temp():
"""Reads the SoC temperature from the Linux thermal zone."""
try:
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as f:
temp = int(f.read().strip()) / 1000.0
return round(temp, 2)
except FileNotFoundError:
return -1.0 # Sensor not found or permissions denied
@app.route('/api/status', methods=['GET'])
def get_status():
"""Returns current state of all sensors and outputs."""
payload = {
'led_state': status_led.is_lit,
'button_pressed': trigger_btn.is_pressed,
'cpu_temp_c': get_cpu_temp()
}
return jsonify(payload), 200
@app.route('/api/led/<string:state>', methods=['POST'])
def set_led(state):
"""Toggles the LED based on URL parameter."""
if state.lower() == 'on':
status_led.on()
return jsonify({'message': 'LED turned ON'}), 200
elif state.lower() == 'off':
status_led.off()
return jsonify({'message': 'LED turned OFF'}), 200
else:
return jsonify({'error': 'Invalid state. Use /api/led/on or /api/led/off'}), 400
if __name__ == '__main__':
# host='0.0.0.0' makes it accessible on the local network, not just localhost
print("[INFO] Starting Raspberry Pi Web Server on port 5000...")
app.run(host='0.0.0.0', port=5000, debug=False)
Run the server with python3 server.py. You can test it from your PC by navigating to http://<PI_IP_ADDRESS>:5000/api/status.
Debugging: When the Server Fails to Bind or GPIO Throws Errors
Headless embedded Linux environments fail differently than desktop environments. If your script crashes on startup, match the exact error string in your terminal to the ranked causes below.
1. The Port Binding Error
Exact Error String: OSError: [Errno 98] Address already in use
Ranked Causes:
- A previous instance of
server.pyis still running in the background (often caused by dropping an SSH session without killing the process). - Another service (like a pre-installed web server or MQTT broker) is bound to port 5000.
Fix: Run sudo lsof -i :5000 to find the PID holding the port, then sudo kill -9 <PID>.
2. The Pi 5 Pin Factory Error
Exact Error String: gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
Ranked Causes:
- Missing
rpi-lgpiopackage (The legacyRPi.GPIOlibrary does not work on the Pi 5's new RP1 I/O chip). - Running the script with
sudowhen the environment variables for the pin factory aren't passed through.
Fix: Install the bridge via sudo apt install rpi-lgpio and run the script as your standard user, not root. See the GPIO Zero Pi 5 Compatibility Notes for architecture details.
3. The Permissions Error
Exact Error String: PermissionError: [Errno 13] Permission denied: '/dev/gpiomem'
Ranked Causes:
- Your user is not in the
gpiogroup. - The OS is an older 32-bit build lacking the updated
udevrules for the Pi 5's RP1 chip.
Fix: Add the user to the group (sudo usermod -aG gpio $USER) and reboot. If it persists, update your OS kernel via sudo rpi-eeprom-update -a.
- Network Reachability: Ping the Pi's IP address. If it times out, your router's DHCP lease expired or the Pi lost WiFi/Ethernet link. Check your router's client list for the new IP.
- Process Ghosts: Always check for zombie Python processes holding ports using
ps aux | grep pythonbefore restarting the script. - Power Supply Brownouts: If the Pi randomly reboots or USB/GPIO drops out, check for the lightning bolt icon on the console. The Pi 5 requires a 27W USB-C PD supply; standard phone chargers will cause I/O failures under web load.
Extending and Simplifying the Build
The Flask development server used in the code above is perfect for local network prototyping, but it is not secure or efficient enough for production or internet-facing deployments. Depending on your end goal, you should adjust the architecture.
How to Simplify (For Low-Power / Remote Nodes)
- Downgrade the Hardware: If you only need to serve a single JSON endpoint every few seconds, switch to a Pi Zero 2 W. It draws under 1.5W, making it viable for solar/battery enclosures.
- Drop HTTP for MQTT: Web servers require the client to poll the Pi. Instead, use the
paho-mqttPython library to push button states to a Mosquitto broker. This reduces CPU wakeups and network chatter by over 90%.
How to Extend (For Production / Internet Access)
If you intend to expose this Raspberry Pi as web server to the wider internet or handle multiple simultaneous dashboard users, you must place a production WSGI server and reverse proxy in front of Flask. The Flask Deployment Guide explicitly warns against using the built-in Werkzeug server in production.
- Install Gunicorn:
pip3 install gunicorn. Run your app withgunicorn -w 4 -b 0.0.0.0:8000 server:app. This spawns 4 worker processes, utilizing all 4 cores of the Pi 5 to handle concurrent requests without blocking the GPIO thread. - Add Nginx as a Reverse Proxy: Install Nginx (
sudo apt install nginx) and configure it to forward port 80/443 traffic to Gunicorn's port 8000. Nginx handles static assets (HTML/CSS/JS dashboards) and SSL termination much more efficiently than Python. - Automate with Systemd: Create a
/etc/systemd/system/pisensor.servicefile so the server automatically restarts on boot or after a crash. Use theRestart=alwaysandWatchdogSec=10directives to ensure maximum uptime. - Secure with HTTPS: If exposing via a domain name, use Certbot to fetch free Let's Encrypt SSL certificates. Never expose unencrypted HTTP GPIO controls to the public internet.
By pairing the Pi 5's upgraded silicon with a proper Gunicorn/Nginx stack, you transform a simple hobby script into a resilient, industrial-grade edge server capable of running 24/7 in a factory or greenhouse environment. For deeper OS-level tuning, refer to the Raspberry Pi OS Configuration Documentation to disable unused services like Bluetooth and PulseAudio, freeing up critical RAM and CPU cycles for your web threads.






