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
cloudflaredrunning on a machine that can reach the Manager. See Cloudflare's Create a tunnel guide. - Two hostnames on that domain:
| Hostname | Serves | Manager port |
|---|---|---|
oc.example.com | The Manager | 5003 |
preview.example.com | The preview gateway | 5004 |
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-stoppedPut the tunnel token from the Cloudflare dashboard in .env as CLOUDFLARE_TUNNEL_TOKEN, then add these routes:
| Hostname | Service |
|---|---|
oc.example.com | http://app:5003 |
preview.example.com | http://app:5004 |
Point the routes at the Manager's LAN address:
| Hostname | Service |
|---|---|
oc.example.com | http://192.168.1.10:5003 |
preview.example.com | http://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:4042. 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.
| Variable | Why |
|---|---|
AUTH_TRUSTED_ORIGINS | Lets the browser sign in from the tunnel hostname. |
AUTH_SECURE_COOKIES | The tunnel serves HTTPS, so session cookies are marked secure. |
PASSKEY_RP_ID, PASSKEY_ORIGIN | Passkeys 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_URL | Without 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:
- Go to Caching > Cache Rules and select Create rule.
- Name it, for example
OpenCode Manager preview: bypass cache. - Under When incoming requests match, choose Custom filter expression: Hostname equals
preview.example.com. - Under Cache eligibility, select Bypass cache.
- 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 fromhttps://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
- Open
https://oc.example.comand sign in. - Start a dev server from the Terminal or Actions.
- Open Preview and select its port. The page appears in the panel.
- Edit a file the dev server serves. The change appears after live reload or a refresh.
- Optional: open
https://preview.example.comin 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
| Symptom | Cause | Fix |
|---|---|---|
| Preview stays blank or cannot connect | PREVIEW_PUBLIC_URL is not set, so the panel uses port 5004 on the Manager hostname | Set PREVIEW_PUBLIC_URL and restart the Manager |
| Preview session expired. Reopen it from OpenCode Manager. inside the panel right after selecting a port | The browser blocked the preview cookie: the hostnames are on different domains, or the Manager was opened by its LAN address | Put 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 hostname | Use a separate hostname for the preview |
| Old file contents, or another project's files | Cloudflare cached the files | Add the cache rule, then purge the cache under Caching > Configuration |
| Live reload does not work, or the Terminal does not connect | WebSockets are off | Turn on WebSockets |
| Access login page or a blank frame in the Preview panel | The preview hostname is in a separate Access application | Follow Cloudflare Access |
| Sign-in fails on the tunnel hostname | AUTH_TRUSTED_ORIGINS does not include https://oc.example.com | Add it and restart the Manager |
Related
- Preview — how the preview gateway works.
- Authentication — trusted origins, secure cookies and passkeys.
- Docker — ports and environment variables.
- Environment Variables —
PREVIEW_PORTandPREVIEW_PUBLIC_URL.