服务部署 Step 1. Docker + Traefik Proxy
History
- 09 Apr. 2021, Update Cloudflare IPs
- 26 Jun. 2021, Polishing
假定我们已经购买了一台云服务器,并且已经装好了系统镜像,可以开机、可以通过 SSH 登入进去。
下面的操作和配置皆以 Debian 系统为例。除了偶尔使用 apt
命令安装之外,我们没有太多 Debian only 的操作(在其他系统上,使用系统自带的包管理器安装软件即可)。尤其是装了 Docker 之后,大多数操作实际上在 Debian 或是其他 Linux 发行版上都没区别。
现在我的服务都在 Docker 里运行,用 Traefik 作为统一的 Reverse Proxy 来处理所有 HTTPS 请求,根据域名或路径分发给不同应用的容器。目前看来数据备份、迁移还算方便:例如有一天我想将某个服务、或全部服务从一台服务器迁移至另一台服务器,只需要将相应的 docker-compose
配置文件夹及数据文件夹 rsync
到目标服务器,在目标服务器上运行 docker-compose up -d
就可以了,整个迁移过程相对省时省力。
域名
本页面及后续的服务搭建页面,均要求您拥有一个自己的域名以方便行事 —— 尤其是申请 HTTPS 证书。现在我假定您已经购买了一个域名,并能自己处理 DNS 解析。对于个人用户,我推荐把域名 DNS 解析 nameserver 设置成 Cloudflare,甚至把域名本身也直接迁移到 Cloudflare。此处仅作推荐,不是必须。
注 1:本页面及后续服务搭建页面,一般会以 my-example.com
域名作为例子进行说明。实际操作时请将代码里所有的该域名替换成您自己购买的域名,并记得在 DNS 服务商处将相应的域名指向您的服务器地址。
注 2:我喜欢给自己的每个服务创建一个二级域名,例如 FreshRSS 服务器就是 rss.my-example.com
,密码管理器就是 password.my-example.com
等。当然这些二级域名都可以指向同一台服务器 —— 如果您只有一台服务器的话。
Server Initial Setup
初次开机之后,我通常会做如下初步设置:
- 编辑
/etc/ssh/sshd_config
修改 SSH 连接信息,包括修改非22
端口、禁用 Root 登录、禁用密码认证等; - 更新软件包;
- 安装常用工具如
git
,gpg
,rsync
,python3-pip
等,此处不一一列举,需要的时候自行安装就是了; - 如果内存较小,可能需要开启 swap 空间;
- 配置自己顺手的终端。我将自己常用的 Zsh 配置(包括 Zinit 和 Powerlevel10k)备份到了 GitHub,通常新装服务器之后
clone
下来就可以用了。 - 安装
ufw
和fail2ban
,并设置相应规则。注意设置ufw
时首先应允许当前的 SSH 端口,然后再enable
,否则会把自己也拦在外面。如果云服务商本身就提供一个外部防火墙,而且我们不需要精细控制防火墙规则,那么可能不需要自己开防火墙,服务商提供的就够用了;ufw
- Uncomplicated Firewall,顾名思义,是一个用户友好的、简易的防火墙程序。可以认为它只是一个前端操作工具,其背后原理实际上还是在修改iptables
的规则。其 ‘Uncomplicated’ 就体现在,你只需使用ufw allow 443
这样简单的语法就可以开放一个端口,而不必去手写涉及到了各种 CHAIN 和 INTERFACES 的iptables
命令。- 需要注意的是,如果你使用 Docker 开放了某个容器的某个端口,并希望使用
ufw
来限制这个端口,例如仅允许部分 IP,这是做不到的:因为ufw
实际上是个用简易语法操作iptables
的前端,而 Docker 本身在开放每个容器的端口时也会在iptables
中添加相应的准入规则,而且 Docker 的规则优先级还在ufw
添加的规则之前。要想限制 Docker 的端口,您需要手动编辑iptables
里的DOCKER-USER
chain,或者在其之前插入你自己的 rule chain。
- 安装 Docker 和 Docker-Compose.
Setup Traefik
Traefik 是一个反向代理工具,简单来说,假如你的服务器上运行着多个服务(容器),分别有不同功能及对应不同的域名或路径:以我为例,我的服务器上运行着 FreshRSS 服务、RSSHub 服务、Bitwarden 密码管理服务,我希望用户在访问 password.my-example.com
时,流量被导向 Bitwarden 密码管理器对应的容器;而用户在访问 rss.my-example.com
时,流量自动导向 FreshRSS 对应的容器。Traefik 就是一个非常理想的完成这项任务的工具,并且它附带了自动管理 HTTPS 证书、负载均衡等功能。
本节我们启动一个 Traefik 容器,同时启动一个简单的 whoami 容器(用户访问它的时候,会回显用户的 HTTP 请求头信息),来确认我们的 Traefik 反向代理服务一切工作正常。下一篇文章再开始介绍具体的 Bitwarden、FreshRSS 等应用容器的搭建。后面那些具体应用,都是由本节启动的这个 Traefik 负责代理的。
假定我们已经购买了一个域名 my-example.com
,并且其 DNS 解析服务托管在 Cloudflare 上。如果你使用的不是 Cloudflare,则请按照这个链接里面的信息对下面的部分变量名进行调整。
我的习惯是,将所有容器的配置文件放在一个名为 site
的文件夹中,将所有的数据卷(volumes)放在一个叫做 volumes
的文件夹中。以后服务器备份、迁移的时候,也直接复制这两个文件夹即可。
Traefik 配置
首先为 Traefik 创建一个项目文件夹,例如:
mkdir -p ~/site/traefik
cd ~/site/traefik
然后创建一个 docker-compose.yml
文件,内容如下:
# docker-compose.yml
version: "3.5"
services:
traefik:
# The official v2.0 Traefik docker image
image: traefik
container_name: traefik
restart: always
ports:
# The HTTP port
# - "80:80"
# The HTTPS port
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
env_file:
# DNS Provider credentials
- dns_provider.env
volumes:
# So that Traefik can listen to the Docker events
- "${HOME}/volumes/traefik-letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik.toml:/etc/traefik/traefik.toml"
- "./dyn.toml:/dyn.toml"
networks:
- traefik
logging:
options:
max-size: "10m"
max-file: "2"
whoami:
# A container that exposes an API to show its IP address
image: containous/whoami
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.my-example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=mydnschallenge"
- "traefik.http.middlewares.whoami.headers.stsSeconds=311040000"
- "traefik.http.middlewares.whoami.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.whoami.headers.stsPreload=true"
networks:
- traefik
networks:
traefik:
name: traefik
注意到这里面包含了 2 个容器,即 Traefik,以及 Traefik 反向代理背后的其中一个 demo 容器 whoami。如上文所说,在后续文章里,Bitwarden、FreshRSS 这些具体的应用,都将是 Traefik 反向代理背后的具体业务容器。
这个 docker-compose.yml
文件引用了数个外部文件,这里一一道来:
dns_provider.env
: 我们希望网站是以 HTTPS 加密协议对外服务的,这就要求我们需要处理申请 HTTPS 证书事宜。好在 Traefik 提供了几个自动处理证书申请的方式,其共同点就是需要验证你对申请的证书所指示的域名的所有权。这里我们采用 DNS Challenge 的方式,也就是说,如果能验证你有权修改域名的 DNS 记录,那么就说明这个域名确实是你的,就可以给你颁发对应的 HTTPS 证书。我们的域名 DNS 是托管在 Cloudflare 上的,所以我们获取 Cloudflare 的 API Key,交给 Traefik,便可完成此验证。只需要 CF_API_EMAIL
和 CF_API_KEY
这两项就可以了。具体内容如下:
# dns_provider.env
# See docs at https://docs.traefik.io/https/acme/#providers
CF_API_EMAIL=[email protected]
CF_API_KEY=dfhkladhfakeyiuw764293ih4xq7x
${HOME}/volumes/traefik-letsencrypt
:存放证书的地方,这个会自动创建。
traefik.toml
:Traefik 的配置文件。
# traefik.toml
[log]
level = "DEBUG"
[providers]
[providers.docker]
[providers.file]
filename = "/dyn.toml"
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.forwardedHeaders]
trustedIPs = ["173.245.48.0/20","103.21.244.0/22","103.22.200.0/22","103.31.4.0/22","141.101.64.0/18","108.162.192.0/18","190.93.240.0/20","188.114.96.0/20","197.234.240.0/22","198.41.128.0/17","162.158.0.0/15","104.16.0.0/13","104.24.0.0/14","172.64.0.0/13","131.0.72.0/22","2400:cb00::/32","2606:4700::/32","2803:f800::/32","2405:b500::/32","2405:8100::/32","2a06:98c0::/29","2c0f:f248::/32"]
[certificatesResolvers.mydnschallenge.acme]
email = "[email protected]"
storage = "/letsencrypt/acme.json"
[certificatesResolvers.mydnschallenge.acme.dnsChallenge]
provider = "cloudflare"
这里面定义了一些 entryPoints
的名称,DNS Challenge 的名称等。在下文中将可以看到,我们会指定某个业务容器的 entrypoint
是 websecure
,也就是上面定义的名称,代表访问这个容器的访客,需要从 websecure
这个 Entry Point 指定的外部端口进来,在这里也就是 443
端口,即 HTTPS 的默认端口。
DNS Challenge 这里定义了一个名为 mydnschallenge
的 Resolver,其中定义我们使用的 DNS 服务商是 Cloudflare。而针对这个服务商的具体 Token 设置,在前面的 dns_provider.env
文件中有写。下文中将看到,我们会指定某个业务容器的 certresolver
为 mydnschallenge
,意味着,对于该容器所对应的域名,Traefik 将调用 mydnschallenge
这个 Resolver 来为该域名申请 HTTPS 证书。
另外有一个 forwardedHeaders.trustedIPs
。原因是,我的网站常常会在前面套一层 Cloudflare 的 CDN,当用户通过 Cloudflare 转发访问服务器时,Traefik 服务器会在 Cloudflare 转发过来的 HTTP 请求头中的 X-Forwarded-For
字段看到 2 个 IP:一个是访客访问 Cloudflare 时的真实 IP,例如 12.34.56.78
,另一个是 Cloudflare 回源服务器的 IP,例如 104.20.9.218
。
默认配置下,Traefik 并不会信任这个 X-Forwarded-For
信息,因为隔着 Cloudflare,它无法验证访客访问 Cloudflare 时是不是真的是 12.34.56.78
。Traefik 会直接丢弃这个字段,并且只相信眼见为实 —— Cloudflare 回源用来连接 Traefik 服务器所用的 IP 的确就是 104.20.9.218
,这是可以验证的。所以 Traefik 会仅将后面这个 Cloudflare 服务器的 IP 作为 X-Forwarded-For
字段转发给后面的容器。这就造成一个问题:当世界各地的访客通过 Cloudflare 代理访问我们的网站时,我们的网站程序看到的全是来自 Cloudflare 的访问,根本无法区分访客的来源。这里的 forwardedHeaders.trustedIPs
设置,即是让 Traefik 服务器信任所有来自 Cloudflare 转发的 X-Forwarded-For
字段,并将这个字段原样转发给后面的应用容器。这样应用便可以隔着层层代理看到访客的真正 IP。
这些 Cloudflare 的 IP 段来自这里。这部分 forwardedHeaders.trustedIPs
的配置不是网站运行必须的,只是方便辨识访客的真正 IP。
dyn.toml
:动态加载的配置,这个可有可无,我在这里设置了一些 TLS 参数,要求网站使用 TLS 1.2 或更新的标准,并且只使用被认为安全的加密算法。这个可以稍稍改善网站在 SSLLabs 测试的安全评级。
# dyn.toml
[tls.options]
[tls.options.default]
minVersion = "VersionTLS12"
cipherSuites = [
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
]
sniStrict = true
[tls.options.mintls13]
minVersion = "VersionTLS13"
whoami 容器示例
后面的内容就简单了,可以看到 whoami
容器通过 labels
设置了 Traefik 服务相关参数,这里再复制粘贴一下:
# docker-compose.yml
# ...
whoami:
# A container that exposes an API to show its IP address
image: containous/whoami
restart: always
labels:
- "traefik.enable=true"
# Replace with your own hostname
- "traefik.http.routers.whoami.rule=Host(`whoami.my-example.com`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=mydnschallenge"
- "traefik.http.middlewares.whoami.headers.stsSeconds=311040000"
- "traefik.http.middlewares.whoami.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.whoami.headers.stsPreload=true"
networks:
- traefik
# ...
其中:
routers.whoami
后面的whoami
是这个 router 的名字,可以自行修改。Host(`xxx`, `xxx`)
里定义了域名规则。括号里面可以定义多个域名,逗号隔开。如果要进一步定义路径,可以Host() && Path()
,同样,路径也可以有多个。entrypoints=websecure
定义了entryPoints
的名称。前面我们在traefik.toml
配置文件里已经定义了entryPoints.websecure
的地址是:443
。这个entryPoints
的名字也可以改,前后一致即可。tls.certresolver=mydnschallenge
指定了我们要用mydnschallenge
这个 DNS Challenge 方法去申请 HTTPS 证书,而这个mydnschallenge
的具体属性也是在traefik.toml
文件中定义的。- 后面 3 个
stsXxx
相关的参数也是提高 HTTPS 安全性,改善 SSLLabs 分数的。不是必须的。
启动容器
接下来,在 docker-compose.yml
文件所在的目录下,运行:
docker-compose up -d
然后稍等一会儿容器启动。由于初次申请证书可能需要时间,可能还需要等几十秒到一两分钟,网站才能正常访问。在浏览器中键入 whoami.my-example.com
(也就是 whoami 容器的 Host()
中的域名),应该会看到返回
Hostname: 7fb82276f82b
IP: 127.0.0.1
IP: 172.27.0.3
RemoteAddr: 172.27.0.4:47234
GET / HTTP/1.1
Host: whoami.my-example.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:86.0) Gecko/20100101 Firefox/86.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,zh-HK;q=0.5,zh-CN;q=0.3
Cookie: __cfduid=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Dnt: 1
Te: trailers
Upgrade-Insecure-Requests: 1
X-Forwarded-For: x.x.x.x
X-Forwarded-Host: whoami.my-example.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: eb2dc150ae10
X-Real-Ip: x.x.x.x
就算大功告成了。
Note: 本文中所有 Traefik 相关的配置如果意义不明,直接 Google 搜索 Traefik + 你想了解的配置项
通常可以搜到文档。