← Back to Blog

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:

  1. IPv6: The cleanest solution. Unfortunately, my ISP confirmed they do not provide IPv6 addresses for home connections.

  2. 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.

  3. 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:

  1. The client looks up immich.mydomain.com and connects to the VPS's public IP.
  2. Caddy (running on the VPS) terminates the HTTPS connection.
  3. Caddy forwards the request to the internal Tailscale IP address of my home server (port 2283 for Immich).
  4. The request travels securely over the Tailscale network.
  5. The home server processes the request and sends the response back through the encrypted Tailscale tunnel to the VPS.
  6. 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