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.
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
- Open the Cloudflare dashboard and go to Networking → Tunnels. (In the older Zero Trust dashboard this is Networks → Tunnels.)
- Click Create a tunnel, choose Cloudflared, give it a name (e.g.
pelican) and save it. - 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 runcloudflaredfor you. - 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
- Subdomain and Domain: the hostname you want the panel on, e.g.
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.
TUNNEL_TOKEN=paste-your-tunnel-token-here
Then create compose.yml next to it:
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_URLis the publichttps://hostname from the tunnel.BEHIND_PROXYmakes the panel listen on plain HTTP port 80 and skip Let's Encrypt, since Cloudflare handles TLS (SSL). See Reverse Proxy Configuration.TRUSTED_PROXIESis the Docker network's subnet, so the panel trusts the forwarded headers from thecloudflaredcontainer. 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
443and the listen port to8080, and point the tunnel's service athttp://<wings host>:8080.
Either way, keep in mind:
- SFTP (port
2022by 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(notlocalhost:80) and the type isHTTP. - Check the panel container is running with
docker compose ps. On first start, it may still be applying migrations, seedocker compose logs panel.
The site redirects in a loop, or the panel exits complaining about a Let's Encrypt email
BEHIND_PROXYis not set to"true".
Pages load but links, assets or logins use the wrong address
APP_URLmust match the tunnel hostname exactly, includinghttps://.
File uploads fail
TRUSTED_PROXIESmust cover the subnet thecloudflaredcontainer 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/remoteand cannot complete an Access login, so before connecting Wings either remove the policy or add a Bypass policy forpanel.example.com/api/remote.