If you just want to serve static HTML, buy a $5/month cloud VPS. We are hosting a website on a Raspberry Pi because we need to bridge the physical and digital worlds—reading sensors, toggling relays, and exposing hardware states to a local network. The direct answer for a reliable, production-grade embedded web server in 2026 is to use a Raspberry Pi 5 (4GB) running Raspberry Pi OS Bookworm, serving a Python Flask application via Gunicorn and fronted by Nginx.
This guide skips the basic 'hello world' tutorials and builds a robust, hardware-interfacing dashboard. We will wire physical components, write fault-tolerant Python code, and configure the systemd services required to keep your server alive after a power flicker.
Decision Tree: Choosing Your Pi Web Server Stack
Not every project needs a full Nginx reverse proxy. Use this decision matrix to lock in your architecture before buying parts.
| If your project needs... | Then choose this board... | And this web stack... |
|---|---|---|
| Static HTML/CSS dashboard only | Pi Zero 2 W ($15) | Lighttpd or Nginx (static) |
| Heavy database / Docker containers | Pi 5 (8GB) ($80) | Docker + PostgreSQL + FastAPI |
| Real-time GPIO control & API (Our Pick) | Pi 5 (4GB) ($60) | Flask + Gunicorn + Nginx |
Hardware Parts List & Pin Mapping
Before flashing the OS, gather these exact components. The Pi 5 has strict power delivery requirements; using an old 5V/3A phone charger will cause the RP1 chip to brownout and drop GPIO states under load.
Spec Sheet & Bill of Materials
| Component | Exact Variant / Model | Est. Cost |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 |
| Power Supply | Official 27W USB-C PD (5V/5A) | $12.00 |
| Cooling | Official Active Cooler (PWM controlled) | $5.00 |
| Storage | Samsung EVO Plus 64GB microSD (A2 rated) | $10.00 |
| Indicator LED | 5mm Blue LED + 330Ω 1/4W Resistor | $0.10 |
| Input Switch | 6x6mm Tactile Pushbutton (4-pin) | $0.05 |
GPIO Pin Mapping
We are using gpiozero which references BCM GPIO numbers, not physical pin numbers. Double-check your wiring against the physical board layout.
| BCM GPIO | Physical Pin | Component | Wiring Notes |
|---|---|---|---|
| GPIO 17 | Pin 11 | Status LED | Anode to Pin 11 via 330Ω resistor. Cathode to GND (Pin 9). |
| GPIO 27 | Pin 13 | Tactile Button | One side to Pin 13, other side to GND (Pin 14). Uses internal pull-up. |
Step-by-Step: Deploying the Flask GPIO Dashboard
Flash Raspberry Pi OS Bookworm (64-bit, Lite version preferred for servers) using the official Imager. SSH into the Pi and execute the following numbered steps.
- Update and Install Dependencies:
sudo apt update && sudo apt upgrade -y
sudo apt install python3-gpiozero python3-lgpio python3-flask gunicorn3 nginx -y
Note:python3-lgpiois mandatory on the Pi 5. The legacyRPi.GPIOlibrary is deprecated and will fail on the RP1 chip. - Create the Application Directory:
mkdir -p ~/gpio-dashboard && cd ~/gpio-dashboard - Write the Flask Application:
Createapp.py(code provided in the next section). - Configure Gunicorn Systemd Service:
Create a service file to keep the Python app running in the background.
sudo nano /etc/systemd/system/gpio-dashboard.service
Paste the following:[Unit] Description=GPIO Dashboard Gunicorn After=network.target [Service] User=pi Group=pi WorkingDirectory=/home/pi/gpio-dashboard ExecStart=/usr/bin/gunicorn3 --workers 1 --bind unix:gpio-dashboard.sock -m 007 app:app Restart=always [Install] WantedBy=multi-user.target
Crucial: We use exactly 1 worker. Multiple Gunicorn workers will fork the process and causeGPIOPinInUsecrashes when multiple Python instances try to claim the same hardware pin. - Configure Nginx Reverse Proxy:
sudo nano /etc/nginx/sites-available/gpio-dashboard
Paste:server { listen 80; server_name _; location / { include proxy_params; proxy_pass http://unix:/home/pi/gpio-dashboard/gpio-dashboard.sock; } }Link it and restart:
sudo ln -s /etc/nginx/sites-available/gpio-dashboard /etc/nginx/sites-enabled
sudo rm /etc/nginx/sites-enabled/default
sudo systemctl restart nginx - Enable and Start the App:
sudo systemctl enable gpio-dashboard
sudo systemctl start gpio-dashboard
The Code: Flask App with Hardware Error Handling
This is the complete, compilable app.py. It includes explicit pin definitions and try/except blocks to handle hardware initialization failures gracefully without crashing the web server.
import os
from flask import Flask, jsonify
from gpiozero import LED, Button
from gpiozero.exc import BadPinFactory, GPIOPinInUse
app = Flask(__name__)
# Explicit Pin Definitions (BCM Numbering)
PIN_LED = 17
PIN_BTN = 27
# Hardware Initialization with Error Handling
status_led = None
user_btn = None
try:
status_led = LED(PIN_LED)
user_btn = Button(PIN_BTN, pull_up=True, bounce_time=0.05)
print('Hardware initialized successfully.')
except (BadPinFactory, GPIOPinInUse, Exception) as e:
print(f'CRITICAL HARDWARE ERROR: {e}')
# App continues to run, but endpoints will report hardware fault
@app.route('/')
def index():
return jsonify({
'status': 'online',
'led_state': status_led.is_lit if status_led else 'hardware_fault',
'button_pressed': user_btn.is_pressed if user_btn else 'hardware_fault'
})
@app.route('/toggle')
def toggle():
if status_led:
status_led.toggle()
return jsonify({'success': True, 'new_state': status_led.is_lit})
return jsonify({'success': False, 'error': 'LED not initialized'}), 503
if __name__ == '__main__':
# Fallback for local testing without Gunicorn
app.run(host='0.0.0.0', port=5000)
Debugging: BadPinFactory and Peripheral Errors
When migrating older Pi code to the Pi 5, or when setting up virtual environments, you will inevitably hit hardware abstraction errors. Here is the exact error string and the ranked causes.
gpiozero.exc.BadPinFactory: Unable to load any default pin factory!
Ranked Causes & Fixes:
- Missing
lgpiobackend (Most Likely on Pi 5): The Pi 5's RP1 chip requires thelgpioC-library.gpiozerolooks for it automatically. If you are in a Python virtual environment (venv), it cannot see the system-installedlgpio.
Fix: Recreate your venv with system packages:python3 -m venv --system-site-packages env, or runpip install lgpioinside the venv. - Legacy
RPi.GPIOInterference: If your code explicitly importsRPi.GPIObeforegpiozero, it will crash on the Pi 5 because the BCM2835 memory map doesn't exist on the new silicon.
Fix: PurgeRPi.GPIOimports. Rely entirely ongpiozeroorrpi-lgpio. - Insufficient Power / Brownout: If the Pi 5 does not detect a 5A PD handshake from the power supply, it restricts peripheral current. The RP1 chip may fail to initialize the GPIO bank.
Fix: Checkvcgencmd get_throttled. If it returns anything other than0x0, upgrade to the official 27W Pi power supply.
First Three Things to Check When It Fails
If you navigate to your Pi's IP address and get a '502 Bad Gateway' or a timeout, do not rewrite your code. Check these three system states first:
- Check Nginx Proxy Status:
Runsudo systemctl status nginx. If Nginx is active but the site fails, it means Nginx cannot talk to the Gunicorn socket. Check socket permissions:ls -l /home/pi/gpio-dashboard/gpio-dashboard.sock. It must be owned by thepiuser and thewww-datagroup needs read/write access (handled by the-m 007flag in the systemd file). - Check Gunicorn Journal Logs:
Runjournalctl -u gpio-dashboard -n 50. This bypasses the web layer and shows you raw Python tracebacks. If you seeAddress already in use, a zombie Flask process is holding port 5000. Kill it withsudo fuser -k 5000/tcp. - Verify GPIO Memory Permissions:
Runls -l /dev/gpiomem. The output should showcrw-rw----with the groupgpio. Ensure your user (pi) is in the gpio group:groups pi. If not, runsudo usermod -aG gpio piand reboot.
Extending or Simplifying the Build
Depending on your deployment environment, you may need to strip this stack down or scale it up.
How to Simplify (Local LAN Only)
If this Pi is sitting on your desk and you are the only user, Nginx and Gunicorn are overkill. You can simplify the build by deleting the Nginx config and the systemd service, and instead running the Flask development server directly. Change the last line of app.py to app.run(host='0.0.0.0', port=80) and run it with sudo python3 app.py. Warning: Never expose the Flask dev server to the public internet; it lacks concurrent request handling and security hardening.
How to Extend (Production IoT)
To make this a true IoT node, extend the build by adding MQTT and SQLite. Install mosquitto and use the paho-mqtt Python library to publish button state changes to a broker, allowing Home Assistant to react to physical button presses instantly without polling the Flask API. Add a SQLite database via sqlite3 to log button press timestamps locally, ensuring you retain data even if the Wi-Fi drops.
By terminating your stack choice on the Pi 5 with Gunicorn and Nginx, you guarantee that your hardware-interfacing website survives reboots, handles concurrent browser requests without dropping GPIO interrupts, and respects the strict power and memory architectures of modern embedded silicon.






