13 min read

How to Build a Personal Portfolio Website on a Raspberry Pi and Make It Live

Host your personal portfolio on a Raspberry Pi with Nginx and make it publicly accessible with a Localtonet HTTPS tunnel. Full HTML and CSS included, no hosting fees required.

🍓 Raspberry Pi · Portfolio Website · Nginx · Static Site · Self-Hosting

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.

🌐 Live HTTPS URL in minutes 💸 Near-zero running cost 🔧 No router configuration 🎨 Fully customisable

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.

💸 Near-zero cost A Pi 4 consumes 3 to 5 watts. Running it continuously for a year costs roughly two to four dollars in electricity. No monthly hosting bill.
🔧 Full control No platform restrictions, no build minute limits, no cold starts. Your server, your rules, your uptime.
📚 Real-world learning Configuring Nginx, managing a Linux server, and setting up networking are practical skills that stand out in a portfolio themselves.
🔄 Instant updates Edit a file on the Pi, save it, and the change is live. No deployment pipeline, no build step for static HTML and CSS.

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.

1

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.

2

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
3

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.

1

Install Localtonet on the Pi

curl -fsSL https://localtonet.com/install.sh | sh
localtonet --authtoken <YOUR_TOKEN>
2

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.

3

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.

1

Add your domain to Localtonet DNS Manager

Go to localtonet.com/dnsmanager and add your domain.

2

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.

3

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 →

Localtonet is a secure multi-protocol tunneling and proxy platform designed to expose localhost, devices, private services, and AI agents to the public internet supporting HTTP/HTTPS tunnels, TCP/UDP forwarding, mobile proxy infrastructure, file server publishing, latency-optimized game connectivity, and developer-ready AI agent endpoint exposure from a single unified control plane.

support