Skip to content

Exposing OpenCodeHub from a NAS or home server

Exposing OpenCodeHub from a NAS or home server

Section titled “Exposing OpenCodeHub from a NAS or home server”

Difficulty: Intermediate | Time: 30-60 minutes | Audience: self-hosters on a NAS, home lab, Raspberry Pi, mini-PC, or VPS without a public IP

This guide covers the two most popular ways to put an OpenCodeHub instance running on a NAS or home server on the public internet without opening inbound ports on your router and without exposing your home IP:

  • Tailscale Funnel — WireGuard-based mesh VPN with a public ingress; the simplest option if you already use Tailscale.
  • Cloudflare Tunnel — an outbound-only cloudflared daemon that fronts your app with Cloudflare’s edge.

Both options work whether your ISP gives you a public IP, CGNAT, a double-NAT, or a captive portal. Both terminate TLS at the edge (Cloudflare’s network or Tailscale’s edge), so you do not need to manage certificates or open :80/:443 on your router.

If you do have a public IP and a domain, the deploy-nas.md guide covers the traditional reverse-proxy + Let’s Encrypt path. The two methods below are preferred because they require zero port forwarding.


MethodOutbound from home?Inbound to home?Free tierCustom domainBest for
Tailscale Funnelyes (WireGuard)noneyes (personal use)yes, but only one hostname per funnelAlready using Tailscale; simple, single-user / small-team setups
Cloudflare Tunnelyes (HTTP/2)noneyes (unlimited tunnels)yes, full DNS supportPublic-facing deployments, larger teams, custom domains, edge caching

Both methods are outbound-only from your NAS. Your router’s firewall never sees an inbound connection to OpenCodeHub ports.


Whatever exposure method you pick, you need:

  1. A working OpenCodeHub instance on your NAS, listening on a local port (default 3000).
  2. A reachable hostname (Tailscale or Cloudflare) pointed at the tunnel.
  3. A persistent cloudflared or tailscaled process — running on the NAS, in Docker, or as a system service.
  4. Trusted Proxies correctly set in OpenCodeHub (so the real client IP is preserved and rate limits are not bypassed). See TRUSTED_PROXIES in .env.example.

If you do not yet have OpenCodeHub running, see deploy-nas.md for the Docker setup first.


Tailscale is a WireGuard-based mesh VPN. Tailscale Funnel is the public-ingress feature: it lets you expose a service running on any node in your tailnet to the public internet through Tailscale’s edge, using a stable HTTPS URL.

  • Single binary, single auth key.
  • TLS terminated at Tailscale’s edge — your NAS never sees the public traffic in the clear.
  • Stable hostname: https://<node-name>.<tailnet>.ts.net.
  • No port forwarding, no firewall changes, no certificate management.
  • Works on CGNAT, mobile broadband, hotel Wi-Fi.

On the NAS (or in a container with --net=host and tailscaled running on the host), install Tailscale:

Terminal window
# Synology DSM 7: install from Package Center (community package), or:
curl -fsSL https://tailscale.com/install.sh | sh
# TrueNAS SCALE: install the TrueNAS Tailscale app from the catalog
# QNAP: install via Container Station as a Linux container
# Generic Linux / Raspberry Pi OS:
curl -fsSL https://tailscale.com/install.sh | sh

Authenticate:

Terminal window
sudo tailscale up

Follow the URL it prints, sign in, and approve the device. Confirm with:

Terminal window
tailscale status # should show your NAS as a node
tailscale ip -4 # 100.x.y.z

Step 1.2 — Expose OpenCodeHub via Funnel

Section titled “Step 1.2 — Expose OpenCodeHub via Funnel”
Terminal window
# Tailscale Funnel is currently in beta and must be enabled per tailnet
# in the Tailscale admin console: https://login.tailscale.com/admin
# (turn on the "Funnel" feature flag).
#
# Then on the NAS, expose the port OpenCodeHub listens on (default 3000):
sudo tailscale funnel 3000 on
# Or expose a specific path with HTTPS-only:
sudo tailscale funnel --https=443 http://localhost:3000

You will get a public URL like https://nas-hostname.tail-net.ts.net. Funnel enforces HTTPS; you do not need to manage certificates.

Step 1.3 — Lock the funnel to your hostname only

Section titled “Step 1.3 — Lock the funnel to your hostname only”

By default, Funnel exposes the service to the public. You can restrict it to your account with tailscale set --operator:

Terminal window
sudo tailscale set --operator=$USER

For ACLs (which users in your tailnet can enable Funnel), see the Tailscale ACL docs.

tailscale funnel does not persist across reboots by itself. To auto-start it on boot, create a small systemd unit or add the command to your NAS’s startup scripts.

For Synology DSM, use Task Scheduler → Triggered task → Boot-up and run:

Terminal window
sudo tailscale funnel 3000 on

For TrueNAS SCALE, configure the Tailscale app’s “Post-Start” command in the app config.

Set the public URL in .env:

SITE_URL=https://nas-hostname.tail-net.ts.net
TRUSTED_PROXIES=127.0.0.1,100.64.0.0/10 # Tailscale CGNAT range

Tailscale’s edge IPs all live in 100.64.0.0/10 (CGNAT). Adding that to TRUSTED_PROXIES lets OpenCodeHub read the real client IP from X-Forwarded-For.

Terminal window
curl -sI https://nas-hostname.tail-net.ts.net
# HTTP/2 200
# server: Caddy (Tailscale's edge)

Open the URL in a browser; you should land on the OpenCodeHub login page. git clone https://nas-hostname.tail-net.ts.net/<owner>/<repo>.git should also work.


Cloudflare Tunnel uses a lightweight daemon (cloudflared) running on your NAS to establish an outbound tunnel to Cloudflare’s edge. Cloudflare terminates TLS and proxies requests to your service.

  • Free, unlimited tunnels.
  • Use any hostname you own on Cloudflare (or bring a partial-setup domain).
  • Edge caching, rate limiting, WAF rules, analytics, all available out-of-the-box.
  • No port forwarding, no public IP required.
  • Zero-trust access controls (Cloudflare Access) for password-protecting /admin paths.
  • Production-grade — same plumbing Cloudflare uses for its own apps.

Step 2.1 — Add your domain to Cloudflare

Section titled “Step 2.1 — Add your domain to Cloudflare”

If you have not already, sign up at dash.cloudflare.com, add your domain (git.example.com), and let Cloudflare manage its DNS. Free tier is enough.

In the Cloudflare Zero Trust dashboard:

  1. Networks → Tunnels → Create a tunnel

  2. Pick a name (e.g. opencodehub-home).

  3. Cloudflare shows you a cloudflared install command and a token that looks like:

    eyJhIjoixxxxxxxxxxxxx... # ~64 bytes, base64

    Save this token; you will pass it as TUNNEL_TOKEN.

Alternatively, you can use the CLI:

Terminal window
cloudflared tunnel login # opens a browser
cloudflared tunnel create opencodehub-home
cloudflared tunnel token opencodehub-home
Section titled “Option A — Docker (recommended on Synology / QNAP / generic)”

Create a docker-compose.cloudflared.yml on the NAS:

services:
cloudflared:
image: cloudflare/cloudflared:2024.12.2
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
# Uncomment if you want host networking (lets cloudflared reach
# services on localhost without the docker bridge):
# network_mode: host
Terminal window
echo "TUNNEL_TOKEN=eyJhIjoixxxxxx..." > .env.cloudflared
docker compose -f docker-compose.cloudflared.yml --env-file .env.cloudflared up -d
Terminal window
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
| sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update && sudo apt install -y cloudflared
sudo cloudflared service install <TUNNEL_TOKEN>

Install the Cloudflare Tunnel community app, paste the tunnel token in the app config, and start it.

Step 2.4 — Route a public hostname to OpenCodeHub

Section titled “Step 2.4 — Route a public hostname to OpenCodeHub”

In the Cloudflare dashboard → your tunnel → Public Hostname:

SettingValue
Subdomaingit (or anything)
Domainexample.com
ServiceType: HTTP, URL: opencodehub:3000 (or localhost:3000 if cloudflared is host-networking)

If you used Option A (Docker bridge network), opencodehub must be the service name in your OpenCodeHub docker-compose.yml. If you used host networking, use localhost:3000.

Save. After a few seconds, https://git.example.com should reach your OpenCodeHub instance.

SITE_URL=https://git.example.com
TRUSTED_PROXIES=127.0.0.1,172.16.0.0/12 # add 100.64.0.0/10 if also using Tailscale

Cloudflare’s edge IPs are public; the requests arriving at your NAS come from the cloudflared daemon, which is local. So 127.0.0.1 in TRUSTED_PROXIES is sufficient. Cloudflare’s real client IP is in CF-Connecting-IP; OpenCodeHub’s middleware reads X-Forwarded-For / CF-Connecting-IP to set the real client IP.

Step 2.6 — Optional: password-protect /admin with Cloudflare Access

Section titled “Step 2.6 — Optional: password-protect /admin with Cloudflare Access”
  1. Access → Applications → Add an application → Self-hosted
  2. Name: OpenCodeHub admin
  3. Domain: git.example.com/admin*
  4. Policy: allow only your email / a specific group.

Now reaching the admin UI requires a one-time code from Cloudflare — no OpenCodeHub credentials are needed for the attacker, but they cannot reach the page at all.

Terminal window
curl -sI https://git.example.com
# HTTP/2 200
# server: cloudflare
# cf-ray: ...

And test git:

Terminal window
git clone https://git.example.com/<owner>/<repo>.git

Once your instance is reachable from the internet, lock it down:

  1. Set strong secrets. Rotate JWT_SECRET, SESSION_SECRET, INTERNAL_HOOK_SECRET, and RUNNER_SECRET with openssl rand -hex 32. Never reuse a value across environments.
  2. Enable rate limiting with RATE_LIMIT_ENABLED=true and set RATE_LIMIT_REDIS_URL to a Redis (or Upstash) instance — without Redis, rate-limit state is per-process and bypassable by horizontal scale-out.
  3. Put the database behind the same tunnel. PostgreSQL, Redis, and the MinIO S3 endpoint should not be exposed on the public internet. If you must use a public Postgres, require SSL and a strong password.
  4. SSH git is optional. Tailscale Funnel and Cloudflare Tunnel expose HTTP/S only. If you need SSH git (git@...), expose a separate Tailscale node (no Funnel) and have your devs join the tailnet, or terminate SSH at the Cloudflare edge with Cloudflare Spectrum.
  5. Set TRUSTED_PROXIES correctly — see the two example values above. Without this, attackers can spoof their client IP by sending X-Forwarded-For themselves and bypass IP-based rate limits.
  6. Enable 2FA for every account (especially admins). OpenCodeHub supports TOTP-based 2FA in user settings.
  7. Back up the storage path and database. Even with S3-compatible storage, the SQLite/Postgres database and .env need offsite backups. scripts/sync-storage.ts will mirror the storage root to a second bucket; pg_dump (or litestream for SQLite) handles the database.
  8. Monitor. Subscribe to the /api/health endpoint with a free uptime monitor (UptimeRobot, Healthchecks.io, BetterStack). Tailscale and Cloudflare both expose access logs you can ship to a SIEM.

  • Pick Tailscale Funnel if you already use Tailscale for other services on your home network, want zero DNS hassle, and are happy with a *.ts.net URL (or one custom domain).
  • Pick Cloudflare Tunnel if you need a real custom domain (git.example.com), want Cloudflare’s edge features (caching, WAF, rate limiting, Access), or expect any kind of public traffic.
  • Pick plain reverse proxy (deploy-nas.md) if you have a static public IP, are comfortable opening ports 80/443, and want to avoid the third-party dependency.

All three are production-acceptable; the difference is operational, not security. Tailscale Funnel and Cloudflare Tunnel have the edge on security because they do not require a publicly routable address on the NAS itself.


”TLS handshake failed” from your domain

Section titled “”TLS handshake failed” from your domain”
  • Confirm the tunnel daemon is running:
    • Tailscale: tailscale status shows the node as active and tailscale funnel status shows the port.
    • Cloudflare: the tunnel status is HEALTHY in the Zero Trust dashboard.
  • Confirm OpenCodeHub is reachable from the tunnel’s network namespace:
    • Tailscale (host networking): curl http://localhost:3000
    • Cloudflare (Docker bridge): docker exec cloudflared wget -qO- http://opencodehub:3000/

TRUSTED_PROXIES is not set, or it does not include the proxy IP. OpenCodeHub’s src/middleware.ts and src/middleware/rate-limit.ts only honour X-Forwarded-For from IPs in TRUSTED_PROXIES.

Tailscale Funnel and Cloudflare Tunnel both have a default timeout of 100 seconds per request. Large initial pushes that take longer than that will time out. Solutions:

  • Use git push --depth=... and shallow-clone for the first push.
  • Run git gc on the local clone before pushing.
  • For Cloudflare: configure a higher timeout under Tunnels → your tunnel → Timeout.

Tailscale Funnel does not support arbitrary TCP — only HTTP/S. For SSH, either:

  • Have your devs install Tailscale on their machines and SSH directly to the NAS via the tailnet (ssh user@nas-hostname), or
  • Use Cloudflare Spectrum (paid) to proxy TCP/22 to the NAS.

Local network is slow after enabling the tunnel

Section titled “Local network is slow after enabling the tunnel”

The tunnel daemon proxies every request through the edge, so local LAN clients may see higher latency. For LAN use, add a local DNS record (git.example.com192.168.x.y) and let LAN clients hit OpenCodeHub directly.