How to Build a Personal Portfolio Website on a Raspberry Pi and Make It Live
A Raspberry Pi sitting on your desk costs roughly two to four dollars a year to run continuously. For a personal portfolio, a CV page, or a project showcase, that is all the server you need. This guide walks through building a clean portfolio site with HTML and CSS, serving it with Nginx on a Raspberry Pi, and giving it a public HTTPS address using a Localtonet tunnel, without touching your router, owning a public IP, or paying for hosting.
📋 What's in this guide
Why Host a Portfolio on a Raspberry Pi?
Most developers rent a VPS or use a shared hosting plan for personal sites. That makes sense for production applications with real traffic. For a portfolio that gets a few hundred visitors a month, it is unnecessary. A Raspberry Pi 4 with Nginx can serve a static portfolio to thousands of concurrent visitors and consume less power than a phone charger doing it.
The more interesting reason to host your own portfolio is the signal it sends.
A URL like https://yourname.com that points at hardware you own and configured yourself
tells a hiring manager or client more about your capabilities than the same site
hosted on Netlify or Vercel. It is a project in itself, not just a deployment.
Step 1: Prepare the Raspberry Pi
Any Raspberry Pi from the Pi 3 onwards works for this guide. A Pi 4 or Pi 5 is recommended for comfort, but a Pi 3B+ handles a static portfolio site without any issues.
Flash Raspberry Pi OS Lite
Download Raspberry Pi Imager and flash Raspberry Pi OS Lite (64-bit) to your SD card. In the Imager's advanced settings, set a hostname, enable SSH, and configure your Wi-Fi credentials before writing. This lets you connect headlessly without a monitor or keyboard.
Connect and update
# Find the Pi's IP (replace with your Pi's hostname)
ssh pi@raspberrypi.local
# Update the system
sudo apt update && sudo apt upgrade -y
Assign a static local IP (recommended)
A static local IP keeps the Pi reachable at the same address on your network.
The easiest way is to set a DHCP reservation in your router settings using the Pi's MAC address.
Alternatively, edit /etc/dhcpcd.conf directly on the Pi.
sudo nano /etc/dhcpcd.conf
Add these lines at the bottom, replacing the values with your network details:
interface eth0
static ip_address=192.168.1.100/24
static routers=192.168.1.1
static domain_name_servers=8.8.8.8
sudo reboot
Step 2: Install and Configure Nginx
sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
# Verify it is running
sudo systemctl status nginx
Open a browser on any machine on your local network and visit http://192.168.1.100
(replace with your Pi's IP). You should see the Nginx welcome page.
Nginx is working.
Now create a dedicated configuration for your portfolio site. The web root is where your HTML, CSS, and image files will live.
# Create the web root directory
sudo mkdir -p /var/www/portfolio
# Give your user write access
sudo chown -R $USER:$USER /var/www/portfolio
Create the Nginx site configuration:
sudo nano /etc/nginx/sites-available/portfolio
Paste the following:
server {
listen 80;
listen [::]:80;
root /var/www/portfolio;
index index.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
# Cache static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Gzip compression
gzip on;
gzip_types text/plain text/css application/javascript image/svg+xml;
}
# Enable the site
sudo ln -s /etc/nginx/sites-available/portfolio /etc/nginx/sites-enabled/
# Remove the default site
sudo rm /etc/nginx/sites-enabled/default
# Test the configuration
sudo nginx -t
# Apply it
sudo systemctl reload nginx
Step 3: Build the Portfolio Site
A portfolio does not need a framework or a build tool. Clean HTML and CSS is faster to load, easier to maintain, and works everywhere. The structure below is a minimal but complete starting point you can customise however you like.
File structure
portfolio/
├── index.html
├── style.css
└── assets/
└── profile.jpg (your photo)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Name | Developer</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<nav>
<span class="logo">Your Name</span>
<ul>
<li><a href="#about">About</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<section class="hero" id="about">
<img src="assets/profile.jpg" alt="Your Name" class="avatar">
<h1>Hi, I'm Your Name</h1>
<p class="tagline">Full-stack developer. I build things for the web.</p>
<div class="links">
<a href="https://github.com/yourusername" target="_blank">GitHub</a>
<a href="https://linkedin.com/in/yourusername" target="_blank">LinkedIn</a>
<a href="mailto:you@example.com">Email</a>
</div>
</section>
<section class="projects" id="projects">
<h2>Projects</h2>
<div class="project-grid">
<article class="card">
<h3>Project One</h3>
<p>A short description of what this project does and what technologies it uses.</p>
<a href="https://github.com/yourusername/project-one" target="_blank">View on GitHub</a>
</article>
<article class="card">
<h3>Project Two</h3>
<p>Another project description. Keep it concise and focused on impact.</p>
<a href="https://github.com/yourusername/project-two" target="_blank">View on GitHub</a>
</article>
<article class="card">
<h3>This Site</h3>
<p>Self-hosted on a Raspberry Pi 4 with Nginx and a Localtonet tunnel. Costs about $3/year to run.</p>
<a href="https://localtonet.com" target="_blank">Powered by Localtonet</a>
</article>
</div>
</section>
<section class="contact" id="contact">
<h2>Get in Touch</h2>
<p>I am open to freelance projects, full-time roles, and interesting collaborations.</p>
<a href="mailto:you@example.com" class="cta-button">Send an Email</a>
</section>
</main>
<footer>
<p>Hosted on a Raspberry Pi. Built with HTML and CSS.</p>
</footer>
</body>
</html>
style.css
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background: #f8f9fa;
}
header {
background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.08);
position: sticky;
top: 0;
z-index: 100;
}
nav {
max-width: 900px;
margin: 0 auto;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
nav .logo { font-weight: 700; font-size: 1.1rem; }
nav ul { list-style: none; display: flex; gap: 2rem; }
nav a { text-decoration: none; color: #555; font-size: .9rem; }
nav a:hover { color: #0ea5e9; }
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem; }
.hero {
text-align: center;
padding: 4rem 0;
}
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1.5rem;
border: 3px solid #0ea5e9;
}
.hero h1 { font-size: 2.25rem; margin-bottom: .5rem; }
.tagline { color: #555; font-size: 1.1rem; margin-bottom: 1.5rem; }
.links { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.links a {
padding: .5rem 1.25rem;
border: 2px solid #0ea5e9;
border-radius: 6px;
text-decoration: none;
color: #0ea5e9;
font-weight: 600;
font-size: .9rem;
transition: all .2s;
}
.links a:hover { background: #0ea5e9; color: #fff; }
.projects, .contact { padding: 3rem 0; }
h2 { font-size: 1.75rem; margin-bottom: 1.5rem; }
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.25rem;
}
.card {
background: #fff;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,.06);
}
.card h3 { margin-bottom: .5rem; font-size: 1.1rem; }
.card p { color: #555; font-size: .9rem; margin-bottom: 1rem; }
.card a { color: #0ea5e9; font-size: .9rem; font-weight: 600; text-decoration: none; }
.card a:hover { text-decoration: underline; }
.contact { text-align: center; }
.contact p { color: #555; margin-bottom: 1.5rem; }
.cta-button {
display: inline-block;
padding: .75rem 2rem;
background: #0ea5e9;
color: #fff;
border-radius: 6px;
text-decoration: none;
font-weight: 700;
transition: background .2s;
}
.cta-button:hover { background: #0284c7; }
footer {
text-align: center;
padding: 2rem;
color: #999;
font-size: .85rem;
border-top: 1px solid #eee;
margin-top: 2rem;
}
@media (max-width: 600px) {
.hero h1 { font-size: 1.75rem; }
nav ul { gap: 1rem; }
}
Step 4: Deploy Your Files to the Pi
Copy your portfolio files from your development machine to the Pi's web root using rsync or scp.
# From your local machine — replace pi-ip with your Pi's IP
rsync -avz ./portfolio/ pi@192.168.1.100:/var/www/portfolio/
# Or with scp
scp -r ./portfolio/* pi@192.168.1.100:/var/www/portfolio/
Visit http://192.168.1.100 in your browser on the same network.
Your portfolio site should load.
If you see a blank page or 403 error, check that Nginx has read permission on the files:
sudo chown -R www-data:www-data /var/www/portfolio
sudo chmod -R 755 /var/www/portfolio
Step 5: Make It Publicly Accessible with Localtonet
Your portfolio works on your local network. To make it accessible to the rest of the world for job applications, sharing with clients, or just having a live URL on your CV create a Localtonet HTTP tunnel. Nginx listens on port 80 by default.
Install Localtonet on the Pi
curl -fsSL https://localtonet.com/install.sh | sh
localtonet --authtoken <YOUR_TOKEN>
Create an HTTP tunnel for port 80
Go to the HTTP tunnel page,
set local IP to 127.0.0.1 and port to 80.
Click Create and start the tunnel.
The dashboard shows a public HTTPS URL such as https://abc123.localto.net.
Keep the tunnel running after every reboot
Register Localtonet as a systemd service so the tunnel comes back automatically every time the Pi boots or loses power.
sudo localtonet --install-service --authtoken <YOUR_TOKEN>
sudo localtonet --start-service --authtoken <YOUR_TOKEN>
Verify both services are running:
sudo systemctl status nginx
sudo systemctl status localtonet
Your portfolio is now live at the HTTPS URL shown in the dashboard. Share it on your CV, LinkedIn profile, or GitHub bio.
Step 6: Add a Custom Domain (Optional)
A URL like https://yourname.dev looks more professional than a random subdomain.
To use your own domain with a Localtonet tunnel, point your domain's nameservers at Localtonet's DNS infrastructure
and then assign the domain to your tunnel in the dashboard.
Add your domain to Localtonet DNS Manager
Go to localtonet.com/dnsmanager and add your domain.
Update nameservers at your registrar
Log in to wherever you bought your domain and set the nameservers to
ns1.localtonet.com and ns2.localtonet.com.
Allow a few hours for propagation.
Assign the domain to your HTTP tunnel
Once the nameservers have propagated, select your custom domain when editing your HTTP tunnel
in the Localtonet dashboard. A Let's Encrypt certificate is provisioned automatically.
Your portfolio is now live at https://yourname.dev.
Full step-by-step instructions with screenshots are in the custom domain guide.
Frequently Asked Questions
Which Raspberry Pi model should I use?
For a static portfolio site, any Pi from the Pi 3B+ onwards handles the load without any issues. A Pi 4 with 2 GB RAM is a comfortable choice and widely available. A Pi 5 is overkill for this use case. If you have a spare Pi of any model gathering dust, it is more than capable enough.
What happens to my site if the Pi loses power or crashes?
Both Nginx and Localtonet are registered as systemd services set to start automatically on boot. The moment the Pi comes back online, both services start and your site is accessible again within seconds. For a personal portfolio, occasional brief downtime due to a power cut is acceptable. If uptime is critical, plug the Pi into a small UPS (uninterruptible power supply).
Can I host a React or Next.js site instead of plain HTML?
Yes. For a React app, run the build step on your development machine (npm run build) and copy the output directory to /var/www/portfolio on the Pi. Nginx serves the static build files exactly as it does plain HTML. For Next.js with server-side rendering, you need to run Node.js on the Pi and configure Nginx as a reverse proxy in front of it. The Pi 4 handles a small Node.js process fine.
How do I update the site after making changes?
Run the rsync command again from your development machine. It only transfers files that have changed since the last sync, so updates are fast. For a more automated workflow, set up a Git repository on the Pi and write a small shell script that pulls the latest changes and copies them to the web root. Trigger it manually over SSH or with a webhook from GitHub when you push.
Will the site be slow because it runs on a Pi?
For a static HTML and CSS site, no. Nginx is extremely efficient at serving static files and a Pi 4 handles thousands of concurrent connections for this kind of content. The bottleneck for a home-hosted site is more likely your home internet upload speed than the Pi's hardware. Most home connections provide enough upload bandwidth for a personal portfolio with modest traffic.
Can I run multiple projects on the same Pi?
Yes. Create a separate web root directory and Nginx configuration file for each project. Each site listens on a different port. Create a separate Localtonet HTTP tunnel per port, assign a subdomain or custom domain to each, and you have multiple independent sites all served from the same Pi. See the virtual hosts documentation for how to configure multiple sites with Nginx and Localtonet.
Your Portfolio, Live on Your Own Hardware
Flash a Pi, install Nginx, copy your files, and open a Localtonet tunnel. A live portfolio site on hardware you own and configured yourself, for roughly the cost of a coffee per year.
Create Free Localtonet Account →