๐Ÿน Home

Homelab Network School

Everything you need to understand your network โ€” IPs, DNS, ports, VLANs, HTTPS, and Wireguard.
Built around your actual stack. Pick a topic below.

๐Ÿ“ก

IP Addresses

Octets, private vs public ranges, subnet masks, CIDR notation. Why your server is 192.168.x.x and what every number means.

6 concepts covered
๐Ÿ”

DNS

How name resolution works, record types (A, CNAME, TXT, PTR), Pi-hole as your network resolver, and local DNS for your services.

8 concepts covered
๐Ÿšช

Ports

Well-known ports, every port your homelab services use, port forwarding in pfSense, TCP vs UDP. The apartment-number analogy.

20+ ports mapped
๐Ÿ”€

VLANs & DHCP

Network segmentation, DHCP leases, static IPs, your four VLANs (Private / Guest / IoT / CCTV) and firewall rules between them.

4 VLANs designed
๐Ÿ”’

HTTPS & Nginx

HTTP vs HTTPS, TLS handshake, how Nginx Proxy Manager reverse-proxies all your services, and free Let's Encrypt certs via DuckDNS.

5 concepts covered
๐Ÿ”

Wireguard VPN

How Wireguard works, tunnels vs port forwarding, OPNsense integration, client setup, and reaching your homelab from anywhere securely.

6 concepts covered

IP Addresses

An IP address is a 32-bit number that identifies every device on a network.
Written as four numbers (octets) separated by dots. Every number is 0โ€“255.

Anatomy of an IP Address
192.168.1.42
Four octets โ€” each 0โ€“255 (8 binary bits). Total: 32 bits = IPv4.
concept
Each octet is 8 binary bits. 2โธ = 256 possible values (0โ€“255). Four octets = 2ยณยฒ = ~4.3 billion possible IPv4 addresses.
Breaking it down
192.168 # fixed prefix โ€” marks this as a private Class C address
1 # subnet โ€” your network number inside 192.168.x.x
42 # host โ€” this specific device. .1 is almost always your router
In binary
192 = 11000000
168 = 10101000
1 = 00000001
42 = 00101010
Network part vs Host part
The subnet mask tells you which octets identify the network and which identify the device.
concept
In a /24 network (255.255.255.0), the first three octets = network, the last = host. Every device on 192.168.1.x is on the same network. Devices on 192.168.2.x are on a different network and need a router to talk to each other.
192.168.1.100 # your server
192.168.1.50 # your gaming PC
192.168.1.1 # your router (always .1 by convention)
192.168.1.255 # broadcast โ€” sent to every device on this subnet
192.168.1.0 # network address โ€” "name" of this subnet, not assignable
Private vs Public Ranges
192.168.0.0 โ€“ 192.168.255.255
Class C private range โ€” your home network lives here
concept
This is the range reserved for home and small office networks. Your router assigns addresses from here via DHCP. These are NOT reachable from the internet without port forwarding.
192.168.1.0/24 # 254 usable addresses (.1 to .254)
192.168.10.0/24 # your planned Private LAN VLAN
192.168.20.0/24 # your planned Guest VLAN
192.168.30.0/24 # your planned IoT VLAN
192.168.40.0/24 # your planned CCTV VLAN
10.0.0.0 โ€“ 10.255.255.255
Class A private range โ€” 16 million addresses, used in enterprise and VPNs
concept
Docker uses 172.16.0.0/12 (Class B private) for its default bridge network. Wireguard tunnels often use 10.x.x.x for tunnel IPs. Your containers talk to each other through Docker's internal 172.x.x.x addresses.
10.0.0.0/8 # entire Class A space โ€” 16,777,214 hosts
172.16.0.0/12 # Class B โ€” Docker bridge network lives here
127.0.0.1 # loopback โ€” "myself." A service listening here is local-only
Public IP โ€” your WAN address
The address your ISP assigns your router. The world-facing identity of your house.
concept
Everything coming FROM your network to the internet looks like this one IP. NAT (Network Address Translation) maps your private 192.168.x.x devices to this single public IP. Port forwarding works in reverse โ€” traffic arriving at your public IP on specific ports gets routed to specific internal machines.
โš ๏ธ Most ISPs give you a dynamic public IP โ€” it changes periodically. DuckDNS solves this by updating dahamsterserver.duckdns.org to your current IP automatically.
curl ifconfig.me # check your current public IP from terminal
Subnet Masks & CIDR Notation
192.168.1.0/24
CIDR โ€” "slash notation" for subnet masks. You'll type this constantly in pfSense and Docker.
concept
The number after the slash = how many bits are the network part. The rest = host bits. More host bits = more addresses.
The ones you'll actually use
/32 = 255.255.255.255 # one single host โ€” used in firewall rules
/24 = 255.255.255.0 # 254 hosts โ€” your VLANs (192.168.10.0/24)
/16 = 255.255.0.0 # 65,534 hosts โ€” Docker internal networks
/8 = 255.0.0.0 # 16 million โ€” entire 10.x.x.x private space
/30 = 255.255.255.252 # 2 hosts โ€” point-to-point links
How to calculate hosts
hosts = 2^(32 - prefix) - 2
/24 โ†’ 2^8 - 2 = 254 hosts
/25 โ†’ 2^7 - 2 = 126 hosts
/16 โ†’ 2^16 - 2 = 65,534 hosts
In pfSense you'll type 192.168.10.0/24 to define a VLAN. In Docker Compose you'll see subnet: 172.20.0.0/16. Same concept, different context.
ip addr show
See your server's IP addresses and subnet masks from the shell
cmd
ip addr show # all interfaces
ip addr show eth0 # just eth0
ip route show # routing table โ€” shows your gateway
ip route | grep default # just the default gateway line
ping -c 4 192.168.1.1 # test if gateway is reachable
ping -c 4 8.8.8.8 # test internet (skips DNS)
ping -c 4 google.com # test internet + DNS

DNS โ€” Domain Name System

DNS translates hostnames into IP addresses. Every time you type a URL,
DNS runs before a single byte of content loads. Your Pi-hole controls this for your whole network.

How a DNS Lookup Works
Step 1 โ€” Browser checks its local cache
Did we look this up recently? If yes, use the saved IP with no network request.
concept
Every DNS answer comes with a TTL (Time To Live) โ€” a number of seconds before the cache expires. This is why DNS changes take time to "propagate." Even if you update your DuckDNS record, old clients hold the old IP until their TTL runs out.
ipconfig /flushdns # Windows: clear DNS cache
sudo resolvectl flush-caches # Linux: clear DNS cache
sudo dscacheutil -flushcache # macOS: clear DNS cache
Step 2 โ€” Ask the configured DNS resolver
Your OS asks the DNS server handed out by pfSense via DHCP โ€” that's Pi-hole.
concept
pfSense's DHCP server tells every device on your network to use Pi-hole's IP as their DNS server. Every device auto-configures. Zero config needed on individual machines.
cat /etc/resolv.conf # Linux: see configured DNS server
nslookup jellyfin.home # test resolving a local name
dig jellyfin.home @192.168.10.5 # query Pi-hole directly
Step 3 โ€” Pi-hole checks blocklist + local records
Ad domains โ†’ blocked. Local names like jellyfin.home โ†’ returns your server's LAN IP directly.
concept
Pi-hole's local DNS records let you create A records like jellyfin.home โ†’ 192.168.10.100. Every device on your network resolves it without knowing or caring about the port. Combine with Nginx Proxy Manager and everything gets a clean domain name over HTTPS.
โœ“ Add local DNS records in Pi-hole: Admin โ†’ Local DNS โ†’ DNS Records. Add a CNAME record pointing to your server's A record to cover all subdomains.
Step 4 โ€” Forward to upstream: 1.1.1.1 or 8.8.8.8
Real internet domains go to Cloudflare or Google. Pi-hole forwards, caches the reply.
concept
Configure Pi-hole to use Cloudflare 1.1.1.1 over DoH (DNS over HTTPS) for the upstream. This encrypts your DNS queries to Cloudflare so your ISP can't see what domains you're resolving โ€” only that you're talking to 1.1.1.1.
Upstream DNS options
1.1.1.1 # Cloudflare โ€” fast, privacy-respecting
1.0.0.1 # Cloudflare secondary
8.8.8.8 # Google โ€” fast, but Google logs queries
9.9.9.9 # Quad9 โ€” blocks malware domains too
DNS Record Types
A record โ€” hostname โ†’ IPv4 address
The most common record. Maps a name to an IP. This is what you create in Pi-hole for local services.
concept
jellyfin.home โ†’ 192.168.10.100 # A record in Pi-hole
r720.home โ†’ 192.168.10.100 # another name for the same IP
pihole.home โ†’ 192.168.10.5 # Pi-hole's own dashboard
CNAME record โ€” alias โ†’ other hostname
Points one name to another name (not an IP). Chain CNAMEs to avoid maintaining many A records.
concept
If your server's IP ever changes, you only update one A record. All CNAMEs pointing to it auto-update. Create one A record for your server, then CNAME all your services to that record.
server.home โ†’ 192.168.10.100 # one A record
jellyfin.home โ†’ server.home # CNAME
nextcloud.home โ†’ server.home # CNAME
navidrome.home โ†’ server.home # CNAME
This is the cleanest setup โ€” one A record, all services as CNAMEs. Change the server IP once, everything follows.
TXT record โ€” arbitrary text data
Used for domain verification, SPF/DKIM email security, and Let's Encrypt DNS challenges.
concept
When you request a wildcard Let's Encrypt cert (*.yourname.duckdns.org), it proves domain ownership by asking you to add a specific TXT record. Nginx Proxy Manager does this automatically via the DuckDNS API.
_acme-challenge.yourname.duckdns.org โ†’ "randomstring123..." # Let's Encrypt proof
PTR record โ€” IP โ†’ hostname (reverse DNS)
The reverse of an A record. Used in logs so you see hostnames instead of IPs.
concept
When Frigate logs "motion detected from 192.168.40.12" it's nicer to see "frontdoor-cam". Pi-hole's local DNS can serve PTR records too. Mostly optional for homelab purposes.
dig -x 192.168.10.100 # reverse lookup โ€” what hostname is this IP?
DNS Diagnostic Commands
dig jellyfin.home @192.168.10.5
Query a specific DNS server directly โ€” essential for debugging Pi-hole
cmd
dig jellyfin.home @192.168.10.5 # ask Pi-hole specifically
dig google.com @1.1.1.1 # ask Cloudflare directly
nslookup jellyfin.home 192.168.10.5 # older but works everywhere
dig +short google.com # just the IP, no extra output
dig jellyfin.home +trace # full resolution chain

Ports

An IP address gets you to the right building. A port number gets you to the right apartment.
Your server has one IP โ€” ports tell traffic which of the dozens of services to talk to.

The Concept
192.168.10.100:8096
IP + port = full address. The colon separates them. This is how you reach Jellyfin.
concept
Ports range 0โ€“65535. Three ranges: 0โ€“1023 are well-known (reserved for standard services), 1024โ€“49151 are registered (apps claim by convention), 49152โ€“65535 are dynamic (used for temporary outbound connections).
TCP vs UDP
TCP # connection-oriented. Reliable delivery. Web, SSH, Jellyfin, Nextcloud.
UDP # connectionless. Faster, no guarantees. Game servers, DNS (port 53), Wireguard.
When you browse the web your browser opens a random high port (e.g. 54231) to talk to the server on port 443. The server responds to that random port. You never configured it โ€” it's automatic.
Well-Known System Ports
:22 โ€” SSH
Secure Shell โ€” how you remotely control Linux servers via terminal
cmd
You will use this constantly. SSH into your R720xd, into VMs, into Docker hosts. Key-based auth is far safer than passwords โ€” set it up early.
ssh user@192.168.10.100 # basic SSH
ssh -p 2222 user@192.168.10.100 # custom port
ssh-keygen -t ed25519 # generate key pair
ssh-copy-id user@192.168.10.100 # push public key (passwordless login)
โš ๏ธ Never expose port 22 directly to the internet. Use Wireguard to reach your LAN, then SSH from inside the tunnel.
:53 โ€” DNS
All DNS queries go here. Pi-hole listens on port 53 for your entire network.
concept
DNS uses both TCP and UDP on port 53. Most queries are UDP (small, fast). Large responses fall back to TCP. Your Pi-hole container must have port 53 exposed on both protocols.
ports:
- "53:53/tcp"
- "53:53/udp" # both required in docker-compose
:80 HTTP  ยท  :443 HTTPS
Web traffic. 80 = unencrypted. 443 = encrypted (TLS). Always redirect 80 โ†’ 443.
concept
Nginx Proxy Manager listens on both. Port 80 should redirect to 443 automatically. Your ISP may block inbound 80/443 โ€” check before setting up external access. Some ISPs require a business account for open inbound ports.
:67/68 โ€” DHCP
Your pfSense DHCP server listens here to hand out IP addresses to all devices.
concept
When a device joins the network it broadcasts on port 68 asking for an IP. pfSense (the DHCP server) listens on port 67 and responds with an IP, subnet, gateway, and DNS server. You never interact with these ports directly โ€” it's automatic.
:51820 โ€” Wireguard
Wireguard VPN default port (UDP). This is the one port you forward to reach your whole homelab securely.
concept
Forward this single UDP port in pfSense to your pfSense WAN interface. Once you're connected via Wireguard, you have full LAN access and can SSH, browse services, reach Proxmox โ€” everything. No other ports need to be forwarded for private access.
Your Homelab Service Ports
Proxmox web UI โ€” :8006
The hypervisor control panel. Only accessible on your LAN (or via Wireguard).
config
https://192.168.10.100:8006 # Proxmox UI (HTTPS, self-signed cert)
โš ๏ธ Never expose port 8006 to the internet. It has no rate limiting on logins. Only access via Wireguard.
Jellyfin โ€” :8096 (HTTP) / :8920 (HTTPS)
Your media server. Internally on 8096 โ€” Nginx Proxy Manager handles HTTPS externally.
config
http://192.168.10.100:8096 # LAN access direct
https://jellyfin.yourdomain.com # external via NPM (preferred)
Navidrome โ€” :4533
Music server web UI and Subsonic API for apps.
config
http://192.168.10.100:4533 # web UI
# apps like Symfonium use the Subsonic API on the same port
Nextcloud โ€” :443 (via NPM)
Cloud storage. Always run behind Nginx Proxy Manager with HTTPS.
config
https://nextcloud.yourdomain.com # external via NPM
http://192.168.10.100:8080 # internal (NPM admin panel)
Nginx Proxy Manager โ€” :81 (admin) / :80 / :443
NPM admin panel on 81. It listens on 80 and 443 for all proxied services.
config
http://192.168.10.100:81 # NPM admin panel (LAN only)
Frigate NVR โ€” :5000 / :8554 (RTSP)
Camera dashboard on 5000. RTSP streams on 8554. Never expose externally.
config
http://192.168.10.100:5000 # Frigate dashboard
rtsp://192.168.10.100:8554/camera_name # RTSP stream
โš ๏ธ Keep Frigate off the public internet. Camera feeds + person detection data should never leave your LAN.
Open WebUI โ€” :3000
Your local AI interface. Reaches Ollama on port 11434 internally.
config
http://192.168.10.100:3000 # Open WebUI
http://192.168.10.100:11434 # Ollama API (internal only)
Calibre Web โ€” :8083
Ebook library web UI.
config
http://192.168.10.100:8083 # Calibre Web UI
Pi-hole โ€” :80 (admin) / :53 (DNS)
Pi-hole admin dashboard on 80. DNS resolver on 53. Keep separate from NPM's port 80.
config
If Pi-hole and Nginx Proxy Manager are on the same host, Pi-hole's web UI needs a different port (e.g. 8082) or run them on different IPs. They both want port 80.
http://192.168.10.5/admin # Pi-hole dashboard
http://192.168.10.5:8082/admin # if NPM owns port 80
Game Server Ports
Minecraft โ€” :25565 TCP
Java Edition default. Forward this in pfSense so friends can connect.
config
pfSense: WAN:25565 TCP โ†’ 192.168.10.100:25565
Friends connect to: yourpublicip:25565
# or dahamsterserver.duckdns.org:25565
DayZ โ€” :2302โ€“2305 UDP  ยท  Zomboid โ€” :16261/16262 UDP
Game servers use UDP. Forward all required ports in pfSense.
config
DayZ: 2302 UDP (game), 2303 UDP (RCON), 2305 UDP (BEC)
Zomboid: 16261 UDP (game), 16262 UDP (direct)
Pavlov: 7777 UDP
# all forwarded WAN โ†’ 192.168.10.100:port
Checking Open Ports
ss -tulpn
Show every open port on the current machine and which process owns it
cmd
ss -tulpn # all listening ports
ss -tulpn | grep :8096 # is Jellyfin listening?
netstat -tulpn # older alternative
nmap -p 1-65535 192.168.10.100 # scan all ports on server from another machine
curl -I http://localhost:8096 # test if service responds

VLANs & DHCP

VLANs are virtual separate networks on the same physical switch. Your managed switch tags traffic
by VLAN ID so different device groups are completely isolated from each other.

Your Four VLANs
VLAN 10 โ€” Private LAN 192.168.10.0/24
Full trust. Your gaming PC, daily devices. Complete access to all homelab services.
config
Firewall rules
โœ“ Access to internet
โœ“ Access to server rack โ€” all services
โœ“ Access to Proxmox UI (8006)
โœ“ Can reach all other VLANs via explicit rules
Fixed IPs in this VLAN
192.168.10.1 # pfSense (gateway)
192.168.10.5 # Pi-hole (DNS)
192.168.10.10 # managed switch (management IP)
192.168.10.100 # R720xd server (Proxmox)
192.168.10.50 # gaming PC (DHCP reservation)
192.168.10.50+ # everything else dynamic DHCP
VLAN 20 โ€” Guest WiFi 192.168.20.0/24
Internet-only. Visitors and friends. Completely blind to your LAN and server.
config
Firewall rules
โœ“ Internet access only
โœ— No access to VLAN 10 (your devices)
โœ— No access to server rack
โœ— No access to any other VLAN
DNS for guests: use Cloudflare 1.1.1.1 directly instead of Pi-hole. No reason to give guests your local resolver.
VLAN 30 โ€” IoT 192.168.30.0/24
Smart TVs, Rokus, smart bulbs. These are notoriously chatty โ€” isolate them.
config
Firewall rules
โœ“ Internet access (cloud features still work)
โœ“ Can reach Frigate only (optional, explicit rule)
โœ— No access to Private LAN
โœ— No access to Proxmox, server management
โœ— No access to Guest or CCTV VLANs
IoT devices are often insecure and phone home constantly. Pi-hole on IoT VLAN will block a shocking amount of their telemetry.
VLAN 40 โ€” CCTV 192.168.40.0/24
IP cameras only. Completely air-gapped from internet. Streams to Frigate only.
config
Firewall rules
โœ“ Can reach Frigate on 192.168.10.100:5000 only
โœ— No internet access โ€” cameras should NEVER call home
โœ— No access to any other VLAN
โœ— No DNS needed
โš ๏ธ Cheap IP cameras regularly attempt to call home to Chinese cloud servers. Hard-blocking their internet access at the VLAN level is the right move.
DHCP โ€” How Devices Get IPs
DORA โ€” Discover โ†’ Offer โ†’ Request โ†’ Acknowledge
The four-step handshake that runs every time a device joins your network.
concept
Discover # device broadcasts "anyone have an IP for me?" (255.255.255.255:67)
Offer # pfSense replies "here's 192.168.10.45, available for 24h"
Request # device says "yes I'll take 192.168.10.45"
Acknowledge # pfSense confirms + sends: subnet mask, gateway, DNS server
pfSense runs one DHCP server per VLAN, each with its own range, gateway, and DNS settings.
DHCP Reservation โ€” static IP via MAC address
In pfSense: "whenever you see this MAC address, always give it this IP." Best of both worlds.
config
Server, Pi-hole, switch โ€” anything that other services depend on needs a fixed IP. DHCP reservations are better than static IPs configured on the device because you manage everything from pfSense.
pfSense โ†’ Services โ†’ DHCP Server โ†’ [your VLAN]
โ†’ DHCP Static Mappings โ†’ Add
โ†’ enter MAC address + desired IP + hostname

ip link show # Linux: find your MAC address
ip addr show eth0 # MAC is the "link/ether" line
DHCP ranges to set in pfSense
VLAN 10: 192.168.10.50 โ€“ 192.168.10.200 # .1-.49 reserved for static
VLAN 20: 192.168.20.50 โ€“ 192.168.20.200
VLAN 30: 192.168.30.50 โ€“ 192.168.30.200
VLAN 40: 192.168.40.50 โ€“ 192.168.40.100 # fewer cameras needed
End-to-End Traffic Traces
You open Jellyfin on your phone
Phone (VLAN 10) โ†’ Pi-hole โ†’ server โ†’ Jellyfin container โ†’ video streams
concept
1. Phone asks Pi-hole: "what is jellyfin.home?"
2. Pi-hole returns 192.168.10.100 (local A record)
3. Phone connects TCP to 192.168.10.100:8096
4. Jellyfin responds, streams media
# total: under 20ms inside your LAN
A friend joins your Minecraft server
Internet โ†’ your public IP:25565 โ†’ pfSense NAT โ†’ server โ†’ Minecraft container
concept
1. Friend's PC connects to dahamsterserver.duckdns.org:25565
2. DuckDNS resolves to your current public IP
3. TCP hits pfSense on WAN port 25565
4. pfSense NAT forwards to 192.168.10.100:25565
5. Minecraft container accepts connection
6. Friend is in your world
A guest connects to WiFi
Guest device โ†’ VLAN 20 โ†’ internet only โ†’ your server is completely invisible
concept
1. Guest connects to guest SSID
2. Gets IP 192.168.20.x via DHCP, DNS = 1.1.1.1
3. pfSense firewall blocks all traffic to VLAN 10
4. Guest can browse internet
5. 192.168.10.x doesn't exist from their perspective
6. Your R720xd is completely invisible to them
Camera detects motion
IP cam (VLAN 40) โ†’ Frigate โ†’ AI detection โ†’ clip saved โ†’ notification sent
concept
1. IP camera on VLAN 40 streams RTSP to Frigate
2. pfSense allows VLAN 40 โ†’ 192.168.10.100:5000 only
3. Frigate AI detects a person in the frame
4. Clip is saved to R720xd storage (TrueNAS dataset)
5. Notification sent (Home Assistant webhook)
6. Camera never touches the internet โ€” not once

HTTPS & Nginx Proxy Manager

Every service you expose externally needs HTTPS. Nginx Proxy Manager sits at the edge of your server,
handles TLS, and routes traffic to the right container by subdomain.

HTTP vs HTTPS
HTTP โ€” port 80 โ€” plaintext
Traffic is unencrypted. Passwords, cookies, file data โ€” all visible to anyone between you and the server.
warn
๐Ÿ”ด On your local LAN it's low risk. The moment anything is accessible externally, HTTP is unacceptable. Your login credentials to Nextcloud or Jellyfin would travel the internet in plaintext.
Nginx Proxy Manager automatically redirects HTTP โ†’ HTTPS. Port 80 exists only to issue the redirect.
HTTPS โ€” port 443 โ€” TLS encrypted
Traffic is encrypted end-to-end. The padlock icon. Mandatory for external access.
concept
โœ“ Login credentials encrypted
โœ“ Session cookies protected from hijacking
โœ“ File uploads/downloads encrypted
โœ“ API keys and tokens protected
โœ“ ISP cannot see what you're doing (only that you're talking to your server)
TLS Handshake
How HTTPS connections are established
A 5-step cryptographic handshake that happens before any data flows. Under 100ms total.
concept
1. Client hello # browser: "I support these algorithms"
2. Server hello # server: picks algorithm, sends TLS certificate
3. Cert verify # browser: is cert signed by a trusted CA? domain match? expired?
4. Key exchange # both sides derive a shared session key (private to this session)
5. Encrypted data # all subsequent traffic encrypted. Handshake done.
Nginx Proxy Manager holds your TLS cert and does step 2. It then forwards plain HTTP to your internal containers. Your containers (Jellyfin, Nextcloud, etc.) don't handle TLS at all โ€” NPM does it for them.
Nginx Proxy Manager โ€” Reverse Proxy
How a reverse proxy works
One public IP, one port 443, one cert โ€” routes to dozens of services by reading the subdomain in the HTTP header.
concept
When a request arrives on port 443, the Host header says "jellyfin.yourdomain.com". NPM reads that header and forwards the request to the right container. You never type a port number again โ€” everything is on a clean subdomain.
NPM routing table
jellyfin.yourdomain.com โ†’ localhost:8096 # SSL on
nextcloud.yourdomain.com โ†’ localhost:5000 # SSL on
music.yourdomain.com โ†’ localhost:4533 # SSL on
ai.yourdomain.com โ†’ localhost:3000 # SSL on
books.yourdomain.com โ†’ localhost:8083 # SSL on
frigate.yourdomain.com โ†’ localhost:5000 # LAN-only (no external access)
docker-compose.yml โ€” NPM setup
Basic NPM container config. Ports 80, 443 (traffic) and 81 (admin panel).
cmd
services:
npm:
image: jc21/nginx-proxy-manager:latest
ports:
- "80:80" # HTTP (redirects to 443)
- "443:443" # HTTPS (all proxied services)
- "81:81" # admin panel
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
Let's Encrypt Certificates
Let's Encrypt โ€” free, auto-renewing TLS certs
A Certificate Authority that issues browser-trusted HTTPS certs at no cost. NPM handles everything.
concept
Let's Encrypt proves you own the domain via a "challenge." For DuckDNS subdomains, NPM uses the DNS-01 challenge โ€” it adds a TXT record to your DuckDNS domain via the API, Let's Encrypt verifies it, cert is issued. All automatic.
Getting a wildcard cert
NPM โ†’ SSL Certificates โ†’ Add Certificate
Domain: *.yourname.duckdns.org
Challenge: DNS / DuckDNS
API Token: [your DuckDNS token]
# one cert covers all subdomains โ€” jellyfin, nextcloud, music, etc.
โœ“ Wildcard cert: *.yourname.duckdns.org covers ALL subdomains
โœ“ Auto-renews every 90 days โ€” zero maintenance
โœ“ DuckDNS API token handles the DNS-01 challenge automatically
DuckDNS โ€” dynamic DNS for your changing public IP
dahamsterserver.duckdns.org always resolves to your current public IP, even when it changes.
config
Your ISP probably gives you a dynamic public IP. DuckDNS is a free service that auto-updates your subdomain every 5 minutes. Run it as a Docker container or a cron job on your server.
# Docker container โ€” run on your server
services:
duckdns:
image: lscr.io/linuxserver/duckdns:latest
environment:
- SUBDOMAINS=dahamsterserver
- TOKEN=your-duckdns-token
- TZ=America/New_York
restart: unless-stopped

Wireguard VPN

Wireguard is the VPN built into OPNsense. Connect from anywhere and your phone/laptop behaves
as if it's sitting on your home LAN โ€” reaching Proxmox, Jellyfin, Pi-hole, everything.

How Wireguard Works
Wireguard โ€” kernel-level VPN, 51820 UDP
~4,000 lines of code vs OpenVPN's 100,000. Faster, simpler, built into Linux kernel.
concept
Wireguard creates an encrypted tunnel between your device and OPNsense. All traffic from your device routes through that tunnel and emerges on your home LAN. Your phone's IP becomes 10.0.0.2 (or whatever tunnel IP you assign) and it can reach 192.168.10.x as if it's home.
Your phone (10.0.0.2) โ†’ encrypted UDP โ†’ dahamsterserver.duckdns.org:51820
โ†’ OPNsense (Wireguard server, 10.0.0.1)
โ†’ routes to 192.168.10.x (your LAN)
โ†’ 192.168.10.100 (R720xd) responds
โ†’ back through tunnel โ†’ your phone
Because you're on your VPN, you can reach Proxmox (8006), SSH to anything, open Jellyfin, check Frigate โ€” all without any additional port forwarding. One port (51820) = full access.
VPN vs Port Forwarding โ€” which for what
Use Wireguard for private access, port forwarding only for things friends need to reach.
concept
Use Wireguard for
โœ“ SSH to your server
โœ“ Proxmox web UI
โœ“ Frigate camera feeds
โœ“ Pi-hole admin
โœ“ Portainer
โœ“ Anything admin or sensitive
Use port forwarding for
Minecraft/DayZ/Zomboid โ€” friends need to connect without Wireguard
Your website (Nginx) โ€” public-facing
Jellyfin/Nextcloud (via NPM) โ€” family access without VPN setup
โš ๏ธ Every forwarded port is an attack surface. Keep the list short.
Public / private key pairs โ€” how Wireguard authenticates
No passwords. Each peer has a keypair. The server holds client public keys. Clients hold server public key.
concept
Wireguard uses Curve25519 key pairs. You generate a keypair per client device. Give the server your public key. The server gives you its public key. Each side uses the other's public key to encrypt; only the matching private key can decrypt. Compromise one client key = that one device is affected, nothing else.
wg genkey | tee privatekey | wg pubkey > publickey # generate keypair
cat privatekey # your private key โ€” never share this
cat publickey # give this to OPNsense
OPNsense Setup
OPNsense โ†’ VPN โ†’ WireGuard โ†’ Local
Configure the server (OPNsense) side. One-time setup.
config
Name: homelab-wg
Listen Port: 51820
Tunnel Address: 10.0.0.1/24 # VPN subnet, not your LAN
DNS Server: 192.168.10.5 # Pi-hole โ€” VPN clients get ad blocking too
Disable Routes: unchecked
Then add a peer (one per device)
OPNsense โ†’ VPN โ†’ WireGuard โ†’ Peers โ†’ Add
Name: my-phone
Public Key: [your phone's public key]
Allowed IPs: 10.0.0.2/32 # VPN IP assigned to this client
Port forward in pfSense/OPNsense WAN rules
WAN UDP port 51820 โ†’ 127.0.0.1:51820 # to OPNsense itself
Firewall rules โ€” allow Wireguard clients onto your LAN
OPNsense needs explicit rules to let VPN clients reach your local network.
config
OPNsense โ†’ Firewall โ†’ Rules โ†’ WireGuard
โ†’ Add rule:
Action: Pass
Interface: WireGuard
Source: WireGuard net (10.0.0.0/24)
Destination: LAN net (192.168.10.0/24)
Protocol: any
# this lets VPN clients reach your entire VLAN 10
Client Configuration
Client config file โ€” wg0.conf
Drop this on any device (or scan as QR code on mobile) to connect.
config
[Interface]
PrivateKey = [your device private key]
Address = 10.0.0.2/24 # VPN IP for this device
DNS = 192.168.10.5 # Pi-hole โ€” use it even while remote

[Peer]
PublicKey = [OPNsense server public key]
Endpoint = dahamsterserver.duckdns.org:51820
AllowedIPs = 192.168.10.0/24, 192.168.20.0/24, 10.0.0.0/24
# routes only your home subnets through tunnel
# (split tunnel โ€” other internet goes direct)
PersistentKeepalive = 25 # keep NAT hole punched open
AllowedIPs โ€” tunnel all traffic vs split tunnel
AllowedIPs = 0.0.0.0/0 # full tunnel โ€” ALL traffic through VPN (more private)
AllowedIPs = 192.168.10.0/24 # split tunnel โ€” only home LAN through VPN (faster)
wg show
Check Wireguard status โ€” see connected peers and last handshake time
cmd
wg show # all tunnel info + peer handshake times
wg show wg0 # specific interface
wg-quick up wg0 # bring tunnel up (Linux client)
wg-quick down wg0 # bring tunnel down
systemctl enable wg-quick@wg0 # auto-connect on boot
If "latest handshake" shows recent time โ†’ tunnel is active. If peers show no handshake, check: is port 51820 forwarded in pfSense? Is OPNsense Wireguard service enabled?
Mobile โ€” Wireguard app (iOS/Android)
Import via QR code. OPNsense can generate the QR code for you.
config
In OPNsense: after creating a peer, click the QR code icon next to it. Open the Wireguard app on your phone โ†’ Add tunnel โ†’ Scan QR code. Done. Your phone now has full LAN access when connected.
โœ“ On-demand tunnels in the app โ€” only active when you want it
โœ“ iOS shortcut: "when not on home WiFi, enable Wireguard"
โœ“ Works on LTE, 5G, hotel WiFi โ€” anything with internet
Troubleshooting VPN connection
Common issues and how to debug them.
warn
# Can't connect at all
โ†’ Check port 51820 UDP is forwarded in OPNsense WAN rules
โ†’ Check OPNsense Wireguard service is running
โ†’ Check DuckDNS resolved to your current public IP (curl ifconfig.me)

# Connected but can't reach LAN
โ†’ Check AllowedIPs in client config includes 192.168.10.0/24
โ†’ Check OPNsense firewall rule: WireGuard โ†’ LAN is Pass
โ†’ Check your server's subnet routes

# DNS not working over VPN
โ†’ Check DNS = 192.168.10.5 in [Interface] section of config
โ†’ Check Pi-hole is allowing queries from 10.0.0.0/24