主节点部署 (Security Hardened)¶
[!CAUTION] 前置条件:请务必先完成 安全基线与加固 的检查,确保防火墙和系统配置已加固。
部署流程¶
0. OS: 安全初始化 (防火墙 + SSH加固) -> 见《安全基线与加固》
↓
1. CLI: 部署基础组件 (Tailscale + Portainer)
↓
2. 隧道: 使用 SSH Tunnel 首次连接 Portainer (Localhost Only)
↓
3. 验证: 确认 Tailscale0 网卡状态
↓
4. 网关: 部署 Traefik (严禁开启 insecure dashboard)
↓
5. 应用: 部署 Homepage / Beszel
1. 基础组件部署 (CLI)¶
推荐目录:
/opt/infra/bootstrap
1.1 准备环境¶
sudo mkdir -p /opt/infra/bootstrap
sudo chown -R $USER:$USER /opt/infra
ln -s /opt/infra ~/infra
# 1. 检查并加载 TUN 模块
if [ ! -c /dev/net/tun ]; then
sudo modprobe tun
fi
cd ~/infra/bootstrap
1.2 配置文件 (docker-compose.yml)¶
[!SECURE] 安全变更: 1. Portainer 仅监听
127.0.0.1:9000,杜绝公网扫描。 2. Tailscale 移除privileged: true,改用 CapAdd (最小权限)。
services:
# 1. 网络底座 (Host Mode)
net-tailscale:
image: tailscale/tailscale:latest
container_name: net-tailscale
hostname: chuncheon1
network_mode: host
restart: always
cap_add:
- NET_ADMIN
- SYS_MODULE
volumes:
- ./tailscale-data:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
environment:
- TS_AUTHKEY=tskey-auth-kBZ4MoVYE321CNTRL-B4VqdYsdmMRh5sp5bn7wMRz3rThQTwXT
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
- TS_EXTRA_ARGS=--accept-routes
# 2. 核心管理 (Bridge Mode + Traefik Secure Access)
ops-portainer:
image: 6053537/portainer-ce:latest
container_name: ops-portainer
restart: always
ports:
# [Break-Glass] 仅保留 127.0.0.1 以备 Traefik 挂掉时通过 SSH Tunnel 救急
- "127.0.0.1:9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./portainer-data:/data
networks:
- proxy-kr
labels:
- "traefik.enable=true"
# [Router] 域名访问 (建议使用 *.i.120224.xyz 并解析到 Tailscale IP)
- "traefik.http.routers.portainer.rule=Host(`portainer.i.120224.xyz`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.routers.portainer.service=portainer"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
# [Middleware] 强制应用 IP 白名单 (Tailscale Only)
- "traefik.http.routers.portainer.middlewares=security-ip-whitelist@docker"
networks:
proxy-kr:
name: proxy-kr
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24
1.3 启动¶
echo "TS_AUTHKEY=tskey-auth-xxxxx" > .env
docker compose up -d force-recreate
2. 首次访问 (SSH Tunnel)¶
由于 Portainer 只监听了 127.0.0.1,你必须打洞才能访问。
2.1 建立隧道 (在你的本地电脑执行)¶
# 格式: ssh -L 本地端口:127.0.0.1:远程端口 用户@服务器IP
ssh -L 9000:127.0.0.1:9000 user@chuncheon1.yourdomain.com
2.2 初始化与救急 (Break-Glass)¶
场景:Traefik 挂了/配置错了,导致 https://portainer.i... 无法访问。此时必须通过 SSH 隧道绕过 Traefik,直接连接 Portainer 的 127.0.0.1:9000 端口。
-
建立隧道 (在本地电脑执行):
(保持 SSH 窗口开启不要关闭)# 将服务器的 localhost:9000 映射到你电脑的 9000 ssh -L 9000:127.0.0.1:9000 user@chuncheon1.yourdomain.com -
访问面板:
- 浏览器打开:
http://localhost:9000 - 现在你直接连到了 Portainer 内部,不受 Traefik 影响。
- 在这里你可以重启 Traefik 容器,或修复配置。
- 浏览器打开:
-
常规初始化:
- 首次登录设置管理员密码。
- 完成后,Portainer 就运行在安全环境中,外网扫描不到,平时通过 Traefik 域名访问。
3. 部署 Traefik-KR (区域网关)¶
[!SECURE] Dashboard 安全:严禁使用
--api.insecure=true。
-
准备目录:
sudo mkdir -p /opt/infra/stacks/net-traefik-kr/dynamic sudo touch /opt/infra/stacks/net-traefik-kr/acme.json sudo chmod 600 /opt/infra/stacks/net-traefik-kr/acme.json -
部署 Stack (Portainer):
- Stack Name:
net-traefik-kr
services:
net-traefik-kr:
image: traefik:v3.6.7
container_name: net-traefik-kr
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
# ⚠️ 必填:DNSPod 认证信息 (ID,Token)
- DNSPOD_API_KEY=YOUR_ID,YOUR_TOKEN
- DNSPOD_HTTP_TIMEOUT=30
command:
- "--log.level=INFO"
- "--api.dashboard=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.websecure.address=:443"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=proxy-kr"
# [DNS-01 验证] (无惧防火墙/端口限制)
- "--certificatesresolvers.letsencrypt.acme.email=your@email.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/acme.json"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=dnspod"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=120"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,119.29.29.29:53"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /opt/infra/stacks/net-traefik-kr/acme.json:/acme.json
networks:
- proxy-kr
labels:
- "traefik.enable=true"
# [Dashboard] 路由配置
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.rule=Host(`traefik.i.120224.xyz`)"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.tls=true" # 显式开启 TLS
- "traefik.http.routers.dashboard.tls.domains[0].main=*.i.120224.xyz"
- "traefik.http.routers.dashboard.tls.domains[0].sans=i.120224.xyz"
- "traefik.http.routers.dashboard.middlewares=auth,security-ip-whitelist@docker"
- "traefik.http.routers.dashboard.tls.certresolver=letsencrypt"
# [Security] 定义通用的 IP 白名单 (Localhost + Tailscale Subnet + Docker Gateway)
# 1. 127.0.0.1/32: 本机访问及 SSH Tunnel
# 2. 100.64.0.0/10: Tailscale 网络
# 3. 172.20.0.1/32: Docker 网关 (固定 IP) - 仅信任网关转发的流量,拒绝容器间横向攻击
- "traefik.http.middlewares.security-ip-whitelist.ipwhitelist.sourcerange=127.0.0.1/32,100.64.0.0/10,172.20.0.1/32"
# [Auth] Dashboard 基础认证 (htpasswd 生成)
- "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$..."
networks:
proxy-kr:
external: true
3.1 开放防火墙 (必做)¶
为了防止 Docker 绕过防火墙,我们在《安全基线》中安装了 ufw-docker。这会导致默认拦截 Traefik 的 80/443。
必须显式允许 Traefik 容器对外提供服务:
# 允许外部访问 Traefik 的 HTTP/HTTPS
sudo ufw-docker allow net-traefik-kr 80/tcp
sudo ufw-docker allow net-traefik-kr 443/tcp
# 验证 (应该看到 80/443 ALLOW FWD)
sudo ufw status | grep 80
4. 部署管理应用 (Homepage / Beszel)¶
配置同原文档,但请确保:
1. 不要映射端口 (如 ports: - 3000:3000 要删除),全靠 Traefik 反向代理。
2. 如果必须映射,请使用 127.0.0.1:3000:3000。