Skip to main content
Version: v0.10.2

Expose OpenWA with Cloudflare Tunnel

Cloudflare Tunnel (cloudflared) connects a public hostname on Cloudflare's edge to an OpenWA instance that is not otherwise reachable from the internet — behind NAT, on a home network, or in a private subnet. The tunnel client runs next to OpenWA and opens a long-lived outbound connection to Cloudflare, so no inbound ports have to be opened and no public IP is required. Cloudflare terminates TLS at the edge and forwards plain HTTP to OpenWA on 127.0.0.1:2785.

Because OpenWA already treats its single port as plain HTTP behind a TLS-terminating reverse proxy (see Deployment), a Cloudflare Tunnel drops in as a drop-in replacement for nginx, Caddy, or Traefik — including for the Socket.IO real-time channel and the bundled dashboard.

Prerequisites
  • An OpenWA instance running on the host (or in a container) reachable at http://127.0.0.1:2785. See Deployment if you have not come this far.
  • A Cloudflare account. The free plan is sufficient for testing and many production setups; see Free-plan limits for where it bites.
  • A domain name on Cloudflare DNS, and the cloudflared client installed on the host where OpenWA runs.

How it fits

OpenWA serves the REST API, the dashboard, Swagger, and the Socket.IO channel on one port (2785). A Cloudflare Tunnel routes a single public hostname to that one origin over HTTP, forwards WebSocket upgrades natively, and terminates TLS on Cloudflare's edge.

No inbound ports need to be opened on the host. The only persistent connection is the one cloudflared opens out to Cloudflare.

OpenWA environment

Set these variables on the OpenWA process before exposing it through a tunnel. They are covered in detail in Deployment and Configuration; reproduced here because they matter specifically for a tunneled deployment.

VariablePurposeWhy it matters behind a tunnel
TRUSTED_PROXIESIPs/CIDRs whose X-Forwarded-For header to honorWithout this, OpenWA reads the tunnel daemon's IP as the client and rate limiting / per-key IP allowlists misbehave. Set to the daemon's address (for example 127.0.0.1 on the host, or the daemon container's subnet in Docker).
BASE_URLPublic URL the API advertisesSet to your public hostname (for example https://wa.example.com) so banners and any URL-building code reference the public origin.
CORS_ORIGINSAllowed browser origins for the dashboardThe wildcard * is refined in production. Set explicit origins, for example https://wa.example.com.
CSP_UPGRADE_INSECURE_REQUESTSForces HTTPS in the dashboard's CSPLeave the production default (true) — Cloudflare's edge serves HTTPS, so the upgrade is a no-op.
ENABLE_SWAGGERInteractive docs at /api/docsSet to false on internet-facing deployments to reduce reconnaissance surface.
API_KEY_PEPPERHMAC pepper for stored API-key hashesRecommended defense in depth on any internet-facing deployment.
Protect the dashboard on public deployments

Exposing OpenWA publicly exposes both the API and the bundled dashboard on the same host. OpenWA authenticates the dashboard by API key, but you should still put a second factor in front of the dashboard surface. See Protect the dashboard.

Quick tunnel (for testing)

A quick tunnel gives you a throwaway *.trycloudflare.com URL with zero configuration — perfect for a one-off test. No Cloudflare login or DNS record is required.

# From the same host where OpenWA is running on :2785
cloudflared tunnel --url http://localhost:2785

cloudflared prints a random https://<random>.trycloudflare.com URL. Open it in a browser and the dashboard should load over HTTPS; the Socket.IO real-time channel should connect within a few seconds. The URL is valid only as long as the process runs and is not suitable for production.

Named tunnel (production)

A named tunnel binds a stable hostname to your OpenWA instance. You create it once, route DNS to it, and then run it.

On the host

# 1. Authenticate once
cloudflared tunnel login

# 2. Create the tunnel
cloudflared tunnel create openwa

# 3. Route a hostname on Cloudflare DNS to the tunnel
cloudflared tunnel route dns openwa wa.example.com

Then point the tunnel at OpenWA with a config file at ~/.cloudflared/config.yml:

tunnel: <TUNNEL_UUID_FROM_CREATE>
credentials-file: /root/.cloudflared/<TUNNEL_UUID>.json

ingress:
- hostname: wa.example.com
service: http://localhost:2785
- service: http_status:404

Run the tunnel:

cloudflared tunnel run openwa

Install it as a system service so it survives reboots:

sudo cloudflared service install

As a Docker sidecar

If OpenWA runs in Docker, run cloudflared as a sidecar on the same Compose network. Create the tunnel and its DNS route from your host first (the steps above), then mount the credentials file into the sidecar container.

services:
openwa:
image: ghcr.io/rmyndharis/openwa:latest
expose:
- "2785" # internal only — no host port mapping needed
environment:
- TRUSTED_PROXIES=172.16.0.0/12 # the cloudflared container's subnet
- BASE_URL=https://wa.example.com
- CORS_ORIGINS=https://wa.example.com
- ENABLE_SWAGGER=false

cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=<token-from-cloudflare-zero-trust-dashboard>
depends_on:
- openwa
Use the token-based setup for Docker

The cleanest way to run cloudflared in a container is to create the tunnel from the Cloudflare Zero Trust dashboard (Networks → Tunnels → Create a tunnel), copy the resulting TUNNEL_TOKEN, and point the container at it. The dashboard writes the tunnel's public hostname → service mapping for you, so no config.yml mount is required.

For the OpenWA container, prefer expose over ports. Mapping the port to the host is harmless on its own, but if you ever stack another reverse proxy in front (Traefik, Coolify, a load balancer), the double hop exhausts the long-lived Socket.IO connections and the dashboard starts returning 504s. See Deployment for the full Traefik-equivalent note.

Free-plan limits

The Cloudflare free plan is enough for many single-tenant deployments, but it imposes two limits that interact with a WhatsApp API gateway:

LimitFree planEffect on OpenWA
Request body size100 MBMedia sends carry base64 in the JSON body, so a 75 MB video can exceed the limit after encoding overhead. Large media uploads are the most common breakage.
Per-request timeout100 secondsLong synchronous operations (a slow bulk send, a session start that waits for QR readiness) can be cut off mid-flight.
No SLA on the tunnel daemonBest-effortAdequate for self-hosting; for higher availability use multiple cloudflared replicas on a paid plan.

If you hit these, either move the affected workload to a paid Cloudflare plan or keep that specific operation on a non-tunneled path (for example, a private network route for media uploads).

Cloudflare features that can interfere with API traffic

A freshly provisioned hostname inherits Cloudflare's proxy (orange-cloud) defaults. Some of those defaults disrupt programmatic API traffic:

  • Bot Fight Mode and Browser Integrity Check can challenge or block API clients. Disable them for the OpenWA hostname in Security → Bots and Security → Settings.
  • Rate limiting rules under Security → WAF → Rate limiting rules can collide with OpenWA's own throttler. Either tune them or disable them for the hostname.
  • Caching can cache a GET that should be live (for example, /api/sessions/{id}/qr). Either set the hostname to bypass cache, or restrict caching to known-static paths.

Protect the dashboard

On a public deployment, OpenWA's API-key auth is the only thing between the dashboard and the internet. Add a second factor with Cloudflare Access (part of Zero Trust, free up to 50 users):

  1. In the Cloudflare Zero Trust dashboard, open Access → Applications → Add an application.
  2. Create a Self-hosted application for the OpenWA hostname (wa.example.com).
  3. Add a policy — for example, allow your email address or an identity provider group.
  4. Optionally create a second application scoped to a path like wa.example.com/api/* with a different (or no) policy if the API must be reachable from arbitrary API clients that cannot complete an interactive login.

For machine-to-machine API access through an Access-protected hostname, use a service token (Access → Service Auth → Service Tokens) and send the CF-Access-Client-Id / CF-Access-Client-Secret headers alongside OpenWA's X-API-Key.

WebSocket and real-time notes

OpenWA's dashboard upgrades to a WebSocket on /socket.io/. Cloudflare Tunnels forward WebSocket upgrades natively, so no special configuration is required. If the dashboard's status indicator stays on "Connecting…" while the REST API works:

  • Confirm cloudflared is running and the tunnel reports Healthy in the Zero Trust dashboard.
  • Confirm nothing else (another proxy, a WAF rule, an aggressive idle-timeout policy) sits between the edge and OpenWA — the dashboard's real-time channel is the most sensitive canary for a misconfigured proxy hop.

Troubleshooting

SymptomCauseFix
Client IP in audit logs is always the tunnel daemonTRUSTED_PROXIES unsetSet it to the daemon's IP/subnet so X-Forwarded-For is honored.
Dashboard loads but status stuck on "Connecting…"WebSocket upgrade blocked upstreamVerify the tunnel is healthy; remove any extra proxy hop; check Cloudflare WAF rules.
413 Request Entity Too Large on media sendsFree-plan 100 MB body limitMove media to a paid plan, or send it over a private network route that bypasses the edge.
524 A Timeout Occurred on long operationsFree-plan 100-second per-request timeoutRestructure the call to be asynchronous (queue a job, poll a status endpoint), or move to a paid plan.
API clients intermittently challenged by a CAPTCHABot Fight Mode onDisable Bot Fight Mode for the OpenWA hostname.
Dashboard works but GET /api/sessions/{id}/qr returns stale dataCloudflare cached the responseBypass cache for /api/*, or set the hostname to "DNS only" (grey-cloud) for testing.
Tunnel reconnects frequentlyHost network flappingPin the tunnel to a wired connection; consider running two cloudflared replicas for redundancy.

For app-level issues unrelated to the tunnel, see Troubleshooting & FAQ.

Next steps

  • Deployment — the canonical production setup with a self-hosted reverse proxy.
  • Configuration — the full environment-variable reference.
  • Authentication — API keys, roles, and per-key IP allowlists.
  • Webhooks — outbound delivery, which is unaffected by the inbound tunnel.