From cloudflare
Use this skill when the user asks about Cloudflare Tunnels (formerly Argo Tunnel), cloudflared, exposing local services to the internet securely, zero-trust ingress, deploying cloudflared via docker-compose, running cloudflared in a Proxmox LXC container, or managing Tunnels with Pulumi. For docker-compose GitOps deployment patterns, also recall the arcane plugin's arcane-gitops skill. For Proxmox LXC host setup, recall the proxmox plugin's proxmox-lxc skill.
npx claudepluginhub nsheaps/ai-mktpl --plugin cloudflareThis skill uses the workspace's default tool permissions.
Cloudflare Tunnels create encrypted, outbound-only connections from your infrastructure to Cloudflare's network. No public IP, open ports, or firewall rules needed — the `cloudflared` daemon runs on your server and reaches out to Cloudflare.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Cloudflare Tunnels create encrypted, outbound-only connections from your infrastructure to Cloudflare's network. No public IP, open ports, or firewall rules needed — the cloudflared daemon runs on your server and reaches out to Cloudflare.
cloudflared (brew install cloudflared)cloudflare.ZeroTrustTunnelCloudflared, cloudflare.ZeroTrustTunnelCloudflaredConfigcloudflare/cloudflared (Docker Hub)Internet → Cloudflare Edge → Tunnel (encrypted) → cloudflared → Your Services
↓
Access Policies (optional)
WAF / DDoS Protection
SSL Termination
The tunnel is outbound-only — your server connects to Cloudflare, not the other way around. This means:
Create a tunnel in the Cloudflare dashboard or via API, then use the generated token:
cloudflared tunnel --no-autoupdate run --token <TUNNEL_TOKEN>
cloudflared tunnel login # Interactive browser auth
cloudflared tunnel create my-tunnel
cloudflared tunnel run my-tunnel # Uses ~/.cloudflared/<tunnel-id>.json
# ~/.cloudflared/config.yml
tunnel: <TUNNEL_UUID>
credentials-file: /path/to/credentials.json
ingress:
- hostname: app.example.com
service: http://localhost:8080
- hostname: api.example.com
service: http://localhost:3000
originRequest:
noTLSVerify: true # Skip TLS verification to origin
connectTimeout: 30s
- hostname: ssh.example.com
service: ssh://localhost:22
- hostname: rdp.example.com
service: rdp://localhost:3389
- service: http_status:404 # Catch-all (REQUIRED — must be last)
| Protocol | Example | Use Case |
|---|---|---|
http://host:port | Web apps, APIs | Most common |
https://host:port | TLS-terminated origins | When origin has its own cert |
ssh://host:port | SSH access | Remote shell via browser |
rdp://host:port | Remote Desktop | Windows RDP via browser |
tcp://host:port | Raw TCP | Databases, custom protocols |
unix:/path/to/socket | Unix sockets | Docker socket, etc. |
http_status:404 | Static response | Catch-all fallback |
# docker-compose.yaml
name: cloudflared
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: always
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
networks:
- cloudflared
networks:
cloudflared:
driver: bridge
name: cloudflared
For production deployments, avoid putting tokens in environment variables. Use an init container to fetch secrets from a secrets manager:
# docker-compose.yaml
name: cloudflared
services:
init-secrets:
image: 1password/op:2
user: "0:0"
environment:
- OP_SERVICE_ACCOUNT_TOKEN=${OP_SERVICE_ACCOUNT_TOKEN}
volumes:
- cloudflare-secrets:/run/secrets:rw
command:
- /bin/bash
- -c
- |
op read "op://Infrastructure/cloudflared/token" > /run/secrets/cloudflared_token
chmod 444 /run/secrets/*
cloudflared:
image: cloudflare/cloudflared:2026.3.0 # Pin version for reproducibility
restart: always
command: tunnel --no-autoupdate run --token-file /run/secrets/cloudflared_token
volumes:
- cloudflare-secrets:/run/secrets:ro
depends_on:
init-secrets:
condition: service_completed_successfully
networks:
- cloudflared
volumes:
cloudflare-secrets:
driver: local
networks:
cloudflared:
driver: bridge
name: cloudflared
Other docker-compose stacks join the cloudflared network to be reachable via the tunnel:
# In another stack's docker-compose.yaml
services:
my-app:
image: my-app:latest
networks:
- cloudflared
networks:
cloudflared:
external: true # Join the network created by the cloudflared stack
Then add the hostname → service mapping in the Cloudflare dashboard (or via Pulumi/API).
If using Arcane for GitOps deployment of docker-compose stacks, place the cloudflared compose file in your iac repo at hosts/<hostname>/cloudflared/docker-compose.yaml. Arcane will sync and deploy it automatically on push to main. See the arcane plugin's arcane-gitops skill for the full GitOps workflow.
Directory structure:
iac/
hosts/
my-server/
cloudflared/
docker-compose.yaml
my-app/
docker-compose.yaml # Joins cloudflared network
For running cloudflared in a Proxmox LXC container (lightweight alternative to a full VM):
# On the Proxmox host, create an unprivileged LXC container
pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname cloudflared \
--memory 512 \
--cores 1 \
--rootfs local-lvm:4 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--unprivileged 1
pct start 200
# Inside the LXC container
pct enter 200
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main" \
| tee /etc/apt/sources.list.d/cloudflared.list
apt update && apt install -y cloudflared
# Run as a service
cloudflared service install <TUNNEL_TOKEN>
systemctl enable --now cloudflared
For running docker-compose stacks (including cloudflared) in an LXC container, you need a privileged container with nesting enabled. See the proxmox plugin's proxmox-lxc skill for detailed setup instructions.
# Create privileged LXC with nesting for Docker support
pct create 201 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname docker-host \
--memory 2048 \
--cores 2 \
--rootfs local-lvm:20 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--features nesting=1 \
--unprivileged 0
pct start 201
# Inside the LXC, install Docker
pct enter 201
curl -fsSL https://get.docker.com | sh
# Then deploy cloudflared via docker-compose as shown above
import * as pulumi from "@pulumi/pulumi";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const cfConfig = new pulumi.Config("cloudflare");
const accountId = cfConfig.require("accountId");
// Generate a tunnel secret (32+ bytes, base64-encoded)
const tunnelSecret = config.requireSecret("tunnelSecret");
const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("my-tunnel", {
accountId,
name: "my-tunnel",
secret: tunnelSecret,
});
// Export the token for use with cloudflared
export const tunnelToken = tunnel.tunnelToken;
const tunnelConfig = new cloudflare.ZeroTrustTunnelCloudflaredConfig("my-tunnel-config", {
accountId,
tunnelId: tunnel.id,
config: {
ingressRules: [
{
hostname: "app.example.com",
service: "http://my-app:8080",
},
{
hostname: "api.example.com",
service: "http://my-api:3000",
originRequest: {
connectTimeout: "30s",
noTlsVerify: true,
},
},
{
// Catch-all (required, must be last)
service: "http_status:404",
},
],
},
});
const zone = cloudflare.getZoneOutput({ name: "example.com", accountId });
const appDns = new cloudflare.Record("app-dns", {
zoneId: zone.id,
name: "app",
type: "CNAME",
content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`,
proxied: true,
});
const apiDns = new cloudflare.Record("api-dns", {
zoneId: zone.id,
name: "api",
type: "CNAME",
content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`,
proxied: true,
});
Protect tunnel-exposed services with Zero Trust Access (see the cloudflare-zero-trust skill):
const accessApp = new cloudflare.ZeroTrustAccessApplication("app-access", {
zoneId: zone.id,
name: "Internal App",
domain: "app.example.com",
type: "self_hosted",
sessionDuration: "24h",
});
const accessPolicy = new cloudflare.ZeroTrustAccessPolicy("allow-team", {
applicationId: accessApp.id,
zoneId: zone.id,
name: "Allow Team",
precedence: 1,
decision: "allow",
includes: [
{
emails: ["admin@example.com"],
},
],
});
# Check tunnel status
cloudflared tunnel info my-tunnel
# View tunnel metrics (Prometheus format)
cloudflared tunnel --metrics localhost:2000
# Dashboard: see active connections, latency, errors
# https://dash.cloudflare.com/?to=/:account/networks/tunnels
| Issue | Solution |
|---|---|
| Tunnel won't connect | Check TUNNEL_TOKEN / credentials file. Verify DNS resolves |
ERR_CONNECTION_REFUSED | Origin service isn't running or wrong port |
| Intermittent 502s | Increase connectTimeout in ingress config |
| Can't reach service from tunnel | Ensure both containers are on the same Docker network |
| Token file permission denied | Check volume mount permissions (:ro vs :rw) |
| LXC DNS issues | Add --nameserver 1.1.1.1 to LXC config |