Expose Your Home Services to the Internet Without Port Forwarding: A Step-by-Step FRP Guide

Learn how to expose multiple home services to the internet using FRP (Fast Reverse Proxy), Docker, and Caddy. No port forwarding needed, perfect for CGNAT environments.

Dilshad Akhtar
Dilshad Akhtar
24 December 2025
15 min read

TLDRQuick Summary

  • FRP (Fast Reverse Proxy) lets you expose home services without port forwarding
  • Works perfectly with CGNAT environments where traditional port forwarding is impossible
  • One VPS can handle multiple services from multiple local machines
  • Caddy provides automatic HTTPS with Let's Encrypt certificates
  • Adding new services requires minimal configuration changes

You've got a home server running Immich, n8n, or some other self-hosted app, and you want clean domains like `images.yourdomain.com` or `automation.yourdomain.com` instead of ugly ports and IPs. Your ISP is behind CGNAT, port forwarding is unreliable or impossible, and paying for a static IP feels like a tax on self-hosting. The better option is to tunnel everything through a VPS using FRP (Fast Reverse Proxy) and Docker.

Step 1: Prepare Your VPS as the Central Entry Point

You need a VPS with:

  • A public static IPv4 (for example, 150.265.32.125)
  • Docker and Docker Compose installed
  • Ports 80, 443, and a FRP control port (like 7000) open in the provider's firewall and on the OS firewall (ufw/firewalld)

SSH into the VPS and create a directory for the FRP server and Caddy:

mkdir -p ~/frp-server
cd ~/frp-server

Create the FRP server config frps.toml:

cat > frps.toml << EOF
bindPort = 7000
vhostHTTPPort = 8080
vhostHTTPSPort = 8443
log.to = "console"
EOF
  • bindPort is where frpc clients connect.
  • vhostHTTPPort is where FRP listens for HTTP virtual-host traffic (per-domain routing).
  • vhostHTTPSPort is reserved if you later do HTTPS-level handling through FRP itself.

Now create a docker-compose.yml to run frps and Caddy on the VPS:

cat > docker-compose.yml << EOF
services:
  frps:
    image: snowdreamtech/frps:latest
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./frps.toml:/etc/frp/frps.toml

  caddy:
    image: caddy:alpine
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config

volumes:
  caddy_data:
  caddy_config:
EOF

Create an initial Caddyfile for one domain (more will be added later):

cat > Caddyfile << EOF
yourdomain.com {
    reverse_proxy localhost:8080
}
EOF

Start the stack:

docker compose up -d
docker logs frps
docker logs caddy

At this point, the VPS is ready to accept FRP client connections on port 7000 and HTTP traffic on port 8080, with Caddy handling HTTPS on 443 and proxying requests down to FRP.

Step 2: Expose Immich from Machine 1 via FRP

On Machine 1, Immich is running at http://192.168.1.14:2283. You will run frpc on this same machine to forward yourdomain.com to that internal address.

Create a client directory:

mkdir -p ~/frp-client
cd ~/frp-client

Create frpc.toml:

cat > frpc.toml << EOF
serverAddr = "150.265.32.125"
serverPort = 7000

[[proxies]]
name = "immich"
type = "http"
localIP = "192.168.1.14"
localPort = 2283
customDomains = ["yourdomain.com"]
EOF

Key points from FRP's HTTP vhost docs:

  • type = "http" is what allows FRP to route based on Host header.
  • customDomains must match the domain you will point at the VPS.

Create a docker-compose.yml for frpc:

cat > docker-compose.yml << EOF
services:
  frpc:
    image: snowdreamtech/frpc:latest
    container_name: frpc
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./frpc.toml:/etc/frp/frpc.toml
EOF

Start the client:

docker compose up -d
docker logs frpc

You should see a log like:

[immich] start proxy success

Step 3: Wire DNS and HTTPS for Immich

In your DNS provider (for example, Cloudflare), create an A record:

Type: A
Name: images
IPv4: 150.265.32.125
Proxy: DNS only (no CDN proxy while debugging)

Caddy is already configured for yourdomain.com and will automatically request a Let's Encrypt certificate and terminate HTTPS.

Test in your browser:

https://yourdomain.com

If Immich is reachable locally and frpc/frps are connected, you should see Immich loaded over HTTPS. If not:

  • Check docker logs frpc on Machine 1.
  • Check docker logs frps and docker logs caddy on the VPS for TLS or proxy errors.

Step 4: Add a Second Local Service (n8n) on the Same Machine

On the same Machine 1, n8n is available at http://192.168.1.14:5678 and should be exposed as automation.yourdomain.com. FRP supports multiple [[proxies]] entries in one frpc.toml, all mapped to different domains.

Edit ~/frp-client/frpc.toml on Machine 1:

cd ~/frp-client

cat > frpc.toml << EOF
serverAddr = "150.265.32.125"
serverPort = 7000

[[proxies]]
name = "immich"
type = "http"
localIP = "192.168.1.14"
localPort = 2283
customDomains = ["yourdomain.com"]

[[proxies]]
name = "n8n"
type = "http"
localIP = "192.168.1.14"
localPort = 5678
customDomains = ["automation.yourdomain.com"]
hostHeaderRewrite = "automation.yourdomain.com"
EOF

The hostHeaderRewrite directive is taken directly from FRP's header modification docs and ensures that the Host header reaching n8n is exactly what n8n expects.

Restart frpc:

docker compose down
docker compose up -d
docker logs frpc

On the VPS, extend your Caddyfile:

cd ~/frp-server

cat > Caddyfile << EOF
yourdomain.com {
    reverse_proxy localhost:8080
}

automation.yourdomain.com {
    reverse_proxy localhost:8080 {
        header_up Host automation.yourdomain.com
    }
}
EOF

docker compose restart caddy

Add DNS for the new subdomain:

Type: A
Name: automation
IPv4: 150.265.32.125
Proxy: DNS only

Then test:

https://automation.yourdomain.com

If you encounter redirect loops with n8n, they almost always come from incorrect Host/Proto headers or misaligned n8n base URL configuration. Using hostHeaderRewrite on frpc and header_up Host in Caddy removes that class of problem.

Step 5: Add Services from a Second Machine on the LAN

Now you want to expose a service running on Machine 2, for example http://192.168.1.63:11000, and map it to cloud.yourdomain.com. You do not reuse the frpc on Machine 1 for this; you run a separate frpc on Machine 2, pointing to the same frps.

On Machine 2:

mkdir -p ~/frp-client
cd ~/frp-client

Create frpc.toml:

cat > frpc.toml << EOF
serverAddr = "150.265.32.125"
serverPort = 7000

[[proxies]]
name = "cloud-dublin"
type = "http"
localIP = "192.168.1.63"
localPort = 11000
customDomains = ["cloud.yourdomain.com"]
EOF

Create docker-compose.yml:

cat > docker-compose.yml << EOF
services:
  frpc:
    image: snowdreamtech/frpc:latest
    container_name: frpc
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./frpc.toml:/etc/frp/frpc.toml
EOF

Start frpc:

docker compose up -d
docker logs frpc

On the VPS, expand Caddyfile again:

cd ~/frp-server

cat > Caddyfile << EOF
yourdomain.com {
    reverse_proxy localhost:8080
}

automation.yourdomain.com {
    reverse_proxy localhost:8080 {
        header_up Host automation.yourdomain.com
    }
}

cloud.yourdomain.com {
    reverse_proxy localhost:8080
}
EOF

docker compose restart caddy

Create DNS for cloud.yourdomain.com pointing to the VPS IP. Now:

  • Immich → yourdomain.com → VPS → frps → Machine 1
  • n8n → automation.yourdomain.com → VPS → frps → Machine 1
  • Second web app → cloud.yourdomain.com → VPS → frps → Machine 2

Quick Checklist Before You Blame FRP

Only one list, but a useful one:

  • FRP server (frps) running and listening on 7000/8080, no port conflicts.
  • FRP clients (frpc) running on each machine, logs show start proxy success.
  • Each service reachable locally on its localIP:localPort.
  • DNS A records for each subdomain point to the VPS public IP.
  • Caddy has matching blocks for each domain and is restarted after edits.
  • For "picky" apps like n8n, hostHeaderRewrite and proper Host headers are set.

Conclusion

Once this pipeline is in place, adding a new service is just: Add a `[[proxies]]` block in `frpc.toml` on the relevant machine, add a domain block in `Caddyfile` on the VPS, add an A record in DNS, and restart frpc and Caddy. No ISP calls, no port forwarding, no CGNAT pain—just clean domains for all your self-hosted tools.

Ready to Build Your Dream Website?

Let's discuss your project and create something amazing together.

Dilshad Akhtar

About Dilshad Akhtar

Founder of Sharp Digital with 5+ years of experience in web development and digital marketing.