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
cloudflareddaemon 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.
At a glance
Section titled “At a glance”| Method | Outbound from home? | Inbound to home? | Free tier | Custom domain | Best for |
|---|---|---|---|---|---|
| Tailscale Funnel | yes (WireGuard) | none | yes (personal use) | yes, but only one hostname per funnel | Already using Tailscale; simple, single-user / small-team setups |
| Cloudflare Tunnel | yes (HTTP/2) | none | yes (unlimited tunnels) | yes, full DNS support | Public-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.
0. Prerequisites
Section titled “0. Prerequisites”Whatever exposure method you pick, you need:
- A working OpenCodeHub instance on your NAS, listening on a local port (default
3000). - A reachable hostname (Tailscale or Cloudflare) pointed at the tunnel.
- A persistent
cloudflaredortailscaledprocess — running on the NAS, in Docker, or as a system service. - Trusted Proxies correctly set in OpenCodeHub (so the real client IP is preserved and rate limits are not bypassed). See
TRUSTED_PROXIESin.env.example.
If you do not yet have OpenCodeHub running, see deploy-nas.md for the Docker setup first.
1. Tailscale Funnel
Section titled “1. Tailscale Funnel”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.
Why Tailscale Funnel
Section titled “Why Tailscale Funnel”- 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.
Step 1.1 — Install Tailscale on the NAS
Section titled “Step 1.1 — Install Tailscale on the NAS”On the NAS (or in a container with --net=host and tailscaled running
on the host), install Tailscale:
# 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 | shAuthenticate:
sudo tailscale upFollow the URL it prints, sign in, and approve the device. Confirm with:
tailscale status # should show your NAS as a nodetailscale ip -4 # 100.x.y.zStep 1.2 — Expose OpenCodeHub via Funnel
Section titled “Step 1.2 — Expose OpenCodeHub via Funnel”# 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:3000You 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:
sudo tailscale set --operator=$USERFor ACLs (which users in your tailnet can enable Funnel), see the Tailscale ACL docs.
Step 1.4 — Persistent funnel
Section titled “Step 1.4 — Persistent funnel”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:
sudo tailscale funnel 3000 onFor TrueNAS SCALE, configure the Tailscale app’s “Post-Start” command in the app config.
Step 1.5 — OpenCodeHub environment
Section titled “Step 1.5 — OpenCodeHub environment”Set the public URL in .env:
SITE_URL=https://nas-hostname.tail-net.ts.netTRUSTED_PROXIES=127.0.0.1,100.64.0.0/10 # Tailscale CGNAT rangeTailscale’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.
Step 1.6 — Verify
Section titled “Step 1.6 — Verify”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.
2. Cloudflare Tunnel
Section titled “2. Cloudflare Tunnel”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.
Why Cloudflare Tunnel
Section titled “Why Cloudflare Tunnel”- 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
/adminpaths. - 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.
Step 2.2 — Create the tunnel
Section titled “Step 2.2 — Create the tunnel”In the Cloudflare Zero Trust dashboard:
-
Networks → Tunnels → Create a tunnel
-
Pick a name (e.g.
opencodehub-home). -
Cloudflare shows you a
cloudflaredinstall command and a token that looks like:eyJhIjoixxxxxxxxxxxxx... # ~64 bytes, base64Save this token; you will pass it as
TUNNEL_TOKEN.
Alternatively, you can use the CLI:
cloudflared tunnel login # opens a browsercloudflared tunnel create opencodehub-homecloudflared tunnel token opencodehub-homeStep 2.3 — Run cloudflared on the NAS
Section titled “Step 2.3 — Run cloudflared on the NAS”Option A — Docker (recommended on Synology / QNAP / generic)
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: hostecho "TUNNEL_TOKEN=eyJhIjoixxxxxx..." > .env.cloudflareddocker compose -f docker-compose.cloudflared.yml --env-file .env.cloudflared up -dOption B — Bare-metal / systemd
Section titled “Option B — Bare-metal / systemd”curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \ | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/nullecho "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.listsudo apt update && sudo apt install -y cloudflaredsudo cloudflared service install <TUNNEL_TOKEN>Option C — TrueNAS SCALE
Section titled “Option C — TrueNAS SCALE”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:
| Setting | Value |
|---|---|
| Subdomain | git (or anything) |
| Domain | example.com |
| Service | Type: 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.
Step 2.5 — OpenCodeHub environment
Section titled “Step 2.5 — OpenCodeHub environment”SITE_URL=https://git.example.comTRUSTED_PROXIES=127.0.0.1,172.16.0.0/12 # add 100.64.0.0/10 if also using TailscaleCloudflare’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”- Access → Applications → Add an application → Self-hosted
- Name:
OpenCodeHub admin - Domain:
git.example.com/admin* - 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.
Step 2.7 — Verify
Section titled “Step 2.7 — Verify”curl -sI https://git.example.com# HTTP/2 200# server: cloudflare# cf-ray: ...And test git:
git clone https://git.example.com/<owner>/<repo>.git3. Hardening checklist
Section titled “3. Hardening checklist”Once your instance is reachable from the internet, lock it down:
- Set strong secrets. Rotate
JWT_SECRET,SESSION_SECRET,INTERNAL_HOOK_SECRET, andRUNNER_SECRETwithopenssl rand -hex 32. Never reuse a value across environments. - Enable rate limiting with
RATE_LIMIT_ENABLED=trueand setRATE_LIMIT_REDIS_URLto a Redis (or Upstash) instance — without Redis, rate-limit state is per-process and bypassable by horizontal scale-out. - 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.
- 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. - Set
TRUSTED_PROXIEScorrectly — see the two example values above. Without this, attackers can spoof their client IP by sendingX-Forwarded-Forthemselves and bypass IP-based rate limits. - Enable 2FA for every account (especially admins). OpenCodeHub supports TOTP-based 2FA in user settings.
- Back up the storage path and database. Even with S3-compatible
storage, the SQLite/Postgres database and
.envneed offsite backups.scripts/sync-storage.tswill mirror the storage root to a second bucket;pg_dump(orlitestreamfor SQLite) handles the database. - Monitor. Subscribe to the
/api/healthendpoint with a free uptime monitor (UptimeRobot, Healthchecks.io, BetterStack). Tailscale and Cloudflare both expose access logs you can ship to a SIEM.
4. When to use which method
Section titled “4. When to use which method”- 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.netURL (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.
5. Troubleshooting
Section titled “5. Troubleshooting””TLS handshake failed” from your domain
Section titled “”TLS handshake failed” from your domain”- Confirm the tunnel daemon is running:
- Tailscale:
tailscale statusshows the node as active andtailscale funnel statusshows the port. - Cloudflare: the tunnel status is HEALTHY in the Zero Trust dashboard.
- Tailscale:
- 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/
- Tailscale (host networking):
Real client IP is wrong
Section titled “Real client IP is wrong”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.
”git push” hangs
Section titled “”git push” hangs”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 gcon the local clone before pushing. - For Cloudflare: configure a higher timeout under Tunnels → your tunnel → Timeout.
SSH git does not work
Section titled “SSH git does not work”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.com → 192.168.x.y) and let LAN clients hit
OpenCodeHub directly.
6. See also
Section titled “6. See also”- deploy-nas.md — full NAS deployment guide (Docker, storage, ssh, etc.).
- storage-adapters.md — pick a storage backend for your repositories.
- security.md — accepted vulnerabilities, secret rotation, audit log review.
- Tailscale Funnel docs
- Cloudflare Tunnel docs