使用 Cloudflare Tunnel 和 Traefik 访问部署的 Docker 服务

✨ 文章记录了个人自建服务的“古法运维”方案:利用 Traefik 作为本地容器的反向代理和动态服务发现,结合 Cloudflare Tunnel 实现内网穿透和安全访问。

TOC

自部署的 Docker 服务用起来很舒心,但数量多了后,端口映射管理,防火墙端口规则,DNS 配置等也很麻烦。

Cloudflare Tunnel 服务,可以在不暴露服务器端口的情况下建立一条服务器连接到 Cloudflare 的隧道,再搭配上 Traefik 来实现各容器服务的服务发现和反向代理,从而可以在公网访问自部署的 Docker 服务。核心逻辑如下:

  1. 流量入口:用户访问域名 -> Cloudflare 边缘节点 -> Cloudflare Tunnel。
  2. 穿透层:cloudflared 容器通过隧道将流量拉取到本地 Docker 网络。
  3. 分发层:流量到达 Traefik,Traefik 根据 HTTP Host 规则(Labels)识别目标容器。
  4. 应用层:流量最终到达具体的业务容器(如 whoami)。

优势:无需公网 IP,无需在防火墙开启 80/443 端口,自带 SSL 证书管理。

流量路径示意

       [ 互联网 (Internet) ]
               ||
               \/
+---------------------------------------+
|        Cloudflare 全球网络             |
|    (DNS 解析 & 安全防护 & CDN)          |
+---------------------------------------+
               ||
      <== Cloudflare Tunnel ==> (加密隧道)
               ||
+--------------||------------------------+
|          宿主机 (Host)                 |
|              ||                       |
|   +----------\/----------+            |
|   |  cloudflared 容器    |            |
|   +----------||----------+            |
|              || (Docker Network: traefik)
|   +----------\/----------+            |
|   |    Traefik 容器      |            |
|   | (反向代理/服务发现)    |            |
|   +----------||----------+            |
|              || (路由分发)             |
|   +----------\/----------+            |
|   |  业务容器 (如 whoami) |            |
|   +----------------------+            |
+---------------------------------------+
💡

这套方案用了很久了,但早前都是手动配置了就开始用了。这次服务器被迫重装顺便记录下这种“古法运维”。


现在 LLM / AI Agent 已经很强了,这些内容都能生成或者直接给你配置完成。
这里就简单贴上配置,不做详细操作说明,毕竟 AI 大概干得更好。

Docker 环境配置略去不表。

Traefik 配置

traefik 容器网络配置脚本

network-setup.sh

#!/bin/bash

NETWORK_NAME=traefik

if [ -z $(docker network ls --filter name=^${NETWORK_NAME}$ --format="{{ .Name }}") ] ; then
    # 创建 Docker 网络
    output=$(docker network create ${NETWORK_NAME});
    # 检查输出是否为 64 位长度的字符串
    if [[ ${#output} -eq 64 ]]; then
        echo "create docker network for traefik ok"
    else
        echo "Network creation failed or output is not 64 characters long."
    fi
fi

执行👆脚本创建一个名为 Traefik 的容器网络。

traefik 容器配置

compose.yaml

services:
  traefik:
    container_name: traefik
    image: traefik
    restart: always
    command:
      - "--global.sendanonymoususage=false"
      - "--global.checknewversion=false"
      - "--entrypoints.web.address=:80"
      - "--api=true"
      - "--api.dashboard=true"
      - "--ping=true"
      - "--log.level=INFO"
      - "--log.format=common"
      - "--accesslog=false"
      - "--providers.docker=true"
      - "--providers.docker.watch=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.endpoint=unix:///var/run/docker.sock"
      - "--providers.docker.useBindPortIP=false"
      - "--providers.docker.network=traefik"
    ports:
      - "80:80" # 如本机/局域网也需要访问,则映射;仅走 Tunnel 可不映射
    environment:
      - TZ=Asia/Shanghai
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=traefik"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.okhk.net`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.entrypoints=web"
      # Traefik 管理面板安全认证配置
      - "traefik.http.routers.dashboard.middlewares=auth"
      # echo $(htpasswd -nb admin 你的密码) | sed -e s/\\$/\\$\\$/g
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."
      - "traefik.http.middlewares.gzip.compress=true"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    extra_hosts:
      - "update.traefik.io:127.0.0.1"
      - "collect.traefik.io:127.0.0.1"
      - "stats.g.doubleclick.net:127.0.0.1"
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider --proxy off localhost:8080/ping || exit 1"]
      interval: 3s
      retries: 10
    logging:
      driver: "json-file"
      options:
        max-size: "5m"
    networks:
      - traefik

networks:
  traefik:
    external: true

自部署服务配置

以 whoami 为例

whoami 容器配置

compose.yaml

services:
  whoami:
    image: traefik/whoami
    container_name: whoami
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.okhk.net`)"
      - "traefik.http.routers.whoami.entrypoints=web"
    networks:
      - traefik

networks:
  traefik:
    external: true

Traefik 可通过 lables 发现并代理 whoami 服务。

Cloudflare Tunnel 配置

cloudflared 容器配置

compose.yaml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: always
    command: tunnel --no-autoupdate run --token $TUNNEL_TOKEN
    environment:
      - TZ=Asia/Shanghai
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "cloudflared", "tunnel", "--metrics", "localhost:20241", "ready"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 20s
    networks:
      - traefik 
    
networks:
  traefik:
    external: true

.env

TUNNEL_TOKEN=

TUNNEL_TOKEN 获取

登录 Cloudflare Zero Trust 控制台 → Networks -> Tunnels,点击 Create a Tunnel。

https://one.dash.cloudflare.com/

可参考 Cloudflare 官网文档:

https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/

复制命令中的 --token 后面的长字符串,将其作为环境变量填入上述文件中,启动 cloudflared 容器,建立 Cloudflare Tunnel 链接。

Cloudflare Tunnel 路由配置

公网域名 (Public Hostname)服务协议内部服务 URL (Service)类型说明
*.okhk.netHTTPhttp://traefik:80泛域名转发 (推荐)
whoami.okhk.netHTTPhttp://traefik:80精确匹配

如果在 Cloudflare 中配置了泛域名(如 *.okhk.net)指向 http://traefik:80,那么之后新增 Docker 服务时,只需在 Docker Labels 里写好域名,无需再回 Cloudflare 页面操作。

如果使用精确域名,则后续每次新增 Docker 服务都需要手动配置域名转发到 http://traefik 。

(当然也可以借用一些工具实现根据 Traefik 服务发现情况自动调用 Cloudflare API 添加对应的 tunnel 路由规则,但出于某些原因(懒得整了)就没再弄了