If you want to host a website with Raspberry Pi to interact with physical hardware, a standard LAMP stack is overkill and a static HTML server is useless. The practical bench standard for embedded IoT dashboards is Python Flask running behind a Gunicorn WSGI server. This guide targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm, building a live web dashboard that reads a BME280 environmental sensor and toggles a 5V relay.
RPi.GPIO will fail. This build uses gpiozero with the lgpio backend, which is the officially supported hardware abstraction layer for Pi 5 in 2026.
Hardware Spec Sheet & Parts List
Do not underpower a Pi 5. The board requires a 5V/5A (27W) USB-C PD power supply to prevent peripheral brownouts when the relay kicks in and the CPU spikes.
| Component | Exact Model / Variant | Approx. Cost (2026) | Notes |
|---|---|---|---|
| Microcomputer | Raspberry Pi 5 (4GB RAM) | $60.00 | 4GB is sufficient for Flask + Gunicorn. 8GB only needed for Docker/ML. |
| Power Supply | Official 27W USB-C PD PSU | $12.00 | Mandatory for full 1.6A PCIe/HAT current limit. |
| Storage | 32GB microSD (A2 / V30 rating) | $9.00 | SanDisk Extreme or Samsung EVO Select. A1 cards will bottleneck I/O. |
| Sensor | BME280 I2C Breakout (3.3V) | $4.50 | Measures Temp, Humidity, Pressure. Ensure it has onboard pull-ups. |
| Actuator | 5V 1-Channel Relay Module | $2.00 | Optocoupler isolated, active-low trigger (JD-VCC jumper removed). |
GPIO Pin Mapping & Wiring
The Pi 5 GPIO header remains physically identical to the Pi 4 (40-pin), but the internal routing goes through the RP1 chip. Wire the components exactly as mapped below.
| Module Pin | Pi 5 GPIO / Rail | Physical Pin # | Wire Color (Standard) |
|---|---|---|---|
| BME280 VCC | 3.3V Power | Pin 1 | Red |
| BME280 GND | Ground | Pin 6 | Black |
| BME280 SDA | GPIO 2 (SDA1) | Pin 3 | Blue |
| BME280 SCL | GPIO 3 (SCL1) | Pin 5 | Yellow |
| Relay VCC | 5V Power | Pin 2 | Red |
| Relay GND | Ground | Pin 9 | Black |
| Relay IN | GPIO 17 | Pin 11 | Orange |
Step-by-Step: Hosting the Flask IoT Dashboard
Boot your Pi 5 into Raspberry Pi OS Bookworm (64-bit, Lite version preferred for headless servers). SSH into the board and follow these steps.
- Enable I2C: Run
sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot. - Verify Sensor: Install i2c-tools and scan the bus.
sudo apt install i2c-tools -y
i2cdetect -y 1
You should see76or77in the grid. - Prepare the Environment: Never install Python packages globally on Bookworm (PEP 668 restricts this). Create a virtual environment.
mkdir ~/iot-dashboard && cd ~/iot-dashboard
python3 -m venv venv
source venv/bin/activate - Install Dependencies:
pip install flask gunicorn adafruit-circuitpython-bme280 gpiozero lgpio - Create the App: Save the Python code from the next section into a file named
app.py. - Run with Gunicorn: Do not use the Flask development server in production. Bind to all interfaces on port 8080.
gunicorn -w 2 -b 0.0.0.0:8080 app:app
The Python Flask Code (With Error Handling)
This script initializes the hardware, defines the web routes, and includes explicit error handling for I2C bus timeouts—a common failure mode on the workbench.
import time
import board
import adafruit_bme280
from gpiozero import OutputDevice
from flask import Flask, jsonify, render_template_string
# --- Hardware Pin & Bus Definitions ---
RELAY_GPIO = 17
# Active-low relay module (IN pin pulled to GND to trigger)
relay = OutputDevice(RELAY_GPIO, active_high=False, initial_value=False)
# Initialize I2C bus and BME280 Sensor
i2c = board.I2C()
try:
# Most Adafruit/Clone boards use 0x77, some Bosch raw chips use 0x76
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
except ValueError:
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76)
app = Flask(__name__)
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head><title>Pi 5 IoT Dashboard</title></head>
<body>
<h2>Server Room Monitor</h2>
<p>Temperature: {{ temp }} °C</p>
<p>Humidity: {{ hum }} %</p>
<p>Relay Status: {{ status }}</p>
<form action="/toggle" method="POST">
<button type="submit">Toggle Exhaust Fan</button>
</form>
</body>
</html>
"""
@app.route('/')
def dashboard():
try:
t = round(sensor.temperature, 2)
h = round(sensor.humidity, 2)
status = "ON" if relay.is_active else "OFF"
return render_template_string(HTML_TEMPLATE, temp=t, hum=h, status=status)
except OSError as e:
# Catch I2C bus lockups or disconnected wires
return f"Hardware Fault: {e}", 500
@app.route('/api/data')
def api_data():
try:
return jsonify({
"temperature": sensor.temperature,
"humidity": sensor.humidity,
"relay_state": relay.is_active
})
except OSError as e:
return jsonify({"error": str(e)}), 500
@app.route('/toggle', methods=['POST'])
def toggle_relay():
relay.toggle()
time.sleep(0.1) # Debounce hardware settling
return dashboard()
if __name__ == '__main__':
# Fallback for local testing; use gunicorn for production
app.run(host='0.0.0.0', port=8080, debug=True)
Debugging: I2C "Remote I/O Error"
When working with I2C sensors on the Pi 5, the most notorious failure you will encounter is the bus locking up or dropping packets. If your dashboard returns a 500 error, check the server logs for this exact string:
OSError: [Errno 121] Remote I/O error
The First Three Things to Check:
- Run
i2cdetect -y 1: If the grid is empty or showsUU, the kernel driver has claimed the bus or the physical connection is dead. - Check Physical Pull-ups: Measure the SDA and SCL lines with a multimeter. They should read ~3.3V when idle. If they read 0V or float, your breakout board lacks pull-up resistors. Add 4.7kΩ resistors to the 3.3V rail.
- Verify Power Supply Voltage: If the Pi 5 is under heavy load and the PSU sags below 4.8V, the RP1 chip will drop I2C transactions. Check the 5V rail (Pin 2/4) under load.
Ranked Causes & Fixes:
- Cause 1: Loose Dupont Wires (60% of cases). Breadboards wear out. Solder the BME280 to a perfboard or use screw-terminal HATs for permanent installs.
- Cause 2: Bus Lockup from Interrupted Writes (30%). If the Pi reboots while the sensor is transmitting, the sensor holds SDA low. Fix: Power cycle the sensor, or write a script to bit-bang 9 clock pulses on SCL to clear the bus.
- Cause 3: EMI from Relay Switching (10%). Switching a 120V AC load via the relay generates a voltage spike that scrambles the I2C logic. Fix: Route I2C wires away from the relay load wires and add a flyback diode across the relay coil.
Extending and Simplifying the Build
How to Simplify: If you just need a static status page and don't want to manage Python environments, strip out Flask entirely. Write a bash script triggered by cron every 5 minutes that reads the sensor via i2cget and overwrites an index.html file served by a basic nginx instance. This drops RAM usage from ~60MB to under 5MB.
How to Extend: To make this production-ready for a commercial greenhouse or server room:
- Reverse Proxy: Put Nginx in front of Gunicorn to handle SSL termination (HTTPS) and static asset caching.
- Systemd Service: Create a
/etc/systemd/system/iot-dashboard.servicefile so the web server automatically restarts on boot or crash. - Database: Add
sqlite3or InfluxDB to log historical temperature data, and integrate Chart.js on the frontend to render 24-hour trend graphs.
Frequently Asked Questions
Is it safe to host a website with Raspberry Pi exposed to the internet?
Port-forwarding your home router directly to the Pi 5 (exposing port 80/443) is highly discouraged; it invites botnet brute-force attacks within hours. The safest modern method is to use Cloudflare Tunnels (cloudflared). It creates an outbound, encrypted tunnel from your Pi to Cloudflare's edge network, allowing you to host a website with Raspberry Pi securely without opening any inbound firewall ports or needing a static IP.
How much does it cost to host a website with Raspberry Pi 5 in 2026?
The upfront hardware cost is roughly $85 (Pi 5, PSU, SD card, and basic sensors). The ongoing cost is electricity. A Pi 5 idling with a Flask server draws about 3.5 watts. At the US average of $0.16/kWh, running the server 24/7 costs approximately $4.90 per year. Compared to a $6/month VPS (Virtual Private Server), the Pi pays for itself in 14 months, though you trade cloud reliability for local hardware control.
Can I host a website with Raspberry Pi without a static IP?
Yes. If your ISP uses CGNAT or dynamic IPs, traditional DDNS (Dynamic DNS) port forwarding will fail. As mentioned above, Cloudflare Tunnels or Tailscale bypass this entirely. Tailscale is ideal if the dashboard is only for your own use (creating a private mesh network), while Cloudflare Tunnels is better if you need to share the dashboard publicly with custom domain routing.
Why does my Pi 5 throttle when the relay clicks?
The Raspberry Pi 5 has aggressive brownout protection. If you are using an older 15W (5V/3A) Pi 4 power supply, the moment the relay coil energizes, it pulls an extra 300mA. This drops the voltage rail, triggering the Pi's firmware to throttle the CPU to 600MHz and disable USB/PCIe peripherals. Always use the official 27W USB-C PD supply for Pi 5 embedded projects.






