Tools: How to Run a Web Server on 27MB of RAM (and a Solar Panel)

Tools: How to Run a Web Server on 27MB of RAM (and a Solar Panel)

The Problem: Modern Web Servers Are Absurdly Bloated

Step 1: Strip the OS Down to Bare Minimum

Step 2: Choose the Right Web Server

Step 3: Make Your Content Tiny

Step 4: Survive the Night

Step 5: Get It on the Internet

What 27MB Actually Looks Like

Prevention Tips: Don't Learn This the Hard Way

Is This Practical? There's something deeply satisfying about running infrastructure that costs literally nothing per month. No AWS bill. No DigitalOcean invoice. Just sunlight hitting a panel on your windowsill, powering a tiny computer serving real HTTP requests to real people. I got nerd-sniped by a project on Hackaday recently — someone running a solar-powered Raspberry Pi as a web server with only 27MB of RAM in use. My first reaction was skepticism. My second was "I need to try this." My third was three days of yak-shaving that taught me more about Linux memory management than the previous eight years combined. Here's what I learned about squeezing a web server into almost nothing. Spin up a basic Node.js Express server that serves a single HTML page. Check your memory usage. You're looking at 30-50MB minimum before a single request hits it. A default Apache install? Easily 100MB+. Nginx is leaner but still pulls in 10-20MB with its worker processes. When your entire system has 512MB of RAM (Raspberry Pi Zero) and you need the OS, the network stack, and the server to coexist — while also surviving on intermittent solar power — every megabyte matters. The goal isn't just "make it work." It's "make it work reliably on a power budget that disappears every night." Forget Raspberry Pi OS with a desktop. Forget Raspberry Pi OS Lite, even. We're going deeper. Start with a minimal image like DietPi or build a custom image with Buildroot. The key is disabling everything you don't need: After stripping services, a fresh DietPi install idles at around 20-25MB. That's your OS budget. Everything else needs to fit in the remaining space. Nginx is the popular "lightweight" choice, but there are servers purpose-built for this constraint level: BusyBox httpd — already included in most minimal Linux distros. Serves static files with almost zero overhead. We're talking kilobytes of memory, not megabytes. darkhttpd — a single-file static HTTP server. One C file. Compiles to a ~30KB binary. Memory usage hovers around 150-300KB. lighttpd — if you need CGI or some dynamic content, lighttpd runs comfortably at 1-2MB. Much heavier than the others, but still a fraction of Nginx. For pure static serving, darkhttpd or BusyBox httpd are hard to beat. Serving a 2MB React bundle from a solar-powered Pi is... a choice. Here's the approach that actually works: Key rules: inline your CSS, skip JavaScript entirely if possible, use system fonts, optimize images aggressively (or just don't use them). This is where it gets interesting. Solar power means your server goes offline when the sun does — unless you plan for it. The approach I've seen work best: Run that via cron every minute. The real killer isn't the downtime — it's SD card corruption from unclean shutdowns. A read-only filesystem helps enormously here: A read-only root filesystem also means you can survive sudden power loss without corruption. The Pi just boots right back up when the sun returns. Your Pi probably doesn't have a static IP or a port-forwarded router. Options: The Cloudflare Tunnel approach is the simplest. WireGuard + a $3/month VPS running Nginx as a reverse proxy gives you the most control with the least RAM impact on the Pi itself. Here's a realistic memory breakdown on a stripped-down Pi Zero: Drop cloudflared in favor of WireGuard (kernel module, negligible userspace cost) and you're comfortably under 27MB. Replace systemd with something like s6 or plain old SysVinit and you shave off another 2-3MB. Honestly? For a personal site, a status page, or a sensor dashboard — absolutely. You're not going to serve a SaaS app this way, but that's not the point. The point is understanding what your software actually needs versus what it lazily consumes. After going through this exercise, I started looking at every docker stats output differently. That 200MB container serving a landing page? Suddenly feels a little embarrassing. Sometimes the best way to understand the ceiling is to start from the floor. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

# Disable services that eat RAM for breakfast -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable bluetooth -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable avahi-daemon -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable triggerhappy -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily--weight: 500;">upgrade.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable man-db.timer # Disable swap — on SD cards, swap kills your storage -weight: 600;">sudo dphys-swapfile swapoff -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable dphys-swapfile # Drop to single TTY instead of six # Edit /etc/systemd/logind.conf NAutoVTs=1 ReserveVT=0 # Disable services that eat RAM for breakfast -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable bluetooth -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable avahi-daemon -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable triggerhappy -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily--weight: 500;">upgrade.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable man-db.timer # Disable swap — on SD cards, swap kills your storage -weight: 600;">sudo dphys-swapfile swapoff -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable dphys-swapfile # Drop to single TTY instead of six # Edit /etc/systemd/logind.conf NAutoVTs=1 ReserveVT=0 # Disable services that eat RAM for breakfast -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable bluetooth -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable avahi-daemon -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable triggerhappy -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable -weight: 500;">apt-daily--weight: 500;">upgrade.timer -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable man-db.timer # Disable swap — on SD cards, swap kills your storage -weight: 600;">sudo dphys-swapfile swapoff -weight: 600;">sudo -weight: 500;">systemctl -weight: 500;">disable dphys-swapfile # Drop to single TTY instead of six # Edit /etc/systemd/logind.conf NAutoVTs=1 ReserveVT=0 # Serve static files from /var/www with BusyBox httpd # -f keeps it in foreground, -p sets port, -h sets document root busybox httpd -f -p 80 -h /var/www # Serve static files from /var/www with BusyBox httpd # -f keeps it in foreground, -p sets port, -h sets document root busybox httpd -f -p 80 -h /var/www # Serve static files from /var/www with BusyBox httpd # -f keeps it in foreground, -p sets port, -h sets document root busybox httpd -f -p 80 -h /var/www # Build from source — it's literally one file gcc -O2 -o darkhttpd darkhttpd.c # Run it ./darkhttpd /var/www --port 80 --daemon --log /var/log/darkhttpd.log # Build from source — it's literally one file gcc -O2 -o darkhttpd darkhttpd.c # Run it ./darkhttpd /var/www --port 80 --daemon --log /var/log/darkhttpd.log # Build from source — it's literally one file gcc -O2 -o darkhttpd darkhttpd.c # Run it ./darkhttpd /var/www --port 80 --daemon --log /var/log/darkhttpd.log <!-- Keep it simple. This entire page is under 5KB. --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>My Solar-Powered Site</title> <style> /* Inline CSS — one fewer HTTP request */ body { font-family: system-ui, sans-serif; /* no web font download */ max-width: 42rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; } /* Use prefers-color-scheme instead of JS theme toggles */ @media (prefers-color-scheme: dark) { body { background: #111; color: #e0e0e0; } } </style> </head> <body> <h1>Served by sunlight</h1> <p>This page was delivered by a computer running on a solar panel.</p> <!-- Server -weight: 500;">status injected by a simple CGI script or cron-generated file --> <p>Battery: <!--#include virtual="/-weight: 500;">status.txt" --></p> </body> </html> <!-- Keep it simple. This entire page is under 5KB. --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>My Solar-Powered Site</title> <style> /* Inline CSS — one fewer HTTP request */ body { font-family: system-ui, sans-serif; /* no web font download */ max-width: 42rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; } /* Use prefers-color-scheme instead of JS theme toggles */ @media (prefers-color-scheme: dark) { body { background: #111; color: #e0e0e0; } } </style> </head> <body> <h1>Served by sunlight</h1> <p>This page was delivered by a computer running on a solar panel.</p> <!-- Server -weight: 500;">status injected by a simple CGI script or cron-generated file --> <p>Battery: <!--#include virtual="/-weight: 500;">status.txt" --></p> </body> </html> <!-- Keep it simple. This entire page is under 5KB. --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>My Solar-Powered Site</title> <style> /* Inline CSS — one fewer HTTP request */ body { font-family: system-ui, sans-serif; /* no web font download */ max-width: 42rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #1a1a1a; } /* Use prefers-color-scheme instead of JS theme toggles */ @media (prefers-color-scheme: dark) { body { background: #111; color: #e0e0e0; } } </style> </head> <body> <h1>Served by sunlight</h1> <p>This page was delivered by a computer running on a solar panel.</p> <!-- Server -weight: 500;">status injected by a simple CGI script or cron-generated file --> <p>Battery: <!--#include virtual="/-weight: 500;">status.txt" --></p> </body> </html> #!/bin/bash # /usr/local/bin/power-monitor.sh # Check battery voltage via ADC and gracefully shut down before power dies VOLTAGE=$(cat /sys/class/power_supply/battery/voltage_now 2>/dev/null) # Threshold in microvolts — 3.3V is the danger zone for most LiPo cells THRESHOLD=3300000 if [ -n "$VOLTAGE" ] && [ "$VOLTAGE" -lt "$THRESHOLD" ]; then logger "Battery critical ($VOLTAGE uV), initiating shutdown" # Sync filesystem before power dies — SD card corruption is the real enemy sync shutdown -h now fi #!/bin/bash # /usr/local/bin/power-monitor.sh # Check battery voltage via ADC and gracefully shut down before power dies VOLTAGE=$(cat /sys/class/power_supply/battery/voltage_now 2>/dev/null) # Threshold in microvolts — 3.3V is the danger zone for most LiPo cells THRESHOLD=3300000 if [ -n "$VOLTAGE" ] && [ "$VOLTAGE" -lt "$THRESHOLD" ]; then logger "Battery critical ($VOLTAGE uV), initiating shutdown" # Sync filesystem before power dies — SD card corruption is the real enemy sync shutdown -h now fi #!/bin/bash # /usr/local/bin/power-monitor.sh # Check battery voltage via ADC and gracefully shut down before power dies VOLTAGE=$(cat /sys/class/power_supply/battery/voltage_now 2>/dev/null) # Threshold in microvolts — 3.3V is the danger zone for most LiPo cells THRESHOLD=3300000 if [ -n "$VOLTAGE" ] && [ "$VOLTAGE" -lt "$THRESHOLD" ]; then logger "Battery critical ($VOLTAGE uV), initiating shutdown" # Sync filesystem before power dies — SD card corruption is the real enemy sync shutdown -h now fi # Mount root filesystem as read-only # Add 'ro' to your /etc/fstab root entry, then: -weight: 600;">sudo mount -o remount,ro / # Use tmpfs for anything that needs writes tmpfs /var/log tmpfs defaults,noatime,nosuid,size=5m 0 0 tmpfs /tmp tmpfs defaults,noatime,nosuid,size=5m 0 0 # Mount root filesystem as read-only # Add 'ro' to your /etc/fstab root entry, then: -weight: 600;">sudo mount -o remount,ro / # Use tmpfs for anything that needs writes tmpfs /var/log tmpfs defaults,noatime,nosuid,size=5m 0 0 tmpfs /tmp tmpfs defaults,noatime,nosuid,size=5m 0 0 # Mount root filesystem as read-only # Add 'ro' to your /etc/fstab root entry, then: -weight: 600;">sudo mount -o remount,ro / # Use tmpfs for anything that needs writes tmpfs /var/log tmpfs defaults,noatime,nosuid,size=5m 0 0 tmpfs /tmp tmpfs defaults,noatime,nosuid,size=5m 0 0 - Cloudflare Tunnel (formerly Argo Tunnel) — the cloudflared daemon is lightweight and gives you HTTPS for free. Memory cost is around 10-15MB though, which is significant at this scale. - WireGuard to a cheap VPS that reverse-proxies to your Pi. WireGuard's kernel module uses almost no userspace memory. - Yggdrasil or Tailscale for overlay networking if you're okay with non-traditional access. - Use overlayfs or a read-only root. I cannot stress this enough. SD card corruption will ruin your weekend. - Monitor with free -h obsessively during testing. Memory leaks that are invisible on a 16GB laptop will crash your server in hours. - Set up a watchdog timer. The Pi has a hardware watchdog — use it. If your server hangs, it auto-reboots. - Test your shutdown script by actually yanking power. Simulate the worst case before the worst case finds you. - Log to tmpfs, not the SD card. Rotate or discard logs on reboot. If you need persistent logs, write them to a USB stick.