From Simple Pi Server to Reverse Proxy Architecture
A practical journey from “just running containers” to understanding infrastructure design.
The Goal
I rebuilt my Raspberry Pi 4 (8GB, NVMe boot) home server from scratch.
The original goal was simple:
- Run Docker
- Host a few services
- Keep it secure
- Learn along the way
What started as a basic container setup turned into a deeper understanding of reverse proxies, networking, and reducing attack surface.
Phase 1: Clean Base System
Fresh install of Raspberry Pi OS Lite.
Security baseline:
- SSH key-only authentication
- Disabled password login
- UFW enabled (default deny incoming)
- Custom SSH port
This created a minimal and hardened foundation before adding any services.
Phase 2: Dockerized Services
I added:
- Homepage (dashboard)
- File Browser (file management)
Initial Docker setup exposed ports directly:
Service Port
Homepage 3000 File Browser 8080
Access looked like:
http://rpi-server.local:3000
http://rpi-server.local:8080
It worked — but something felt off.
The Realization: Too Many Exposed Ports
Running:
docker ps
Showed:
homepage 0.0.0.0:3000->3000/tcp
filebrowser 0.0.0.0:8080->80/tcp
That meant:
- Multiple services were publicly reachable
- UFW had multiple allow rules
- Every service was an entry point
Even though this was “just a LAN server,” the attack surface was larger than necessary.
That question — “Can I close off those exposed ports?” — led directly to learning about reverse proxies.
Phase 3: Introducing Nginx (Reverse Proxy)
Instead of exposing every service, I added an Nginx container.
New architecture:
Client
↓
Port 80 (Nginx)
↓
Internal Docker Network
↓
Homepage / Filebrowser
Only Nginx is exposed externally.
All other containers are internal-only.
Docker Compose (Final Structure)
Reverse Proxy (Nginx)
nginx:
image: nginx:alpine
container_name: reverse-proxy
ports:
- "80:80"
volumes:
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
File Browser (Internal Only)
filebrowser:
image: filebrowser/filebrowser
container_name: filebrowser
expose:
- "80"
volumes:
- /srv:/srv
- ./config/filebrowser:/config
- ./config/filebrowser-db:/database
restart: unless-stopped
Homepage (Internal Only)
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
expose:
- "3000"
environment:
HOMEPAGE_ALLOWED_HOSTS: rpi-server.local
volumes:
- ./config/homepage:/app/config
restart: unless-stopped
Key difference:
ports:exposes a service to the host.expose:makes it available only inside Docker.
Nginx Routing Configuration
events {}
http {
server {
listen 80;
server_name rpi-server.local;
location / {
proxy_pass http://homepage:3000;
}
location /files/ {
proxy_pass http://filebrowser:80/;
}
}
}
Access now looks like:
http://rpi-server.local
http://rpi-server.local/files
No more port numbers.
Firewall Cleanup
Before:
ALLOW 3000/tcp
ALLOW 8080/tcp
ALLOW 80/tcp
After:
ALLOW 80/tcp (LAN only)
ALLOW SSH
Attack surface significantly reduced.
What Actually Changed
Before:
LAN → Homepage (3000)
LAN → Filebrowser (8080)
After:
LAN → Nginx (80)
↓
Internal Docker Network
↓
Services (not directly reachable)
Only one exposed entry point.
This mirrors real-world infrastructure design.
Lessons Learned
- Reverse proxies centralize control.
- Docker
portsvsexposematters for security. - Host header validation prevents misuse.
- VPNs can affect local hostname routing.
- Path-based proxying may require base URL adjustments.
Why This Matters
Understanding reverse proxies teaches:
- Infrastructure layering
- Trust boundaries
- Attack surface reduction
- Header handling
- Real-world architecture patterns
This wasn’t just “installing Nginx.”
It was learning why modern systems are built the way they are.
Final Architecture
LAN
↓
Port 80 → Nginx
↓
Docker Internal Network
↓
Homepage + Filebrowser
Minimal. Controlled. Intentional.
Next Steps
- Add HTTPS internally
- Implement rate limiting
- Add logging analysis
- Introduce additional services behind the proxy
This started as a simple Pi server.
It evolved into understanding infrastructure design.
And that’s the real win.