发布于

使用 Nginx 转发容器服务

作者

总体设想

总体而言,需要在服务器上搭建网络服务(Clash)、网络管理服务(Clash-GUI)、文件服务(Filebrowser)、服务器监控服务(InfluxDB)和容器监控服务(Portainer)。由于这些服务分别工作在不同的端口上,直接部署在裸机上就可以满足要求,但是为了方便后期的维护、升级、重新部署和管理,最终决定将这些服务全部容器化并通过 Docker 统一管理。

想要在单个主机的不同端口上暴露多个服务,有两种比较简单且通用的方式:第一种是按需编写 iptables 规则,通过域名和端口来转发服务请求,第二种是通过部署 Nginx 服务并编写相应规则实现服务转发。二者的区别是 iptables 工作在网络层而 Nginx 主要工作在应用层,性能上来讲 iptables 是占优的,但是在扩展性和规则配置的难易程度上,Nginx 更优,综合考虑后采用 Nginx 的方案。使用 Nginx 的另外一个好处是 Nginx 本身也可以作为容器部署在服务器上,更方便管理。

总体设想如下:

服务总体设想

服务部署

Clash 和 Clash-GUI

由于实验室有网络访问需求,因此需要部署 Clash 服务,此外为了方便管理 Clash,还需要额外部署 Clash-GUI。这里为了服务器重启后可以快速恢复服务,还需要在启动命令后添加 --restart always 选项,此外由于 Clash 需要代理本机网络,因此还需要设置 --network host

docker run --name clash --network host --restart always \
			-v /home/configs/clash/config.yaml:/root/.config/clash/config.yaml \
            -d dreamacro/clash-premium
docker run --name clash-ui --restart always -p 9091:80 -d haishanh/yacd

Portainer 和 InfluxDB

Portainer 是开源的容器管理工具,部署后可以可视化监控容器运行状态、管理和部署容器服务。

docker run -p 8000:8000 -p 9443:9443 -p 9000:9000 --name portainer --restart always \
		   -v /var/run/docker.sock:/var/run/docker.sock \
		   -v portainer_data:/data portainer/portainer-ce:latest

InfluxDB 则是开源的服务器监控工具,可以通过自定义规则实现不同类型的时序数据监控,比如服务器历史 CPU 核心占用情况、内存使用率、网络请求情况等。

docker run -d --name InfluxDB --restart always -p 8086:8086 \
		   -v /tmp/testdata/influx:/root/.influxdb2 --net=monitoring-net influxdb

Filebrowser

实验室内部有时需要存储和共享一些数据,一般的文档类数据我们保存在公共的飞书知识库中进行动态修改、协作、分享,而一些冷数据则需要有其他地方作存储。服务器本身除开 1T 的 NVME SSD 系统盘外还有一块 8T 的机械盘没有得到充分使用。服务器的文件系统采用 LVM 来配置,方便后期扩容、备份和管理,如下所示:

实验室服务器文件系统

起初的想法是使用成熟的自建网盘方案,比如 NextcloudCloudreve 来存储和管理冷数据。但是考虑到一般数据共享的情况我们已经通过飞书解决了,没必要多此一举,因而最终采用了更轻量的 Filebrowser 来作为冷数据管理的方案。

docker run \
    -v /public:/srv \
    -v /home/configs/filebrowser/database.db:/database.db \
    -v /home/configs/filebrowser/settings.json:/.filebrowser.json \
    -p 12345:80 \
    --name filebrowser \
    --restart=always \
    -d filebrowser/filebrowser

其中 settings.json 文件的内容如下:

{
  "address": "0.0.0.0",
  "port": 80,
  "locale": "zh-cn",
  "baseURL": "/",
  "log": "stdout",
  "database": "/database.db",
  "root": "/srv"
}

使用 Nginx 转发容器服务

实际上我们实验室本身拥有一个公共的域名 seu-netsi.net,而现在服务器又有了一个固定的内网 IP:10.201.0.222,理论上在内网内访问服务是不需要用到域名的,这里为了方便记忆各种服务的地址,额外配置了域名解析规则,如下所示,可以看到所有域名都指向了同一个内网服务器地址。

域名记录服务
server.seu-netsi.net10.201.0.222解析服务器地址,方便 SSH 连接
clash.seu-netsi.net10.201.0.222Clash Web GUI
docker.seu-netsi.net10.201.0.222Portainer Web GUI
dashboard.seu-netsi.net10.201.0.222InfluxDB Web GUI
filebrowser.seu-netsi.net10.201.0.222Filebrowser

在 Docker 中配置 Nginx 需要先拿到 Nginx 的配置文件,这里先启动一个 Nginx 容器,之后使用 docker cp 命令将 Nginx 容器默认的配置文件复制出来后以做后续修改:

docker cp nginx:/etc/nginx/nginx.conf /home/huohaodong/nginx.conf

之后按照域名、端口、服务之间的对应关系编写规则即可,最终得到相应的 Nginx 配置文件:

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    server {
        listen  80;
        server_name  server.seu-netsi.net;
        return 301 https://seunetsi.feishu.cn/wiki/wikcnmON8L4XBNML3I13R0k9gOf;
    }

    server {
        listen  80;
        server_name  filebrowser.seu-netsi.net;
        location / {
          proxy_pass  http://localhost:12345;
        }
    }

    server {
        listen  80;
        server_name  dashboard.seu-netsi.net;
        location / {
          proxy_pass  http://localhost:8086;
        }
    }

    server {
        listen  80;
        server_name  docker.seu-netsi.net;
        location / {
          proxy_pass  http://localhost:9000;
        }
    }

    server {
        listen  80;
        server_name  api.clash.seu-netsi.net;
        location / {
          proxy_pass  http://localhost:9090;
        }
    }

    server {
        listen  80;
        server_name  clash.seu-netsi.net;
        location / {
          proxy_pass  http://localhost:9091;
        }
    }

    include /etc/nginx/conf.d/*.conf;
}

这里有一点需要注意的是,在没有指定 --network 参数的情况下,Docker 默认会为每个新创建的容器分配一个子网合并与默认的 docker0 虚拟网络桥接。在上面的 Nginx 配置文件中,proxy_pass 设置的主机为地址为 localhost,这对于裸机部署的 Nginx 是可行的,但对于容器内部署的 Nginx 而言,其处于 Docker 为其分配的子网中,此时 localhost 指向的是这个子网而不是容器主机对应的 localhost。这时又由于其他服务不在 Nginx 的子网内,因此在默认网络配置情况下 Nginx 容器无法成功转发外部请求到对应的容器中。

解决上面的问题有几种方案,第一种方案也是最简单的方案,只需要在启动 Nginx 容器时指定 Nginx 容器网络为 host 即可,此时可以认为 Nginx 和主机处于同一网络环境内,因而可以正常转发服务。第二种方案则是利用 Docker 提供的网络管理服务来定制容器之间的网络通信行为,可以参考 docker 官方手册或者这个教程,具体实现细节这里不再赘述。

启动 Nginx 容器的命令如下:

docker run --name nginx -d --restart always \
		   --network host \
		   -v /home/configs/nginx/nginx.conf:/etc/nginx/nginx.conf nginx

总结

相比直接在裸机上配置各类服务,使用容器可以减少很多不必要的麻烦,部署、管理和升级起来都更加方便。多个容器之间进行互通需要留意 Docker 容器默认的网络行为,按需配置各个容器的网络互通关系。网络问题调试起来是比较困难且痛苦的,配置容器网络时要多加小心。