To host a website on a Raspberry Pi that is robust enough for daily use, you need a two-tier stack: a reverse proxy (Nginx) to handle incoming HTTP traffic on port 80, and an application server (Python Flask) to serve your dynamic content and interact with hardware. While you can run Flask directly on port 80, doing so bypasses standard security boundaries and causes permission conflicts with GPIO access on modern Raspberry Pi OS.
This guide targets the Raspberry Pi 5 (8GB variant) running the 64-bit Raspberry Pi OS (Bookworm or later). We will build a web dashboard that toggles a physical LED, giving you a functional template for IoT control panels, sensor dashboards, or local network tools.
Project Overview & Hardware Spec Sheet
Time to Complete: 45 minutes
Target Board: Raspberry Pi 5 (8GB RAM)
SD cards are a liability for any web server due to write-cycle degradation from log files. For a 2026-standard build, we boot directly from an NVMe SSD. Below is the exact bill of materials.
| Component | Exact Variant / Model | Approx. Cost (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | $80.00 |
| Storage HAT | Pimoroni NVMe Base for Pi 5 | $15.00 |
| Storage Drive | 128GB M.2 2230 NVMe SSD (e.g., WD SN740) | $22.00 |
| Power Supply | Official Raspberry Pi 27W USB-C PD PSU | $12.00 |
| Indicator LED | 5mm Red LED + 330Ω 1/4W Resistor | $0.10 |
| Networking | Cat6 Ethernet Cable (Hardwired preferred over WiFi) | $5.00 |
GPIO Pin Mapping Table
We are using the gpiozero library, which is the officially supported GPIO interface for the Pi 5's RP1 southbridge chip. Legacy libraries like RPi.GPIO will fail on this board.
| Component | BCM GPIO Pin | Physical Pin | Wiring Note |
|---|---|---|---|
| LED Anode (+) | GPIO 17 | Pin 11 | Connect via 330Ω current-limiting resistor |
| LED Cathode (-) | N/A | Pin 9 (GND) | Direct to ground |
Step-by-Step: Installing Nginx and Flask
Before writing code, we need to prepare the OS environment. Ensure your Pi 5 is booted, connected to your network via Ethernet, and you are logged in via SSH.
- Update the system and install Nginx:
sudo apt update && sudo apt upgrade -y
sudo apt install nginx -y - Install Python virtual environment tools:
Never install Flask globally via pip on modern Debian-based systems; it breaks OS package managers. Use a virtual environment.
sudo apt install python3-venv python3-pip -y - Create your project directory and virtual environment:
mkdir ~/pi-web-server && cd ~/pi-web-server
python3 -m venv venv
source venv/bin/activate - Install Flask and gpiozero:
pip install flask gpiozero - Configure Nginx as a reverse proxy:
Open the default Nginx config:sudo nano /etc/nginx/sites-available/default.
Find thelocation /block and replace it with:
Save and restart Nginx:location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }sudo systemctl restart nginx.
pi) while still serving web pages on the standard HTTP port.
The Code: Flask Web Server with GPIO Control
Create a file named app.py in your project directory (nano app.py) and paste the following complete, compilable code. This script includes explicit pin definitions, HTML templating, and graceful error handling to ensure the GPIO pin resets if the server crashes.
from flask import Flask, render_template_string
from gpiozero import LED
import signal
import sys
import os
app = Flask(__name__)
# PIN DEFINITION: BCM GPIO 17 (Physical Pin 11)
# Using gpiozero which is fully compatible with Pi 5 RP1 chip
STATUS_LED = LED(17)
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>Pi 5 Hardware Dashboard</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; text-align: center; }
.btn { padding: 15px 30px; font-size: 18px; background: #005fcc; color: white; border: none; border-radius: 5px; cursor: pointer; }
.btn:hover { background: #0047a3; }
.status { font-weight: bold; color: {{ color }}; }
</style>
</head>
<body>
<h1>Raspberry Pi 5 Web Server</h1>
<p>LED Status: <span class="status">{{ state }}</span></p>
<a href="/toggle"><button class="btn">Toggle GPIO 17</button></a>
</body>
</html>
'''
@app.route('/')
def index():
state = 'ON' if STATUS_LED.is_lit else 'OFF'
color = 'green' if STATUS_LED.is_lit else 'red'
return render_template_string(HTML_TEMPLATE, state=state, color=color)
@app.route('/toggle')
def toggle():
STATUS_LED.toggle()
return index()
def graceful_shutdown(sig, frame):
"""Ensures GPIO pins are turned off when the server is stopped via Ctrl+C."""
print('\nShutting down server and resetting GPIO...')
STATUS_LED.off()
sys.exit(0)
# Bind the signal handler for SIGINT (Ctrl+C) and SIGTERM
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)
if __name__ == '__main__':
try:
# Host on 0.0.0.0 to accept connections from the local network
# Port 5000 is intercepted by Nginx reverse proxy
app.run(host='0.0.0.0', port=5000, debug=False)
except OSError as e:
print(f'FATAL: Failed to bind to port 5000. Error: {e}')
STATUS_LED.off()
sys.exit(1)
except Exception as e:
print(f'FATAL: Unexpected server error: {e}')
STATUS_LED.off()
sys.exit(1)
Run the application: python3 app.py. Open your desktop browser and navigate to your Pi's IP address (e.g., http://192.168.1.50). Nginx will route you to the Flask dashboard, and clicking the button will physically toggle the LED.
Debugging: Common Errors and Failure Modes
When deploying embedded web servers, things break at the intersection of network ports and Linux permissions. Here is how to diagnose the most frequent roadblocks.
Error 1: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Ranked Causes:
- Apache2 is pre-installed: Some third-party Pi images ship with Apache running on port 80 by default.
- Zombie Nginx process: A previous Nginx instance didn't shut down cleanly and is still holding the socket.
- Docker/Container conflict: Another container mapped to port 80 on the host network.
The Fix: Run sudo lsof -i :80 to identify the PID holding the port. If it's Apache, disable it: sudo systemctl disable apache2 && sudo systemctl stop apache2. If it's a zombie Nginx process, kill it with sudo kill -9 <PID> and restart the service.
Error 2: RuntimeError: No access to /dev/mem. Try running as root!
Ranked Causes:
- Using legacy RPi.GPIO: The old
RPi.GPIOlibrary attempts direct memory mapping, which is blocked on the Pi 5's RP1 chip and modern Bookworm OS kernels. - Missing user permissions: Your user is not in the
gpiogroup (rare on official images, but common on minimal Ubuntu Server builds).
The Fix: Uninstall the legacy library (pip uninstall RPi.GPIO) and ensure you are using gpiozero with the lgpio backend, which communicates via the standard /sys/class/gpio or character device interfaces safely without root.
- Is Nginx actually running? Check with
sudo systemctl status nginx. Look for 'active (running)'. - Is Flask listening? Run
sudo lsof -i :5000. If nothing returns, your Python script crashed before binding the socket. - Is it a firewall issue? Run
sudo ufw status. If active, ensure port 80 is allowed:sudo ufw allow 80/tcp.
Extending and Simplifying the Build
How to Simplify: If you don't need dynamic hardware control and just want to host static HTML/CSS files (like a portfolio or documentation site), delete the Flask setup entirely. Place your .html files directly into /var/www/html/. Nginx will serve them natively without a Python backend, reducing RAM usage to under 15MB.
How to Extend: To make this production-ready for 24/7 operation, you must daemonize the Flask app.
1. Install Gunicorn: pip install gunicorn.
2. Create a systemd service file at /etc/systemd/system/piweb.service that executes gunicorn -w 4 -b 127.0.0.1:5000 app:app.
3. Enable it with sudo systemctl enable --now piweb. This ensures the server survives reboots and handles concurrent web requests using multiple worker threads.
Frequently Asked Questions
Can I host a website on Raspberry Pi without a router?
Yes, but with limitations. If you connect a laptop directly to the Pi via Ethernet, you will need to configure a static link-local IP address (e.g., 169.254.x.x) on both machines, or run a DHCP server on the Pi. Without a router, you have no DNS resolution and no gateway to the internet, meaning your Pi cannot fetch external assets (like CDN-hosted CSS frameworks) and you cannot access the site via a domain name. For 99% of use cases, a standard home router is required to handle DHCP and local DNS.
How do I host a website on Raspberry Pi and access it from anywhere?
To expose your local Pi web server to the public internet, you have two main paths. The traditional method is Port Forwarding: log into your router, forward external port 80 (and 443 for HTTPS) to your Pi's local IP, and set up Dynamic DNS (DDNS). However, this exposes your home IP to the open web. The modern, safer alternative is using a reverse tunnel service like Cloudflare Tunnels or Tailscale. These create an outbound encrypted connection from your Pi to their edge network, allowing you to route a public domain to your Pi without opening any inbound ports on your home router.
Is it safe to host a website on Raspberry Pi connected to my home network?
It is safe if you follow basic network hygiene, but it carries inherent risks if exposed to the internet. If the site is strictly for local network use (e.g., a smart home dashboard), the risk is minimal. If you open it to the public internet, Nginx must be kept updated (sudo apt upgrade nginx), and you should absolutely implement Fail2Ban to block brute-force SSH and HTTP exploits. Furthermore, never run your web application as the root user; always use a reverse proxy and a standard user account to limit the blast radius if the application is compromised. For more on securing embedded Linux, refer to the official Raspberry Pi security documentation.






