Difficulty: Intermediate | Time: 45 minutes | Cost: ~$95

To host a dynamic IoT website on a Raspberry Pi, use a Raspberry Pi 5 (4GB) running Raspberry Pi OS Bookworm, Python 3.11, and the Flask microframework. This specific stack handles local sensor polling and GPIO switching without the memory overhead of a full LAMP stack or the compilation headaches of Node.js on ARM. You will have a live web dashboard reading physical I2C sensors and toggling GPIO relays in under an hour.

The Decision Path: Which Pi and Web Stack?

Before flashing an SD card, you need to match the board to the workload. Hosting a website on a Raspberry Pi ranges from serving a 50KB static HTML file to streaming live MJPEG camera feeds. Here is the decision matrix to pick your hardware and software stack.

Use CaseBoard VariantWeb StackVerdict
Static HTML/CSS portfolio or documentationPi Zero 2 W ($15)Lighttpd or NginxOverkill for dynamic, perfect for static.
Heavy database (MySQL) + PHP web appPi 4 Model B (8GB)LAMP (Apache/PHP)Good, but Pi 5 I/O is significantly faster.
IoT Dashboard, GPIO control, Sensor APIPi 5 (4GB)Python Flask + GunicornDEFAULT PICK. Best balance of I/O, RAM, and native GPIO library support.
The Concrete Pick: Buy the Raspberry Pi 5 (4GB). The 8GB variant is unnecessary unless you are running local LLMs or Docker containers alongside your web server. Pair it with Flask for the application layer.

Hardware Spec Sheet and GPIO Pin Mapping

This build integrates physical hardware so your website actually interacts with the real world. We are using a BME280 environmental sensor (I2C) and a 5V relay module (GPIO) to simulate an HVAC or lighting control system.

ComponentExact Variant / ModelApprox. Cost
MicrocontrollerRaspberry Pi 5 (4GB RAM)$60.00
StorageSanDisk Extreme 32GB A2 U3 microSD$12.00
Environmental SensorAdafruit BME280 I2C Breakout (Product ID: 2652)$15.00
ActuatorHiLetgo 1-Channel 5V Relay Module (Optocoupler)$6.00
Power SupplyOfficial Raspberry Pi 27W USB-C PD Power Supply$12.00

Pin Mapping Table

Wire the components to the Pi 5's 40-pin header as follows. Double-check your I2C lines; swapping SDA and SCL will result in silent I2C bus failures.

Component PinPi 5 Physical PinPi 5 GPIO / FunctionWire Color (Standard)
BME280 VINPin 13.3V PowerRed
BME280 GNDPin 6GroundBlack
BME280 SDAPin 3GPIO 2 (SDA1)Blue
BME280 SCLPin 5GPIO 3 (SCL1)Yellow
Relay VCCPin 25V PowerRed
Relay GNDPin 9GroundBlack
Relay INPin 11GPIO 17Green

Step-by-Step: Hosting a Website on Raspberry Pi 5

Follow these steps to prep the OS and install the dependencies. This guide assumes you are running Raspberry Pi OS Bookworm (64-bit), which is the current standard for the Pi 5.

  1. Flash the OS and Boot: Use Raspberry Pi Imager to flash Raspberry Pi OS (64-bit) to your A2 microSD card. Set your hostname to iot-pi, enable SSH, and configure your WiFi in the imager's advanced settings.
  2. Enable the I2C Interface: SSH into your Pi. Run sudo raspi-config. Navigate to Interface Options > I2C and select Yes to enable the ARM I2C interface. Reboot the Pi.
  3. Install System Dependencies: The Pi 5 uses the lgpio library for GPIO access, deprecating the old RPi.GPIO C-extension. Install the required Python packages via APT to ensure they compile correctly against the system Python:
    sudo apt update
    sudo apt install python3-flask python3-gpiozero python3-lgpio python3-smbus i2c-tools -y
  4. Verify I2C Hardware: Run sudo i2cdetect -y 1. You should see 77 in the grid, which is the default I2C address for the Adafruit BME280. If the grid is empty, check your SDA/SCL wiring.
  5. Create the Project Directory:
    mkdir ~/iot-dashboard
    cd ~/iot-dashboard
    mkdir templates
  6. Configure the Firewall: If you have UFW enabled, allow traffic on port 5000 (Flask's default):
    sudo ufw allow 5000/tcp

The Code: Flask IoT Dashboard with GPIO Error Handling

Save the following code as app.py in your ~/iot-dashboard directory. This script initializes the GPIO and I2C buses, handles hardware faults gracefully, and serves both a web UI and a JSON API.

import board
import adafruit_bme280
from gpiozero import OutputDevice
from flask import Flask, jsonify, render_template_string
import time
import sys

# --- PIN DEFINITIONS ---
RELAY_GPIO_PIN = 17  # Physical Pin 11

app = Flask(__name__)

# --- HARDWARE INITIALIZATION WITH ERROR HANDLING ---
try:
    # Initialize I2C sensor
    i2c = board.I2C()
    bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
    sensor_status = "Online"
except ValueError as e:
    print(f"[FATAL] BME280 not found on I2C bus. Check wiring. Error: {e}")
    bme280 = None
    sensor_status = "Offline - I2C Fault"

try:
    # Initialize GPIO Relay (Active Low for most HiLetgo modules)
    relay = OutputDevice(RELAY_GPIO_PIN, active_high=False, initial_value=False)
    gpio_status = "Online"
except Exception as e:
    print(f"[FATAL] GPIO initialization failed. Is lgpio installed? Error: {e}")
    relay = None
    gpio_status = "Offline - GPIO Fault"

# --- HTML TEMPLATE ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head><title>Pi 5 IoT Dashboard</title></head>
<body>
    <h1>Raspberry Pi 5 Environmental Control</h1>
    <p>Sensor Status: {{ sensor_status }} | GPIO Status: {{ gpio_status }}</p>
    <h2>Telemetry</h2>
    <ul>
        <li>Temperature: {{ temp }} °C</li>
        <li>Humidity: {{ hum }} %</li>
        <li>Pressure: {{ pres }} hPa</li>
    </ul>
    <h2>Relay Control</h2>
    <a href="/api/relay/on"><button>Turn ON</button></a>
    <a href="/api/relay/off"><button>Turn OFF</button></a>
</body>
</html>
"""

@app.route('/')
def dashboard():
    temp, hum, pres = "N/A", "N/A", "N/A"
    if bme280:
        try:
            temp = round(bme280.temperature, 2)
            hum = round(bme280.relative_humidity, 2)
            pres = round(bme280.pressure, 2)
        except OSError:
            pass # Sensor disconnected during runtime
    return render_template_string(HTML_TEMPLATE, temp=temp, hum=hum, pres=pres, 
                                  sensor_status=sensor_status, gpio_status=gpio_status)

@app.route('/api/relay/<state>')
def control_relay(state):
    if not relay:
        return jsonify({"error": "GPIO hardware unavailable"}), 500
    if state == 'on':
        relay.on()
        return jsonify({"status": "relay_on"})
    elif state == 'off':
        relay.off()
        return jsonify({"status": "relay_off"})
    return jsonify({"error": "Invalid state"}), 400

if __name__ == '__main__':
    # host='0.0.0.0' makes it accessible on your local network
    app.run(host='0.0.0.0', port=5000, debug=False)

Run the server using python3 app.py. Access the dashboard by navigating to http://<your-pi-ip>:5000 in your browser.

Debugging: Exact Error Strings and Ranked Causes

When hosting a website on a Raspberry Pi that interacts with hardware, you will inevitably hit OS-level or bus-level errors. Here are the exact strings you will see, ranked by probability, and how to fix them.

The First Three Things to Check When It Fails:
  1. Is I2C actually enabled? Run ls /dev/i2c*. If it returns "No such file", you forgot to enable it in raspi-config or forgot to reboot.
  2. Is the port blocked? Run sudo ss -tulpn | grep 5000. If another process is holding the port, Flask will crash on startup.
  3. Is the GPIO backend installed? The Pi 5 requires lgpio. Run dpkg -l | grep lgpio to verify the system package is present.

Error 1: OSError: [Errno 98] Address already in use

  • Cause A (Most Likely): You have a zombie Python process from a previous run still holding port 5000.
  • Fix: Run sudo lsof -i :5000 to find the PID, then sudo kill -9 <PID>.
  • Cause B: Another service (like a lingering Docker container or Node app) is bound to 5000.
  • Fix: Change the Flask port in the code to 5001, or stop the conflicting service.

Error 2: FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'

  • Cause A: The I2C kernel module is not loaded because it is disabled in the OS configuration.
  • Fix: Run sudo raspi-config, enable I2C, and reboot.
  • Cause B: You are running the script in a virtual environment that lacks access to the host's /dev hardware mappings (rare, but happens in Docker).
  • Fix: Run the script natively on the host OS, or pass --device /dev/i2c-1 to your Docker run command.

Error 3: gpiozero.exc.BadPinFactory: Unable to load any default pin factory

  • Cause: You are on a Pi 5 running Bookworm, and the legacy RPi.GPIO library is either missing or incompatible, and lgpio is not installed.
  • Fix: Install the correct backend via APT: sudo apt install python3-lgpio. Do not use pip install RPi.GPIO on a Pi 5; it will fail to compile or crash at runtime.

Extending and Simplifying the Build

Once you have the baseline dashboard running, you need to decide how to scale the project based on your actual deployment environment.

How to Simplify (The Static Route)

If you do not need live sensor data or GPIO control and just want to host a static HTML portfolio or documentation site on your Pi:

  1. Delete the BME280 and Relay wiring.
  2. Uninstall Flask and gpiozero.
  3. Install Nginx: sudo apt install nginx.
  4. Drop your index.html file into /var/www/html/.
  5. Start the service: sudo systemctl enable --now nginx.

This drops RAM usage to under 20MB and requires zero Python maintenance.

How to Extend (The Production Route)

Flask's built-in development server (app.run()) is single-threaded and not secure for the open internet. If you plan to expose this dashboard outside your local network, you must upgrade the stack:

  1. Add a WSGI Server: Install Gunicorn (sudo apt install gunicorn3) and run your app via gunicorn3 -w 4 -b 0.0.0.0:5000 app:app. This adds multi-processing to handle concurrent web requests.
  2. Add a Reverse Proxy: Install Nginx and configure it to forward port 80/443 traffic to Gunicorn's port 5000. This handles SSL termination and static file caching.
  3. Secure External Access: Do not open port 80 on your home router. Instead, install Cloudflare Tunnels (cloudflared) on the Pi. This creates a secure outbound-only tunnel to the internet, allowing you to access your Pi via a custom domain without exposing your home IP address to port scanners.

By starting with Flask and the Pi 5's native lgpio backend, you establish a robust foundation. You can iterate from a simple local sensor dashboard to a production-grade, cloud-tunneled IoT controller without rewriting your core Python logic.