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 Case | Board Variant | Web Stack | Verdict |
|---|---|---|---|
| Static HTML/CSS portfolio or documentation | Pi Zero 2 W ($15) | Lighttpd or Nginx | Overkill for dynamic, perfect for static. |
| Heavy database (MySQL) + PHP web app | Pi 4 Model B (8GB) | LAMP (Apache/PHP) | Good, but Pi 5 I/O is significantly faster. |
| IoT Dashboard, GPIO control, Sensor API | Pi 5 (4GB) | Python Flask + Gunicorn | DEFAULT PICK. Best balance of I/O, RAM, and native GPIO library support. |
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.
| Component | Exact Variant / Model | Approx. Cost |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 |
| Storage | SanDisk Extreme 32GB A2 U3 microSD | $12.00 |
| Environmental Sensor | Adafruit BME280 I2C Breakout (Product ID: 2652) | $15.00 |
| Actuator | HiLetgo 1-Channel 5V Relay Module (Optocoupler) | $6.00 |
| Power Supply | Official 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 Pin | Pi 5 Physical Pin | Pi 5 GPIO / Function | Wire Color (Standard) |
|---|---|---|---|
| BME280 VIN | Pin 1 | 3.3V Power | Red |
| BME280 GND | Pin 6 | Ground | Black |
| BME280 SDA | Pin 3 | GPIO 2 (SDA1) | Blue |
| BME280 SCL | Pin 5 | GPIO 3 (SCL1) | Yellow |
| Relay VCC | Pin 2 | 5V Power | Red |
| Relay GND | Pin 9 | Ground | Black |
| Relay IN | Pin 11 | GPIO 17 | Green |
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.
- 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. - 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. - Install System Dependencies: The Pi 5 uses the
lgpiolibrary for GPIO access, deprecating the oldRPi.GPIOC-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 - Verify I2C Hardware: Run
sudo i2cdetect -y 1. You should see77in the grid, which is the default I2C address for the Adafruit BME280. If the grid is empty, check your SDA/SCL wiring. - Create the Project Directory:
mkdir ~/iot-dashboard cd ~/iot-dashboard mkdir templates - 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.
- Is I2C actually enabled? Run
ls /dev/i2c*. If it returns "No such file", you forgot to enable it inraspi-configor forgot to reboot. - Is the port blocked? Run
sudo ss -tulpn | grep 5000. If another process is holding the port, Flask will crash on startup. - Is the GPIO backend installed? The Pi 5 requires
lgpio. Rundpkg -l | grep lgpioto 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 :5000to find the PID, thensudo 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
/devhardware mappings (rare, but happens in Docker). - Fix: Run the script natively on the host OS, or pass
--device /dev/i2c-1to 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.GPIOlibrary is either missing or incompatible, andlgpiois not installed. - Fix: Install the correct backend via APT:
sudo apt install python3-lgpio. Do not usepip install RPi.GPIOon 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:
- Delete the BME280 and Relay wiring.
- Uninstall Flask and gpiozero.
- Install Nginx:
sudo apt install nginx. - Drop your
index.htmlfile into/var/www/html/. - 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:
- Add a WSGI Server: Install Gunicorn (
sudo apt install gunicorn3) and run your app viagunicorn3 -w 4 -b 0.0.0.0:5000 app:app. This adds multi-processing to handle concurrent web requests. - 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.
- 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.






