Skip to main content

Cloudflare Tunnel

A Cloudflare Tunnel lets you serve the panel over HTTPS without opening any inbound ports on your host. A small cloudflared container makes an outbound connection to Cloudflare, and Cloudflare forwards requests for your hostname down that connection to the panel.

This guide builds on the Docker installation. Read that page first, as the steps for backing up your encryption key, installing, and stopping the panel are the same.

info

This page only covers the panel. Wings still needs to be reachable by your users' browsers and by the panel, see Wings below.

Prerequisites​

  • Docker CE with Docker Compose v2 (see the Docker installation guide)
  • A domain whose DNS is managed by Cloudflare
  • A Cloudflare Zero Trust account (the free plan is enough)

Create the Tunnel​

  1. Open the Cloudflare dashboard and go to Networking → Tunnels. (In the older Zero Trust dashboard this is Networks → Tunnels.)
  2. Click Create a tunnel, choose Cloudflared, give it a name (e.g. pelican) and save it.
  3. On the install screen, copy the token from the shown command (the long string after --token). You do not need to install anything on the host, Docker will run cloudflared for you.
  4. Under Routes, click Add route and choose Published application (called a public hostname in older versions of the dashboard):
    • Subdomain and Domain: the hostname you want the panel on, e.g. panel.example.com
    • Service Type: HTTP
    • URL: panel:80

panel:80 is the panel container's name and internal port. Cloudflare creates the DNS record for the hostname automatically.

Create compose.yml​

Create a directory for the panel, and in it a .env file holding your tunnel token. Docker Compose reads this file automatically, which keeps the token out of compose.yml.

.env
TUNNEL_TOKEN=paste-your-tunnel-token-here

Then create compose.yml next to it:

compose.yml
services:
panel:
image: ghcr.io/pelican/panel:latest
restart: always
networks:
- default
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- pelican-data:/pelican-data
- pelican-logs:/var/www/html/storage/logs
environment:
XDG_DATA_HOME: /pelican-data
APP_URL: "https://panel.example.com"
ADMIN_EMAIL: "USEYOUROWNEMAILHERE@example.com"
BEHIND_PROXY: "true"
TRUSTED_PROXIES: "172.20.0.0/16"

cloudflared:
image: cloudflare/cloudflared:latest
restart: always
networks:
- default
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${TUNNEL_TOKEN}

volumes:
pelican-data:
pelican-logs:

networks:
default:
ipam:
config:
- subnet: 172.20.0.0/16

Compared to the standard Docker setup:

  • There is no ports: section. Nothing is published on the host, all traffic arrives through the tunnel.
  • APP_URL is the public https:// hostname from the tunnel.
  • BEHIND_PROXY makes the panel listen on plain HTTP port 80 and skip Let's Encrypt, since Cloudflare handles TLS (SSL). See Reverse Proxy Configuration.
  • TRUSTED_PROXIES is the Docker network's subnet, so the panel trusts the forwarded headers from the cloudflared container. If you change the subnet, change both.

Start the Panel​

docker compose up -d

Check that the tunnel connected:

docker compose logs cloudflared

You should see Registered tunnel connection, and the tunnel should show as Healthy in the Cloudflare dashboard.

Now back up your encryption key and open https://panel.example.com/installer to finish installing.

Wings​

Wings cannot share the panel's tunnel hostname. The panel talks to Wings, and your users' browsers open a websocket directly to Wings for the console, so the node's FQDN must be reachable on its own. You can either:

  • Expose Wings directly, optionally through the Cloudflare proxy. See Enabling Cloudflare proxy.
  • Route a second public hostname on a tunnel to Wings. When creating the node, choose HTTPS with (reverse) proxy, which sets the connection port to 443 and the listen port to 8080, and point the tunnel's service at http://<wings host>:8080.

Either way, keep in mind:

  • SFTP (port 2022 by default) is plain TCP and does not work through an HTTP tunnel. Expose it directly if you need it.
  • Cloudflare limits request body size by plan (100 MB on Free and Pro). Uploads through a proxied Wings hostname larger than that will fail.
  • Run Wings in its own compose project rather than adding it to this one, so a not-yet-configured Wings does not restart in a loop alongside the panel.

Troubleshooting​

The tunnel is healthy but the site returns a 502 or "Unable to reach origin service"

  • Check the public hostname's URL is panel:80 (not localhost:80) and the type is HTTP.
  • Check the panel container is running with docker compose ps. On first start, it may still be applying migrations, see docker compose logs panel.

The site redirects in a loop, or the panel exits complaining about a Let's Encrypt email

  • BEHIND_PROXY is not set to "true".

Pages load but links, assets or logins use the wrong address

  • APP_URL must match the tunnel hostname exactly, including https://.

File uploads fail

  • TRUSTED_PROXIES must cover the subnet the cloudflared container is on.

Security​

  • Do not commit .env. Anyone with the tunnel token can run your tunnel.
  • Consider putting a Cloudflare Access policy in front of the panel, at least until the installer has been completed. Wings calls the panel's API at /api/remote and cannot complete an Access login, so before connecting Wings either remove the policy or add a Bypass policy for panel.example.com/api/remote.