Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions examples/cloudflare-tunnel-lan/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Cloudflare Tunnel Token
# Get this from: Cloudflare Dashboard -> Zero Trust -> Networks -> Tunnels
# Create a tunnel and copy the token from the install command
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here

# Public hostname routed through Cloudflare
PANEL_DOMAIN=panel.yourdomain.com

# Panel Configuration
ADMIN_EMAIL=admin@yourdomain.com

# APP_URL (used by Pelican for generating links)
APP_URL=https://${PANEL_DOMAIN}

# LAN binding address (host IP that should accept LAN HTTPS connections)
LAN_BIND_ADDRESS=192.168.1.50

# Docker Network Configuration
DOCKER_SUBNET=172.21.0.0/16

# PHP upload/post size limit
PHP_UPLOAD_LIMIT=256M
38 changes: 38 additions & 0 deletions examples/cloudflare-tunnel-lan/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
admin off
servers {
trusted_proxies static {$DOCKER_SUBNET}
}
}

:80 {
root * /var/www/html/public
encode gzip

php_fastcgi 127.0.0.1:9000 {
env PHP_VALUE "upload_max_filesize = {$PHP_UPLOAD_LIMIT}
post_max_size = {$PHP_UPLOAD_LIMIT}"
}

file_server
}

:443 {
root * /var/www/html/public
encode gzip

tls internal {
on_demand
}

header {
Strict-Transport-Security "max-age=31536000"
}

php_fastcgi 127.0.0.1:9000 {
env PHP_VALUE "upload_max_filesize = {$PHP_UPLOAD_LIMIT}
post_max_size = {$PHP_UPLOAD_LIMIT}"
}

file_server
}
24 changes: 24 additions & 0 deletions examples/cloudflare-tunnel-lan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Pelican Panel + Cloudflare Tunnel + LAN HTTPS

This variant extends the base Cloudflare Tunnel example by also exposing the Panel over HTTPS on your LAN IP. Cloudflared keeps the service private to the internet, while LAN devices can connect straight to the host when they are on the same network.

## What You Get

- `docker-compose.yml` that adds a bound 443/tcp listener for LAN clients while keeping the Cloudflare tunnel flow untouched
- `Caddyfile` that continues to serve the tunnel on plain HTTP and serves the same application over HTTPS with an internal CA for LAN traffic
- `.env.example` with the additional `LAN_BIND_ADDRESS` helper so you can decide which host interface should accept LAN HTTPS connections

## Important Notes About APP_URL

Pelican only serves requests for the canonical `APP_URL`. To keep authentication and signed URLs working, continue to visit the Panel using that hostname, even on the LAN. Achieve the “local IP” requirement by pointing that hostname at the server’s private IP when you are on-site (split-horizon DNS, router override, or a hosts file entry). When you are away, let public DNS resolve the hostname back to Cloudflare so the tunnel handles traffic.

## Usage

1. Copy this directory somewhere outside of the repository, rename `.env.example` to `.env`, and populate the variables.
2. Set `PANEL_DOMAIN`, `APP_URL`, and `ADMIN_EMAIL` as in the base example, then set `LAN_BIND_ADDRESS` to the LAN IP you want Docker to bind on (for example `192.168.1.50`).
3. Create your Cloudflare Tunnel token and assign your hostname to `panel:80` exactly like the base example.
4. Start the stack with `docker compose up -d` and watch logs via `docker compose logs -f cloudflared panel`.
5. On first LAN connection, Caddy issues an internal CA certificate. Export it with `docker compose cp pelican-panel:/data/caddy/pki/authorities/local/root.crt ./pelican-panel-root.crt` and trust it on devices so the browser accepts the LAN HTTPS session.
6. Configure your local DNS/hosts so the `APP_URL` hostname resolves to the LAN IP when you are on-site. Leave public DNS pointing at Cloudflare for remote access.

Once trusted, LAN traffic stays on your network over HTTPS while remote traffic continues to flow through Cloudflare Zero Trust.
43 changes: 43 additions & 0 deletions examples/cloudflare-tunnel-lan/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
services:
panel:
image: ghcr.io/pelican-dev/panel:latest
container_name: pelican-panel
restart: always
networks:
- pelican
ports:
- "${LAN_BIND_ADDRESS:-0.0.0.0}:443:443"
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security: Default binding to 0.0.0.0 exposes HTTPS to all interfaces.

The LAN_BIND_ADDRESS defaults to 0.0.0.0, which binds the HTTPS port to all network interfaces, potentially exposing the panel to the internet if the host is directly connected. Consider requiring users to explicitly set this variable without a default, or default to 127.0.0.1 with clear documentation to change it.

Apply this diff to require explicit configuration:

     ports:
-      - "${LAN_BIND_ADDRESS:-0.0.0.0}:443:443"
+      - "${LAN_BIND_ADDRESS:?LAN_BIND_ADDRESS must be set in .env}:443:443"

This will cause Docker Compose to fail with a clear error message if LAN_BIND_ADDRESS is not set, forcing users to make a conscious choice.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- "${LAN_BIND_ADDRESS:-0.0.0.0}:443:443"
- "${LAN_BIND_ADDRESS:?LAN_BIND_ADDRESS must be set in .env}:443:443"
🤖 Prompt for AI Agents
In examples/cloudflare-tunnel-lan/docker-compose.yml at line 9, the
LAN_BIND_ADDRESS currently defaults to 0.0.0.0 which exposes HTTPS on all
interfaces; change this to require explicit configuration or default to
127.0.0.1: remove the 0.0.0.0 fallback and use a required env var expansion (so
compose fails if unset) or set the default to 127.0.0.1, and update the
README/usage docs to explain how to override LAN_BIND_ADDRESS when binding to
other interfaces.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. Maintainers, do you have a preference?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find other docker-compose.yml files in the examples directory to check for patterns
fd -e yml -e yaml docker-compose examples/

# Check how other examples handle port bindings
fd -e yml -e yaml docker-compose examples/ --exec grep -A 2 -B 2 "ports:"

Length of output: 361


@k3vmcd Thanks for checking with the maintainers! I looked through the examples and didn't find an established pattern for port bindings with environment variables—this would be the first.

Here are the trade-offs for each approach to help inform the decision:

Option 1: Require explicit config (no default)

- "${LAN_BIND_ADDRESS:?LAN_BIND_ADDRESS must be set in .env}:443:443"
  • ✅ Most secure: forces conscious decision
  • ✅ Clear error message if forgotten
  • ❌ Slightly less convenient for quick testing

Option 2: Default to 127.0.0.1

- "${LAN_BIND_ADDRESS:-127.0.0.1}:443:443"
  • ✅ Secure by default (localhost only)
  • ✅ Still works out-of-box for tunnel-only usage
  • ✅ Users can override for LAN access
  • ❌ Defeats the purpose of the "LAN" variant if localhost-only

Option 3: Keep 0.0.0.0 with strong documentation

  • ✅ Most convenient, works immediately for LAN use case
  • ❌ Security risk if used carelessly
  • Requires prominent warnings in README

Given this is the LAN variant specifically designed for local network access, Option 2 (defaulting to 127.0.0.1) might not make sense. Perhaps Option 1 with a well-documented .env.example entry would balance security and usability?

Happy to adjust based on what the maintainers prefer! 🙂

extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- pelican-data:/pelican-data
- pelican-logs:/var/www/html/storage/logs
- ./Caddyfile:/etc/caddy/Caddyfile
environment:
XDG_DATA_HOME: /pelican-data
APP_URL: ${APP_URL}
ADMIN_EMAIL: ${ADMIN_EMAIL}
PANEL_DOMAIN: ${PANEL_DOMAIN}
DOCKER_SUBNET: ${DOCKER_SUBNET:-172.21.0.0/16}
PHP_UPLOAD_LIMIT: ${PHP_UPLOAD_LIMIT:-256M}

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

volumes:
pelican-data:
pelican-logs:

networks:
pelican:
name: pelican
ipam:
config:
- subnet: ${DOCKER_SUBNET:-172.21.0.0/16}
19 changes: 19 additions & 0 deletions examples/cloudflare-tunnel/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Cloudflare Tunnel Token
# Get this from: Cloudflare Dashboard -> Zero Trust -> Networks -> Tunnels
# Create a tunnel and copy the token from the install command
CLOUDFLARE_TUNNEL_TOKEN=your_tunnel_token_here

# Public hostname routed through Cloudflare
PANEL_DOMAIN=panel.yourdomain.com

# Panel Configuration
ADMIN_EMAIL=admin@yourdomain.com

# APP_URL (used by Pelican for generating links)
APP_URL=https://${PANEL_DOMAIN}

# Docker Network Configuration
DOCKER_SUBNET=172.21.0.0/16

# PHP upload/post size limit
PHP_UPLOAD_LIMIT=256M
18 changes: 18 additions & 0 deletions examples/cloudflare-tunnel/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
admin off
servers {
trusted_proxies static {$DOCKER_SUBNET}
}
}

:80 {
root * /var/www/html/public
encode gzip

php_fastcgi 127.0.0.1:9000 {
env PHP_VALUE "upload_max_filesize = {$PHP_UPLOAD_LIMIT}
post_max_size = {$PHP_UPLOAD_LIMIT}"
}

file_server
}
20 changes: 20 additions & 0 deletions examples/cloudflare-tunnel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Pelican Panel + Cloudflare Tunnel Example

This example shows how to run Pelican Panel entirely behind a Cloudflare Tunnel so you can keep every port on the host closed while still serving traffic over HTTPS.

## What You Get

- `docker-compose.yml` with Pelican Panel and `cloudflared` on an isolated Docker network
- `Caddyfile` tuned for Pelican Panel behind a reverse proxy, including higher upload limits
- `.env.example` documenting the environment variables that the stack expects

## Usage

1. Copy this directory somewhere outside of the repository, then rename `.env.example` to `.env` and fill in the variables.
2. Update `APP_URL` and `ADMIN_EMAIL` in the `.env` file to match your domain and administrator address.
3. Create a Cloudflare Tunnel in Zero Trust, copy the tunnel token, and paste it into `CLOUDFLARE_TUNNEL_TOKEN`.
4. Configure the tunnel to route your hostname (for example `panel.yourdomain.com`) to the internal service `panel:80`.
5. Start the stack with `docker compose up -d`.
6. Watch the logs with `docker compose logs -f cloudflared panel` and proceed through the Pelican installer once the services are healthy.

Detailed guidance and troubleshooting steps in the docs at `docs/panel/advanced/cloudflare-tunnel`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
42 changes: 42 additions & 0 deletions examples/cloudflare-tunnel/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
services:
panel:
image: ghcr.io/pelican-dev/panel:latest
container_name: pelican-panel
restart: always
networks:
- pelican
#ports:
# Port 80 not exposed - only accessed via Cloudflare Tunnel internally by cloudflared container
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- pelican-data:/pelican-data
- pelican-logs:/var/www/html/storage/logs
- ./Caddyfile:/etc/caddy/Caddyfile
environment:
XDG_DATA_HOME: /pelican-data
APP_URL: ${APP_URL}
ADMIN_EMAIL: ${ADMIN_EMAIL}
DOCKER_SUBNET: ${DOCKER_SUBNET:-172.21.0.0/16}
PHP_UPLOAD_LIMIT: ${PHP_UPLOAD_LIMIT:-256M}

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

volumes:
pelican-data:
pelican-logs:

networks:
pelican:
name: pelican
ipam:
config:
- subnet: ${DOCKER_SUBNET:-172.21.0.0/16}