Your home ISP gave you CGNAT — no public IPv4, no port forwarding, no way to reach your home server from outside. Or you've got a public IP but you don't want to drill holes in your firewall and expose your home network to the open internet. A reverse-proxy VPS solves both — a cheap cloud box with a real IP, forwarding traffic through a private WireGuard tunnel to whatever you have at home.

This guide builds a production reverse-proxy VPS using Nginx Proxy Manager (NPM — the most popular self-hosted reverse proxy with a web GUI), pairs it with WireGuard to tunnel back to your home network, and adds Let's Encrypt SSL termination so your home services get HTTPS with no port-forwarding required. The result: a single VPS that publishes your Jellyfin, Nextcloud, Home Assistant, dev box, family photo gallery — all on their own subdomains, all on real SSL certificates, none of it directly exposed to the internet.

🐾 What you'll need:
  • A Linux VPS (Ubuntu 22.04 or Debian 12 — the Starter plan is plenty)
  • A domain name with the ability to add A records (Cloudflare, Namecheap, anywhere)
  • Home server(s) you want to reach from outside
  • Roughly 35 minutes

Looking for a pre-built reverse-proxy VPS? Our Reverse Proxy VPS plans ship with NPM and WireGuard already installed — connect your home server in 5 minutes instead of 35.

1. When you need a reverse-proxy VPS

Three scenarios where this setup pays for itself in a week:

You have CGNAT. Most mobile-tethered, fiber, and even some cable ISPs hand out shared IPs now. You can't port-forward to a shared IP because it's not yours. A reverse-proxy VPS gives you a real IPv4 with all the ports you want, and tunnels traffic to your home over WireGuard outbound (which works through CGNAT fine).

You don't want to expose your home network. Even with a public IP, port-forwarding to home means your router becomes a target. Every bot on the internet probes 22, 80, 443, 8080, 8443. A reverse-proxy VPS absorbs that noise; your home network stays invisible.

You're running 10+ home services on different ports. Without a reverse proxy: home.example.com:8096 for Jellyfin, :32400 for Plex, :8080 for Home Assistant. Ugly, easy to forget, breaks on networks that block non-443 ports (most corporate Wi-Fi). With a reverse proxy: jellyfin.example.com, ha.example.com, all on 443, all with HTTPS, all clean.

The Cloudflare Tunnel alternative works for HTTP traffic but routes through Cloudflare's network (terms-of-service constraints, no raw TCP), and Cloudflare can see decrypted traffic. A self-hosted reverse proxy is yours end-to-end.

2. Prepare the VPS

SSH in as root and run the basics:

apt update && apt upgrade -y
apt install -y curl ca-certificates gnupg ufw wireguard

# Allow Docker + NPM + WireGuard
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 81/tcp     # NPM admin GUI (we'll restrict this later)
ufw allow 51820/udp  # WireGuard
ufw --force enable

# Enable IP forwarding (needed for WireGuard NAT)
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
sysctl -p

Create a non-root user and add them to the docker group (we'll install Docker next):

adduser proxy
usermod -aG sudo proxy
rsync --archive --chown=proxy:proxy ~/.ssh /home/proxy

3. Install Docker

NPM runs in Docker. Use Docker's official repo, not your distro's:

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
usermod -aG docker proxy

For more detail on this step, our Docker on Ubuntu 22.04 tutorial covers daemon hardening and rootless mode.

4. Deploy Nginx Proxy Manager

Switch to the proxy user and create a compose file:

su - proxy
mkdir -p ~/npm/{data,letsencrypt}
cd ~/npm

cat > compose.yaml <<'EOF'
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "80:80"      # public HTTP
      - "443:443"    # public HTTPS
      - "127.0.0.1:81:81"  # admin GUI on localhost only
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    environment:
      DB_SQLITE_FILE: "/data/database.sqlite"
EOF

docker compose up -d
docker compose logs -f

Wait ~30 seconds for the SQLite database to initialize. The admin GUI is now on http://127.0.0.1:81 — but only accessible from the VPS itself. To use it from your laptop, SSH-tunnel it:

# On your laptop:
ssh -L 8181:127.0.0.1:81 proxy@your-vps-ip

Then visit http://localhost:8181 in your browser. The default login is admin@example.com / changeme — NPM forces you to change both immediately.

Why hide the admin port? NPM's admin GUI on port 81 has had a few CVEs over the years. Locking it to localhost + SSH means an attacker has to compromise SSH first — a much higher bar.

5. Add your first proxied host

Inside the NPM admin GUI: HostsProxy HostsAdd Proxy Host.

FieldValue
Domain Namestest.example.com
Schemehttp
Forward Hostname / IP10.8.0.2 (your home WG IP — we'll set this in step 6)
Forward Port8096 (or whatever your home service uses)
Block Common Exploits
Websockets Support✓ (needed for Jellyfin, Plex, anything with live updates)

Switch to the SSL tab: select Request a new SSL Certificate, enable Force SSL and HTTP/2 Support, accept Let's Encrypt's terms, and save. NPM provisions the cert via the HTTP-01 challenge — takes about 20 seconds. Done. https://test.example.com now forwards encrypted traffic to your home server.

Except… we haven't built the home tunnel yet. The proxy will respond but get connection refused. Let's fix that.

🐾 Skip the manual install

Our Reverse Proxy VPS ships with NPM, WireGuard, and the IP-forwarding kernel sysctls already configured. Just import a WG peer config and you're done. Same plans, same hardware, ~25 minutes saved.

See Reverse Proxy Plans →

6. Set up WireGuard tunnel to home

WireGuard is a kernel-mode VPN — small, fast, simple. We need a server config on the VPS (which we already started installing in step 2) and a peer config on each home machine that should be reachable.

Generate keys on the VPS:

sudo -i
cd /etc/wireguard
umask 077

# Generate VPS keypair
wg genkey | tee server.key | wg pubkey > server.pub

# Generate one peer keypair (your home server)
wg genkey | tee home.key | wg pubkey > home.pub

ls -la

Now write the server config at /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = <contents of server.key>
Address = 10.8.0.1/24
ListenPort = 51820

# NAT for tunnel traffic — replace eth0 with your VPS's main interface
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Peer: home server
[Peer]
PublicKey = <contents of home.pub>
AllowedIPs = 10.8.0.2/32

Start the tunnel:

systemctl enable --now wg-quick@wg0
wg show

You should see interface: wg0, peer: ..., latest handshake: (none). The handshake will fire once the home side connects.

For a full walkthrough of WireGuard concepts and key management, see our WireGuard VPN tutorial.

7. Configure the home-server side

On your home server (the one running Jellyfin / Home Assistant / whatever), install WireGuard:

sudo apt install -y wireguard
sudo -i
cd /etc/wireguard
umask 077

Create /etc/wireguard/wg0.conf on the home server:

[Interface]
PrivateKey = <contents of home.key from the VPS>
Address = 10.8.0.2/24

[Peer]
PublicKey = <contents of server.pub from the VPS>
Endpoint = YOUR_VPS_IP:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25

The PersistentKeepalive = 25 matters — it tells the home WG client to send a keepalive packet every 25 seconds, which keeps the NAT mapping open on your home router. Without it, the VPS can't initiate connections to the home side after a few minutes of idleness.

systemctl enable --now wg-quick@wg0
wg show

Back on the VPS, run wg show again. You should now see a latest handshake timestamp. Ping the home server through the tunnel:

ping -c 3 10.8.0.2

Replies = tunnel works. Now revisit your NPM proxy host, hit https://test.example.com, and you should reach your home service. ✓

8. Security hardening

fail2ban for SSH + NPM admin.

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

Restrict NPM admin to your home IP via the VPS firewall.

sudo ufw delete allow 81/tcp
sudo ufw allow from YOUR.HOME.IP.ADDR to any port 81 proto tcp

Or better — keep the admin GUI bound to 127.0.0.1 and access only through SSH tunnel, as configured in step 4.

Rotate WireGuard keys on a schedule. Every 6 months, regenerate keys on both sides. Old peer entries become invalid; new ones replace them. Takes 5 minutes and is good hygiene.

Monitor for cert renewal failures. NPM auto-renews Let's Encrypt certs every 60 days. If a renewal fails (DNS misconfig, NPM crash), you only find out when the cert expires and users see browser warnings. Add a daily cron to check expiry:

echo "@daily echo | openssl s_client -connect test.example.com:443 2>/dev/null | openssl x509 -noout -dates | mail -s 'Cert check' you@example.com" | crontab -e

9. Reference: NPM advanced features

Things you'll probably want to enable later, organized for quick lookup.

Custom Nginx directives

Each proxy host has an Advanced tab where you can drop arbitrary Nginx config. Useful for: client IP allowlists, custom headers, body size limits, rate limiting.

# Example: rate-limit /admin/ paths
location /admin/ {
    limit_req zone=admin burst=5 nodelay;
    proxy_pass http://10.8.0.2:8096;
}

# Example: allow only your office IPs
allow 203.0.113.0/24;
deny all;

Stream proxying (TCP/UDP, not HTTP)

For non-HTTP services (game servers, IRC, custom protocols), use NPM's Streams tab. Forwards raw TCP/UDP through the proxy. Note: no SSL termination at the proxy for streams — your backend handles its own TLS if needed.

Access Lists

NPM has built-in HTTP basic auth at the proxy layer. Useful for protecting admin dashboards (Sonarr, Radarr, qBittorrent) without configuring auth per-app. Access Lists → create a list with username/password → attach to a proxy host.

DNS-01 challenge for wildcard certs

If you want *.example.com on one cert (covers all your subdomains forever), use the DNS-01 challenge. NPM supports Cloudflare, Route53, DigitalOcean, and ~30 other DNS providers. Configure under SSL CertificatesAddDNS Challenge.

Multiple WireGuard peers

You can tunnel to multiple home machines — just add more [Peer] blocks to wg0.conf on the VPS, each with its own keys and tunnel IP. NPM then proxies different subdomains to different tunnel IPs.

Performance limits

ThroughputWhat you can do
Up to 200 MbpsStarter ($7.99) plan handles this easily
200–500 MbpsPro plan, with WireGuard at ~80% line rate
500+ MbpsPremium plan, dedicated cores matter (WG is CPU-bound)

FAQ

Reverse-proxy VPS vs Cloudflare Tunnel?

Cloudflare Tunnel is great for HTTP/HTTPS but limited to web traffic — no raw TCP, no UDP, traffic routes through Cloudflare's network (which can decrypt it). A self-hosted reverse-proxy VPS handles any protocol, no third-party in the path, and your traffic stays end-to-end between you and your visitors. Trade-off: you pay for the VPS (~$8/month), and you're responsible for keeping it patched.

Does this work behind CGNAT?

Yes — that's a primary use case. WireGuard initiates outbound from the home side (the side stuck behind CGNAT), so no inbound ports are needed at home. The VPS handles all inbound traffic from the internet. CGNAT is invisible to this setup.

How much bandwidth does it use?

Traffic goes through the VPS twice — in from the visitor and out to your home (down). Each visit counts as 2× the file size against your VPS bandwidth quota. A 1 TB monthly cap covers ~500 GB of actual user traffic. Plan accordingly for media servers — see our bandwidth explainer.

Can I expose multiple home services?

Yes — that's the whole point. Add a proxy host in NPM per service, each on its own subdomain. We comfortably run 10+ services through a single Starter-plan reverse-proxy VPS for testing.

Is the home server visible from the public internet?

No — the home server only listens on the WireGuard tunnel interface (10.8.0.2). The internet sees only the VPS. If the VPS is compromised, an attacker can reach the home network through the tunnel — so keep the VPS patched.

What about IPv6?

WireGuard handles IPv6 natively. Add an IPv6 subnet to wg0.conf (e.g. fd00:8:0::1/64 on VPS, fd00:8:0::2/64 on home), and NPM can proxy AAAA records the same way it handles A records. Most home ISPs that hand out CGNAT IPv4 give you a real IPv6 — which means you may not need the tunnel for IPv6-capable clients at all.

🐱
OliveVPS Team

We run this exact setup at the office — one Frankfurt VPS reverse-proxies six home labs across the team. Total monthly cost: $8. Cloudflare Tunnel would have been free but we wanted raw TCP for our IRC bouncers.