How to Run a Web Server on a Raspberry Pi and Make It Publicly Accessible
A Raspberry Pi running Nginx consumes about 3 to 5 watts of power and can comfortably serve a personal website, portfolio, API, or small web application to the public internet around the clock, at a cost of a few dollars per year in electricity. The challenge has never been the server software itself — it is getting traffic from the public internet into a device sitting behind your home router. This guide covers the complete path: choosing the right Pi model, installing and configuring Nginx with PHP support, serving your first pages, and using a Localtonet tunnel to give your server a stable public HTTPS address without port forwarding, a static IP, or a domain purchase.
Which Raspberry Pi Should You Use as a Web Server?
Not all Raspberry Pi models are equally suited to running a permanent web server. Here is what actually matters for this use case:
| Model | CPU | Max RAM | Web Server Suitability | Power Draw |
|---|---|---|---|---|
| Pi Zero 2 W | Cortex-A53 quad-core 1GHz | 512 MB | Static sites only. PHP is sluggish. Not suitable for dynamic applications. | ~1-2W |
| Pi 3B+ | Cortex-A53 quad-core 1.4GHz | 1 GB | Works for light PHP sites. RAM is tight for anything database-backed. | ~3-4W |
| Pi 4 (4GB) | Cortex-A72 quad-core 1.8GHz | 8 GB | Excellent. Handles WordPress, PHP applications, multiple virtual hosts comfortably. | ~4-6W |
| Pi 5 (4/8GB) | Cortex-A76 quad-core 2.4GHz | 16 GB | Outstanding. Up to 3x faster than Pi 4. Overkill for a static site, ideal for anything dynamic. | ~5-10W under load |
For this guide, any Pi 4 or Pi 5 gives the best experience. The instructions work on all models, but the performance notes are calibrated for Pi 4 and Pi 5. If you have a Pi 3B+ collecting dust, it will work for a simple personal site.
SD card vs SSD: use an SSD if this is for serious use
SD cards have limited write endurance. A web server that writes access logs, processes forms, or runs a database will wear out an SD card in months to years of continuous use. On Pi 4, a USB 3.0 SSD is the right choice. On Pi 5, the PCIe M.2 HAT lets you connect an NVMe SSD directly, making the Pi 5 an extremely capable server platform. For learning and experimentation, an SD card is fine. For something that runs permanently, use an SSD.
Step 1: Flash Raspberry Pi OS and Configure SSH
Download and install Raspberry Pi Imager from raspberrypi.com. Use it to flash Raspberry Pi OS Lite (64-bit) onto your SD card or SSD. For a server, you do not need the desktop environment. The Lite version boots faster, uses less RAM, and has fewer background processes competing with your web server.
In Raspberry Pi Imager, click the gear icon (or press Ctrl+Shift+X) to open the OS customization settings. Here you can set your hostname (e.g. mypi), enable SSH with a password or public key, and configure your Wi-Fi credentials. Setting these up before flashing means your Pi is accessible over the network on first boot without needing a monitor or keyboard. This is called a headless setup.
After flashing and booting, find your Pi on the network and connect:
ssh pi@mypi.local
# or use the IP address if mDNS doesn't work in your network:
ssh pi@192.168.1.XXX
Once connected, update the system before installing anything:
sudo apt update && sudo apt upgrade -y
This can take a few minutes. When it finishes, your Pi is running the latest version of Raspberry Pi OS Bookworm, which is based on Debian 12.
Nginx or Apache: Which to Use on a Pi?
Both are available in the Raspberry Pi OS package repository, and both will work for a personal web server. The difference matters more on limited hardware.
Apache uses a process-per-connection model: each incoming request spawns or borrows a process. For low traffic, this works fine. Under heavier concurrent load, Apache's memory usage grows significantly. On a Pi 4 with 4GB RAM, this is rarely a problem in practice.
Nginx uses an event-driven, asynchronous architecture: a single worker process handles many connections simultaneously through non-blocking I/O. On constrained hardware, Nginx consistently uses less RAM at idle and stays stable under concurrent load. It does not natively process PHP the way Apache does, it delegates PHP execution to a separate process called PHP-FPM, which adds one configuration step but results in better performance.
This guide uses Nginx. For a home web server on Pi hardware, Nginx is the correct choice. Apache is fine if you already know it and prefer its .htaccess configuration model.
Step 2: Install Nginx
sudo apt install nginx -y
After installation, Nginx starts automatically. Verify it is running:
sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Wed 2025-03-05 10:12:34 GMT; 23s ago
Main PID: 1847 (nginx)
Tasks: 5 (limit: 9257)
CGroup: /system.slice/nginx.service
├─1847 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
├─1848 nginx: worker process
├─1849 nginx: worker process
├─1850 nginx: worker process
└─1851 nginx: worker process
You will see one master process and four worker processes, one per CPU core (Pi 4 and Pi 5 both have quad-core processors). Find your Pi's local IP address and open it in a browser on another device on the same network:
hostname -I
192.168.1.45
Navigate to http://192.168.1.45 (use your actual IP) in a browser. You should see the Nginx default welcome page confirming the server is working.
Step 3: Understanding the Nginx File Structure
Before making any changes, it helps to understand where Nginx keeps its files on a Debian-based system like Raspberry Pi OS:
| Path | Purpose |
|---|---|
/etc/nginx/nginx.conf | Main Nginx configuration. Sets global settings like worker count, logging, and includes other config files. |
/etc/nginx/sites-available/ | Directory where you create server block configuration files. Files here are not active by default. |
/etc/nginx/sites-enabled/ | Symlinks to files in sites-available. Only files symlinked here are loaded by Nginx. |
/var/www/html/ | Default document root. Put your HTML files here, or change the root in your server block. |
/var/log/nginx/access.log | Access log: one line per request. Useful for traffic analysis and debugging. |
/var/log/nginx/error.log | Error log: permission problems, failed PHP requests, configuration errors. Check this first when something breaks. |
The workflow for adding a new site is: create a configuration file in sites-available, create a symlink to it in sites-enabled, test the configuration, then reload Nginx.
Step 4: Serve Your First Static Site
Create a directory for your site content and set permissions so Nginx can read it:
sudo mkdir -p /var/www/mysite
sudo chown -R $USER:$USER /var/www/mysite
sudo chmod -R 755 /var/www/mysite
Create a simple index page to verify everything works:
cat > /var/www/mysite/index.html << 'EOF'
My Raspberry Pi Server
Hello from my Raspberry Pi!
This page is being served by Nginx on a Raspberry Pi.
EOF
Create a server block configuration file for your site:
sudo nano /etc/nginx/sites-available/mysite
server {
listen 80;
listen [::]:80;
# server_name is a placeholder for now.
# After setting up Localtonet, replace _ with your public hostname.
server_name _;
root /var/www/mysite;
index index.html index.htm;
# Standard try_files for static sites.
# Tries to serve the request as a file, then as a directory,
# and returns 404 if neither exists.
location / {
try_files $uri $uri/ =404;
}
# Deny access to hidden files (e.g. .git, .env).
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
access_log /var/log/nginx/mysite-access.log;
error_log /var/log/nginx/mysite-error.log;
}
Enable the site by creating a symlink, test the configuration for syntax errors, and reload Nginx:
# Disable the default site
sudo rm /etc/nginx/sites-enabled/default
# Enable your site
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
# Test the configuration syntax
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
sudo systemctl reload nginx
Visit http://192.168.1.45 again. You should see your custom page instead of the Nginx default. Always run sudo nginx -t before reloading. A syntax error in a configuration file will prevent Nginx from reloading, and the error will be clearly reported.
Step 5: Add PHP Support with PHP-FPM
Nginx does not process PHP natively. To run PHP, you install PHP-FPM (FastCGI Process Manager), a separate service that runs PHP scripts and communicates with Nginx via a Unix socket. On Raspberry Pi OS Bookworm, the default PHP version available in the standard repository is PHP 8.2.
sudo apt install php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip -y
This installs PHP 8.2 and modules needed by most PHP applications. php-fpm installs php8.2-fpm automatically. Verify it is running:
sudo systemctl status php8.2-fpm
● php8.2-fpm.service - The PHP 8.2 FastCGI Process Manager
Loaded: loaded (/lib/systemd/system/php8.2-fpm.service; enabled; preset: enabled)
Active: active (running) since Wed 2025-03-05 10:18:22 GMT; 12s ago
Main PID: 2043 (php-fpm8.2)
Status: "Processes active: 0, idle: 2, Requests: 0"
Confirm the PHP-FPM socket file exists. Nginx will communicate with PHP-FPM through this file:
ls /var/run/php/
php8.2-fpm.pid php8.2-fpm.sock php-fpm.sock
The file php8.2-fpm.sock is what Nginx will use. Now update your site configuration to enable PHP processing:
sudo nano /etc/nginx/sites-available/mysite
server {
listen 80;
listen [::]:80;
server_name _;
root /var/www/mysite;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Pass .php files to PHP-FPM via the Unix socket.
# The socket path /var/run/php/php8.2-fpm.sock is correct for
# Raspberry Pi OS Bookworm with the default apt-installed PHP 8.2.
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
}
# Block access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
access_log /var/log/nginx/mysite-access.log;
error_log /var/log/nginx/mysite-error.log;
}
The socket file path /var/run/php/php8.2-fpm.sock is correct for Raspberry Pi OS Bookworm with PHP 8.2 installed from the standard apt repository. If you install a different PHP version, the path will change to match — for example, /var/run/php/php8.3-fpm.sock for PHP 8.3. If Nginx returns a 502 Bad Gateway error on PHP pages, the most common cause is a mismatch between the socket path in the Nginx config and the actual socket file. Always verify with ls /var/run/php/ and use the path that ends in .sock (not .pid).
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
Create a test PHP file to verify PHP is working:
echo "" | sudo tee /var/www/mysite/info.php
Navigate to http://192.168.1.45/info.php in a browser. You should see the PHP information page showing PHP 8.2 running on your Pi. After verifying, delete this file — it exposes detailed server configuration and should not remain publicly accessible:
sudo rm /var/www/mysite/info.php
Step 6: Add a Database (Optional)
If your application needs a database — WordPress, a custom PHP app, or anything that stores data — install MariaDB. MariaDB is the standard MySQL-compatible database in Raspberry Pi OS and is a direct drop-in replacement for MySQL:
sudo apt install mariadb-server -y
sudo mysql_secure_installation
The mysql_secure_installation script walks through securing your database installation: setting a root password, removing anonymous users, disabling remote root login, and removing the test database. Answer yes to all prompts for a secure default setup.
Create a database and user for your application:
sudo mysql -u root -p
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'myapp_user'@'localhost' IDENTIFIED BY 'use-a-strong-password-here';
GRANT ALL PRIVILEGES ON myapp.* TO 'myapp_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
The LEMP stack (Linux, Nginx, MariaDB, PHP) is now complete. Your Pi can serve dynamic PHP applications backed by a database.
Step 7: Tune Nginx for Raspberry Pi
The default Nginx configuration is conservative and works well, but a few adjustments improve performance on Pi hardware:
sudo nano /etc/nginx/nginx.conf
Find and adjust these settings in the http block:
# One worker per CPU core. Pi 4 and Pi 5 both have 4 cores.
# 'auto' sets this automatically to the number of CPU cores.
worker_processes auto;
http {
# Enable sendfile for efficient static file serving
sendfile on;
# Optimize TCP packet transmission
tcp_nopush on;
tcp_nodelay on;
# How long to keep a keep-alive connection open
# 65 seconds is a good balance for home servers
keepalive_timeout 65;
# Enable gzip compression for text content
# Reduces bandwidth and speeds up page loads significantly
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/javascript application/json application/xml
application/rss+xml font/ttf image/svg+xml;
# Security: hide Nginx version in Server header
server_tokens off;
# Include site configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
sudo nginx -t && sudo systemctl reload nginx
Step 8: Basic Security Hardening
A web server accessible from the public internet needs several security measures applied before exposure. These are the most important ones for a Pi web server:
Enable the firewall
Raspberry Pi OS Bookworm includes UFW (Uncomplicated Firewall). Enable it and allow only the ports you need:
sudo apt install ufw -y
# Allow SSH (do this before enabling UFW or you will lock yourself out)
sudo ufw allow ssh
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable the firewall
sudo ufw enable
# Verify the rules
sudo ufw status
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
Security headers in Nginx
Add these headers to your site's server block inside the server {} section. They protect against a range of common browser-side attacks:
# Prevent browsers from MIME-sniffing the Content-Type
add_header X-Content-Type-Options nosniff;
# Prevent clickjacking attacks
add_header X-Frame-Options SAMEORIGIN;
# Enable XSS protection in older browsers
add_header X-XSS-Protection "1; mode=block";
# Control how much referrer info is sent
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Only send traffic over HTTPS once a visitor has connected
# Enable this after HTTPS is working
# add_header Strict-Transport-Security "max-age=31536000" always;
Disable SSH password authentication
If you set up SSH key authentication (recommended), disable password login to prevent brute-force SSH attacks:
sudo nano /etc/ssh/sshd_config
Find and set:
PasswordAuthentication no
PubkeyAuthentication yes
sudo systemctl restart sshd
Step 9: Make Your Pi Web Server Publicly Accessible with Localtonet
Your Raspberry Pi web server is now running locally. To access it from outside your home network, you need to get public internet traffic to it. The traditional approach requires port forwarding on your router, a static IP address from your ISP, and a domain name. None of that is needed with a Localtonet tunnel.
Localtonet creates an outbound connection from your Pi to Localtonet's servers. Incoming public traffic arrives at Localtonet's infrastructure and is forwarded through that established connection to your Pi. This works through NAT, CGNAT, and without any router configuration. Your Pi gets a stable public HTTPS URL immediately.
Install and authenticate Localtonet
Download the Localtonet client for Linux ARM64 (for Pi 4 and Pi 5):
# Download Localtonet (check localtonet.com for the latest version)
wget https://localtonet.com/download/localtonet-linux-arm64.zip
unzip localtonet-linux-arm64.zip
chmod +x localtonet
sudo mv localtonet /usr/local/bin/
Authenticate with your token from the Localtonet dashboard:
localtonet authtoken YOUR_TOKEN_HERE
Create an HTTP tunnel
In the Localtonet web dashboard, create a new tunnel with these settings:
| Setting | Value |
|---|---|
| Protocol | HTTP |
| Local IP | 127.0.0.1 |
| Local Port | 80 |
| Subdomain | Choose a name, e.g. mypi → mypi.localto.net |
Start the Localtonet client on your Pi:
localtonet
Localtonet is running...
Tunnel active: https://mypi.localto.net → http://127.0.0.1:80
Navigate to https://mypi.localto.net from any device anywhere in the world. Your Raspberry Pi is now serving your site over HTTPS with a TLS certificate provided automatically by Localtonet.
Update the Nginx server_name
Update your server block to recognize your public hostname so Nginx can handle it correctly:
server_name mypi.localto.net;
sudo nginx -t && sudo systemctl reload nginx
Run Localtonet as a service so it survives reboots
Currently, the tunnel stops when you close the terminal. To make it permanent, install Localtonet as a systemd service:
sudo nano /etc/systemd/system/localtonet.service
[Unit]
Description=Localtonet Tunnel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
ExecStart=/usr/local/bin/localtonet
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable localtonet
sudo systemctl start localtonet
sudo systemctl status localtonet
The tunnel now starts automatically when the Pi boots and restarts if the process exits unexpectedly. Your web server is publicly accessible whenever the Pi is powered on and connected to the internet.
Step 10: Hosting Multiple Sites (Virtual Hosts)
Nginx can serve multiple completely separate sites from the same Pi, each with its own domain or subdomain. Create separate Localtonet tunnels (one per site) and configure separate server blocks:
server {
listen 80;
server_name myproject.localto.net;
root /var/www/site2;
index index.php index.html;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
}
location ~ /\. {
deny all;
}
}
sudo ln -s /etc/nginx/sites-available/site2 /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Nginx uses the Host header in incoming requests to decide which server block handles the request. When Localtonet forwards traffic from myproject.localto.net, it passes the original hostname in the host header, and Nginx routes it to the correct server block automatically.
What Can You Host on a Raspberry Pi Web Server?
Common Problems and Solutions
| Problem | Likely Cause | Fix |
|---|---|---|
| 502 Bad Gateway on PHP pages | Nginx cannot reach the PHP-FPM socket — wrong socket path or PHP-FPM is not running | Run ls /var/run/php/ to find the actual socket path. Verify it ends in .sock. Check PHP-FPM is running: sudo systemctl status php8.2-fpm. Start it if stopped: sudo systemctl start php8.2-fpm. |
| 403 Forbidden on files you can see in the directory | File or directory permissions are too restrictive for the www-data user that Nginx runs as |
Run sudo chmod -R 755 /var/www/mysite and sudo chown -R www-data:www-data /var/www/mysite. Directories need execute permission (755) and files need read permission (644). |
| PHP file is downloaded instead of executed | The PHP location block in the Nginx config is missing or commented out | Verify the location ~ \.php$ block is present and uncommented in your site config. Test and reload: sudo nginx -t && sudo systemctl reload nginx. |
| nginx -t fails with "unknown directive" | Typo or syntax error in a configuration file | Read the error message carefully — it includes the file path and line number. Fix the line it points to. |
| Site unreachable after enabling UFW | Forgot to allow port 80 (or 22 for SSH) before enabling UFW | If you locked yourself out of SSH, connect a keyboard and monitor to the Pi. Run sudo ufw allow ssh and sudo ufw allow 80/tcp before re-enabling. |
| Localtonet tunnel connects but public URL shows blank page | Nginx server_name does not match the hostname Localtonet sends in the Host header |
Set server_name mypi.localto.net; to match your Localtonet subdomain exactly, or use server_name _; as a catch-all for testing. |
| Changes to PHP files not reflected in browser | Browser cache or PHP OPcache serving the old version | Hard refresh the browser (Ctrl+Shift+R). For development, you can disable OPcache by setting opcache.enable=0 in /etc/php/8.2/fpm/php.ini and restarting php8.2-fpm. |
Frequently Asked Questions
Can I use a custom domain instead of the localto.net subdomain?
Yes. In the Localtonet dashboard, you can configure a custom domain for your tunnel. You point a CNAME DNS record at Localtonet's servers, and incoming requests to your domain are forwarded to your Pi just like the localto.net subdomain. Localtonet handles the TLS certificate for your custom domain automatically. Update your Nginx server_name to the custom domain name after configuring this.
How much traffic can a Raspberry Pi handle?
For static HTML and CSS files, a Raspberry Pi 4 running Nginx can handle thousands of concurrent connections. Nginx's event-driven architecture is very efficient for static content and the Pi's Gigabit Ethernet provides plenty of network bandwidth. PHP pages are slower because each request executes PHP code, a Pi 4 can comfortably handle tens to low hundreds of simultaneous PHP requests depending on how complex the PHP is. For a personal site, portfolio, small blog, or API with modest traffic, a Pi 4 is more than sufficient. For a high-traffic production application, you would want a VPS or dedicated server instead.
My ISP is behind CGNAT and my router doesn't have a public IP. Does this still work?
Yes. CGNAT is exactly the problem Localtonet tunnels solve. With CGNAT, your router does not have a public IP address, which means port forwarding is impossible inbound connections from the internet cannot reach your home network at all. Localtonet works by having your Pi initiate an outbound connection to Localtonet's servers. Since outbound connections work even behind CGNAT, the tunnel establishes successfully. Incoming traffic arrives at Localtonet's infrastructure (which does have a public IP) and returns to your Pi through the established outbound connection.
What happens to the website when the Pi loses power or restarts?
Nginx is configured to start automatically on boot (this is the default after installation). The Localtonet service configured in this guide also starts automatically on boot and will reconnect. After a reboot, your website should be back online and accessible at its public URL within a minute or two of the Pi finishing its startup sequence, assuming it has network connectivity.
Can I run WordPress on a Raspberry Pi?
Yes. WordPress runs on the LEMP stack (Nginx, MariaDB, PHP) covered in this guide. A Pi 4 with 4GB RAM handles a lightly loaded WordPress site comfortably. Performance improves significantly if you use a caching plugin (WP Super Cache or W3 Total Cache) to serve cached HTML instead of executing PHP on every request. A WordPress site with active caching enabled behaves much like a static site from Nginx's perspective, which is very fast on Pi hardware. Boot from an SSD rather than an SD card for much better database performance.
Your Raspberry Pi Web Server, Accessible from Anywhere
Nginx + PHP-FPM gives you a complete web server stack on a $35 to $80 device. Localtonet turns it into a publicly accessible HTTPS server in under two minutes, without port forwarding, without a static IP, and without a domain purchase. Create your first tunnel and put your Pi on the internet today.
Create a Free Localtonet Tunnel →