Cloudflare Tunnel

Reach an OpenCode Manager running on your local network from anywhere over HTTPS, without opening ports on your router. This guide covers the Manager itself and the Preview panel, which needs a hostname of its own.

What You Need

  • A domain managed by Cloudflare, for example example.com.
  • A Cloudflare Tunnel with cloudflared running on a machine that can reach the Manager. See Cloudflare's Create a tunnel guide.
  • Two hostnames on that domain:
HostnameServesManager port
oc.example.comThe Manager5003
preview.example.comThe preview gateway5004

The examples below use these names; replace them with your own.

1. Publish Both Hostnames

In your tunnel, add a published application route for each hostname. The service URL depends on where cloudflared runs.

Run cloudflared next to the Manager in the same docker-compose.yml, so it reaches the Manager over the Compose network:

services:
  app:
    # the existing OpenCode Manager service

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
    restart: unless-stopped

Put the tunnel token from the Cloudflare dashboard in .env as CLOUDFLARE_TUNNEL_TOKEN, then add these routes:

HostnameService
oc.example.comhttp://app:5003
preview.example.comhttp://app:5004

Point the routes at the Manager's LAN address:

HostnameService
oc.example.comhttp://192.168.1.10:5003
preview.example.comhttp://192.168.1.10:5004

With Docker, ports 5003 and 5004 must be published, as they are in the default docker-compose.yml.

For a tunnel configured with a config.yml file:

tunnel: <TUNNEL-ID>
credentials-file: /etc/cloudflared/<TUNNEL-ID>.json

ingress:
  - hostname: oc.example.com
    service: http://192.168.1.10:5003
  - hostname: preview.example.com
    service: http://192.168.1.10:5004
  - service: http_status:404

2. Configure the Manager

Add to .env:

AUTH_TRUSTED_ORIGINS=https://oc.example.com
AUTH_SECURE_COOKIES=true

PASSKEY_RP_ID=oc.example.com
PASSKEY_ORIGIN=https://oc.example.com

PREVIEW_PUBLIC_URL=https://preview.example.com

Then restart the Manager. With Docker: docker compose up -d.

VariableWhy
AUTH_TRUSTED_ORIGINSLets the browser sign in from the tunnel hostname.
AUTH_SECURE_COOKIESThe tunnel serves HTTPS, so session cookies are marked secure.
PASSKEY_RP_ID, PASSKEY_ORIGINPasskeys are bound to the hostname. Passkeys added under another hostname (such as localhost) do not work here; add a new one from Settings > Account.
PREVIEW_PUBLIC_URLWithout it, the Preview panel loads https://oc.example.com:5004, which the tunnel does not serve, and the preview stays blank.

3. Turn Off Caching for the Preview Hostname

This step is required. Cloudflare caches JavaScript, CSS, images and fonts by default, and keeps responses that have no cache headers for up to two hours (Cloudflare default cache behavior). The cache cannot tell dev servers or users apart. Every dev server shares the one preview hostname, and the gateway picks the target from your login cookie. Without this rule:

  • Changes to your files do not appear for up to two hours.
  • Two projects that serve the same path, such as /main.js, can receive each other's files.
  • Cloudflare serves a cached file without going through the preview login, so anyone with its URL can download it.

Create a cache rule in the Cloudflare dashboard:

  1. Go to Caching > Cache Rules and select Create rule.
  2. Name it, for example OpenCode Manager preview: bypass cache.
  3. Under When incoming requests match, choose Custom filter expression: Hostname equals preview.example.com.
  4. Under Cache eligibility, select Bypass cache.
  5. Select Deploy.

The Manager hostname does not need this rule.

4. Check WebSockets

The Terminal and Preview live reload (HMR) use WebSockets. On your domain's Network page in the Cloudflare dashboard, make sure WebSockets is On.

5. Cloudflare Access (Optional)

If you protect oc.example.com with Cloudflare Access, choose one of these for the preview hostname:

  • Add it to the same Access application. In an application with several domains, Access issues the login cookie for the other domains after you sign in once, so the Preview frame never shows a login page (multi-domain applications).
  • Leave it out of Access. The preview gateway has its own login: it only opens a preview from a single-use token issued by a signed-in Manager, and answers every other request with 401.

Do not put the preview hostname in a separate Access application. The Preview frame would be sent to the Access login page, which cannot be shown inside a frame, and the preview stays blank.

Using the LAN Address as Well

PREVIEW_PUBLIC_URL applies however you open the Manager:

  • Through the tunnel (https://oc.example.com): Preview works in the panel.
  • Through a LAN address (http://192.168.1.10:5003): the panel still loads the preview from https://preview.example.com. That is a different site, so the browser blocks the preview cookie inside the frame. Use Open in new tab in the Preview toolbar, or use the tunnel hostname.

With AUTH_SECURE_COOKIES=true you also cannot sign in over plain HTTP, because browsers do not store secure cookies for http:// addresses. Sign in through the tunnel hostname.

Check the Setup

  1. Open https://oc.example.com and sign in.
  2. Start a dev server from the Terminal or Actions.
  3. Open Preview and select its port. The page appears in the panel.
  4. Edit a file the dev server serves. The change appears after live reload or a refresh.
  5. Optional: open https://preview.example.com in a private window. It shows Preview session expired. Reopen it from OpenCode Manager. (or the Access login page, if the hostname is behind Access), which confirms that the preview cannot be opened without signing in.

Troubleshooting

SymptomCauseFix
Preview stays blank or cannot connectPREVIEW_PUBLIC_URL is not set, so the panel uses port 5004 on the Manager hostnameSet PREVIEW_PUBLIC_URL and restart the Manager
Preview session expired. Reopen it from OpenCode Manager. inside the panel right after selecting a portThe browser blocked the preview cookie: the hostnames are on different domains, or the Manager was opened by its LAN addressPut both hostnames on the same domain and open the Manager through the tunnel, or use Open in new tab
Preview must run on a different origin than OpenCode Manager.PREVIEW_PUBLIC_URL points at the Manager's own hostnameUse a separate hostname for the preview
Old file contents, or another project's filesCloudflare cached the filesAdd the cache rule, then purge the cache under Caching > Configuration
Live reload does not work, or the Terminal does not connectWebSockets are offTurn on WebSockets
Access login page or a blank frame in the Preview panelThe preview hostname is in a separate Access applicationFollow Cloudflare Access
Sign-in fails on the tunnel hostnameAUTH_TRUSTED_ORIGINS does not include https://oc.example.comAdd it and restart the Manager