From the founder: Escaping CG-NAT and Connecting From The Internet
March 8, 2025
Self-hosting services like Immich from home comes with a modern nemesis: Carrier-Grade NAT (CG-NAT). Like many others, I found myself with a residential internet connection that lacked a public IPv4 address, trapping my home server behind my ISP’s network.
Understanding CG-NAT
Carrier-Grade NAT (CG-NAT) , also known as Large Scale NAT (LSN), is a technique where internet service providers assign a single public IP address to multiple customers simultaneously. Instead of giving each subscriber a unique public IPv4 address, the ISP creates a private network and uses NAT to share one public address among many users.
The problem this creates: Your home router receives a private IP address (typically in the 100.64.0.0/10 range) instead of a public one. This means:
-
You can initiate outbound connections to the internet
-
Incoming connections from the internet cannot reach your devices
-
Port forwarding becomes impossible
-
You effectively have no direct inbound access to self-hosted services
This blog post details how I bypassed CG-NAT to access my Immich instance from the internet using a cheap VPS as a secure relay with Tailscale, and why I chose this path over other popular solutions.
The Problem and The Dead Ends
My goal was simple: access my photo library on the go. However, my ISP puts all residential customers behind CG-NAT, meaning I don't have a public IPv4 address to point my domain to. I explored three initial options:
-
IPv6: The cleanest solution. Unfortunately, my ISP confirmed they do not provide IPv6 addresses for home connections.
-
Static IPv4 (Paid): My ISP offers a static IP for a small fee. While this solves the reachability problem, it exposes my entire home network to the open internet. Hardening against constant bot scans, DDoS attacks, and potential zero-day exploits wasn't what I set out to do. A compromise of a replacable VPS was safer than a compromise of my personal home network.
-
Cloudflare Tunnel: This is a fantastic service that creates a secure outbound tunnel to Cloudflare's edge. However, their Terms of Service prohibit using it as a conduit for non-web content or large amounts of video and photos, specifically citing a 1GB/month limit for non-html content on the free plan. Immich, being a photo backup service, would violate this immediately and risk getting my domain flagged.
The Solution: A VPS Relay with a Mesh VPN
I needed a stable public endpoint, strong encryption, and network segmentation. The solution was to combine a cheap VPS (with a public IP) with a mesh VPN (Tailscale).
Why this approach won:
-
Stable Endpoint: The VPS provides a public IPv4 address to point my DNS (e.g., immich.mydomain.com).
-
Secure Encryption: Tailscale creates a Wireguard-based tunnel. Traffic between my client and my home server is encrypted, even while passing through the VPS.
-
Network Segmentation: My home server is not exposed to the internet. It only trusts the Tailscale network. I can further harden the VPS with firewalls and IDS/IPS without risking my home LAN.
-
Flexibility: The VPS can act as a relay for multiple services. It also serves as a Tailscale exit node, allowing me to route all my mobile traffic through it if needed.
The main drawback is added latency. To mitigate this, I chose a VPS region within 10ms of my home, making the relay effectively transparent for photo browsing.
Traffic Flow Breakdown:
- The client looks up
immich.mydomain.comand connects to the VPS's public IP. - Caddy (running on the VPS) terminates the HTTPS connection.
- Caddy forwards the request to the internal Tailscale IP address of my home server (port 2283 for Immich).
- The request travels securely over the Tailscale network.
- The home server processes the request and sends the response back through the encrypted Tailscale tunnel to the VPS.
- The VPS relays the response back to the client.
Conclusion
By combining a $5/month VPS with Tailscale, I successfully punched through CG-NAT without exposing my home network. The setup is secure, encrypted, and flexible. The initial complexity understanding CG-NAT and learning Tailscale has paid off with a stable, self-hosted solution that I fully control.
flowchart TD
Client["Client Device<br/>(Browser/App)"]
subgraph Cloudflare [Cloudflare]
DNS["DNS Resolution<br/>(immich.siloed.in → VPS IP)<br/>."]
end
Internet["Internet"]
VPS["VPS (Public IP)<br/>• Caddy Reverse Proxy<br/>• Tailscale Node<br/>."]
Tailscale["Tailscale VPN<br/>(Encrypted Tunnel)"]
%% DNS query (dotted line)
Client -.->|DNS query for immich.siloed.in<br/>.| DNS
DNS -.->|Returns VPS IP| Client
%% Main traffic flow (solid lines)
Client -->|HTTPS request| Internet
Internet -->|Request reaches VPS| VPS
VPS -->|HTTPS response| Internet
Internet -->|Response to client| Client
subgraph VPS_Internal [VPS Internal Flow]
VPS -->|Caddy proxies request to port 2283 of home server's VPN IP<br/>.| VPS_Tailscale["Tailscale Interface<br>VPN IP: 100.64.0.1"]
end
VPS_Tailscale <--> Tailscale
Tailscale <--> HomeServer_Tailscale["Tailscale Interface<br>VPN IP: 100.64.0.2"]
subgraph Home_Internal [Home Server Internal Flow]
HomeServer["Home Server<br/>(Behind CG-NAT)<br/>• Docker Host <br/>• Tailscale Node<br/>."]
HomeServer_Tailscale <-->HomeServer
HomeServer <-->|Docker bridge| HomeServer_Immich["Immich Service<br>0.0.0.0:2283->2283"]
end
VPS_Tailscale -->|Caddy forwards response| VPS
style Client fill:#f9f,stroke:#333,stroke-width:2px
style Cloudflare fill:#e5e5e5,stroke:#f90,stroke-width:2px
style VPS fill:#bbf,stroke:#333,stroke-width:1px
style HomeServer fill:#bfb
style Tailscale fill:#feb,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5
style Internet fill:#feb