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 onlyPi Zero 2 W ($15)Lighttpd or Nginx (static)
Heavy database / Docker containersPi 5 (8GB) ($80)Docker + PostgreSQL + FastAPI
Real-time GPIO control & API (Our Pick)Pi 5 (4GB) ($60)Flask + Gunicorn + Nginx
Why the Pi 5 4GB? The Pi 5's RP1 southbridge chip handles GPIO interrupts significantly faster than the Pi 4's BCM2711 peripheral bus. The 4GB variant provides ample headroom for Python's garbage collection and Nginx worker processes without paying the premium for 8GB, which is only necessary if you are running local LLMs or heavy Docker stacks.

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

ComponentExact Variant / ModelEst. Cost
MicrocontrollerRaspberry Pi 5 (4GB RAM)$60.00
Power SupplyOfficial 27W USB-C PD (5V/5A)$12.00
CoolingOfficial Active Cooler (PWM controlled)$5.00
StorageSamsung EVO Plus 64GB microSD (A2 rated)$10.00
Indicator LED5mm Blue LED + 330Ω 1/4W Resistor$0.10
Input Switch6x6mm 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 GPIOPhysical PinComponentWiring Notes
GPIO 17Pin 11Status LEDAnode to Pin 11 via 330Ω resistor. Cathode to GND (Pin 9).
GPIO 27Pin 13Tactile ButtonOne 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.

  1. 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-lgpio is mandatory on the Pi 5. The legacy RPi.GPIO library is deprecated and will fail on the RP1 chip.
  2. Create the Application Directory:
    mkdir -p ~/gpio-dashboard && cd ~/gpio-dashboard
  3. Write the Flask Application:
    Create app.py (code provided in the next section).
  4. 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 cause GPIOPinInUse crashes when multiple Python instances try to claim the same hardware pin.
  5. 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
  6. 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.

Exact Error String:
gpiozero.exc.BadPinFactory: Unable to load any default pin factory!

Ranked Causes & Fixes:

  1. Missing lgpio backend (Most Likely on Pi 5): The Pi 5's RP1 chip requires the lgpio C-library. gpiozero looks for it automatically. If you are in a Python virtual environment (venv), it cannot see the system-installed lgpio.
    Fix: Recreate your venv with system packages: python3 -m venv --system-site-packages env, or run pip install lgpio inside the venv.
  2. Legacy RPi.GPIO Interference: If your code explicitly imports RPi.GPIO before gpiozero, it will crash on the Pi 5 because the BCM2835 memory map doesn't exist on the new silicon.
    Fix: Purge RPi.GPIO imports. Rely entirely on gpiozero or rpi-lgpio.
  3. 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: Check vcgencmd get_throttled. If it returns anything other than 0x0, 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:

  1. Check Nginx Proxy Status:
    Run sudo 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 the pi user and the www-data group needs read/write access (handled by the -m 007 flag in the systemd file).
  2. Check Gunicorn Journal Logs:
    Run journalctl -u gpio-dashboard -n 50. This bypasses the web layer and shows you raw Python tracebacks. If you see Address already in use, a zombie Flask process is holding port 5000. Kill it with sudo fuser -k 5000/tcp.
  3. Verify GPIO Memory Permissions:
    Run ls -l /dev/gpiomem. The output should show crw-rw---- with the group gpio. Ensure your user (pi) is in the gpio group: groups pi. If not, run sudo usermod -aG gpio pi and 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.