如何同时运行多个 docker 容器, 并通过不同的域名访问

作者: 科技微讯

日期:

我这个博客部署在腾讯云服务器, 对应的域名是 kejiweixun.com, 但我这个服务器还部署了其他网站或程序, 可以分别通过不同的域名访问.

kejiweixun.com 这个网站已经 docker 化了, 我还分享了 gatsby 网站 docker 化的方法, 通过 docker 部署感觉很方便, 所以我也把其他网站和程序都 docker 化了, 现在我的服务器运行的网站或程序都是一个个的 docker 容器.

于是问题来了, 一个服务器运行多个 docker 容器, 并且让这些容器通过独立的域名访问, 怎么做到的?

当然是用 nginx 啦. 把所有这些域名 dns 指向服务器的 ip 地址, 在服务器安装 nginx, 所有访问这个 ip 地址的流量首先经过 nginx, nginx 根据这些流量所对应的域名, 把流量转发给相对应的网站或程序. 例如访问 kejiweixun.com 的用户, 会被 nginx 转给 kejiweixun.com 这个 docker 容器.

既然服务器的网站和程序都可以 docker 化, 那这个负责转发流量的 nginx 服务器可以 docker 化吗? 当然可以啦! 事实上早就有人思考过这个问题了, 有人把这个 nginx 服务器配置完之后, docker 化成一个 image, 并开源给大家使用, 其中一个是 jwilder/nginx-proxy.

jwilder/nginx-proxy 如何使用

前提

首先你要知道怎么 docker 化一个网站或程序, 可参考我的 gatsby 网站 docker 化方法, 这里假设我已经创建了两个网站的 docker image, 分别叫 kejiweixun.com/blog1 和 kejiweixun.com/blog2, 假设要分别通过 blog1.kejiweixun.com 和 blog2.kejiweixun.com 访问, 当然不一定是两个子域名, 也可以是两个一级域名.

第一步: 创建网络

参考官方文档, 首先创建一个叫 nginx-proxy (也可以起其他名称) 的网络, 执行:

docker network create nginx-proxy

第二步: 运行 proxy

按照官方的文档, 接下来应该是下载并运行 jwilder/nginx-proxy 这个 image, 如下所示, 但并不建议这样做:

docker run --rm -d -p 80:80 --name nginx --net nginx-proxy -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy

这条命令包含一段 -v /var/run/docker.sock:/tmp/docker.sock:ro 的特殊属性, 意思是把宿主 (即你的电脑或云服务器等) 的 docker socket 映射到容器中, 让容器可以读取宿主的 docker socket, 这种做法相当于让容器拥有了对整个宿主的控制权, 而 jwilder/nginx-proxy 这个容器是暴露在网络中的, 所以存在安全隐患.

为了解决这个问题, jwilder/nginx-proxy 提供了第二种配置方式: 把 jwilder/nginx-proxy 拆分成 nginx 和 jwilder/docker-gen 两个容器运行, 其中 nginx 是对外的, 但它不直接绑定 docker socket, 而是通过 jwilder/docker-gen 绑定, 而 jwilder/docker-gen 在背后支持 nginx 运行, 不暴露在外网中.

按照官方文档, 如果你在乎安全性问题, 不要执行前面那条命令, 应该执行:

docker run --rm -d -p 80:80 --name nginx --net nginx-proxy -v /tmp/nginx:/etc/nginx/conf.d -t nginx

上面这条命令启动第一个容器 nginx, 注意通过 --net nginx-proxy 告诉这个 container 基于刚刚创建的网络运行. 运行 docker run 时, 如果本地电脑或服务器没有相应的 image, 会自动从 docker hub 下载, 下载完成后自动运行. -p 表示端口, 第一个 80 端口是服务器的端口, 你可以设置为其他值, 但建议 80, 因为这样访问域名时就不需要在域名最后加 :80 这样的东西, 第二个是容器 nginx 暴露出来的端口, 不是我们定义的, 不能随意改动.

第三步: 运行 docker-gen

接着启动第二个容器 jwilder/docker-gen:

docker run --rm -d --volumes-from nginx \
    --net nginx-proxy \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    -v $(pwd):/etc/docker-gen/templates \
    -t jwilder/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf

你可以看到这个容器绑定了 docker socket, 但正如前面所说, 这个容器不暴露在外网.

第四步: 运行网站容器

最后再分别运行 kejiweixun.com/blog1 和 kejiweixun.com/blog2 这两个容器, 继续参考官方文档, 逐一执行以下两条命令:

docker run -d --rm --expose 80 --net nginx-proxy -e VIRTUAL_HOST=blog1.kejiweixun.com kejiweixun.com/blog1
docker run -d --rm --expose 80 --net nginx-proxy -e VIRTUAL_HOST=blog2.kejiweixun.com kejiweixun.com/blog2

如果你已经把域名都指向了宿主的 ip 地址, 那现在应该可以分别通过这两个域名访问这两个网站.

如果你是在本地电脑测试, 还没有部署到云服务器, 那把 blog1.kejiweixun.com 和 blog2.kejiweixun.com 分别更换为 blog1.localhost 和 blog2.localhost:

docker run -d --rm --expose 80 --net nginx-proxy -e VIRTUAL_HOST=blog1.localhost kejiweixun.com/blog1
docker run -d --rm --expose 80 --net nginx-proxy -e VIRTUAL_HOST=blog2.localhost kejiweixun.com/blog2

然后在浏览器输入 blog1.localhost 和 blog2.localhost 就可以分别访问 blog1 和 blog2.

第五步: 通过 docker compose 运行

手动输入这么长的命令有点麻烦, 而且容易忘记, 建议把这些命令记录在 docker-compose.yml 文件, 文件内容如下:

version: "3.7"
services:
  blog1:
    image: kejiweixun.com/blog1
    container_name: blog1
    environment:
      - VIRTUAL_HOST=blog1.localhost
    expose:
      - "80"
    networks:
      - nginx-proxy
    restart: always
  blog2:
    image: kejiweixun.com/blog2
    container_name: blog2
    environment:
      - VIRTUAL_HOST=blog2.localhost
    expose:
      - "80"
    networks:
      - nginx-proxy
    restart: always
  nginx:
    image: nginx
    container_name: nginx
    networks:
      - nginx-proxy
    ports:
      - "80:80"
    restart: always
    volumes:
      - data:/etc/nginx/conf.d
  dockergen:
    image: jwilder/docker-gen
    container_name: docker-gen
    command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    networks:
      - nginx-proxy
    restart: always
    volumes:
      - data:/etc/nginx/conf.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
volumes:
  data:
networks:
  nginx-proxy:
    name: nginx-proxy

以后每次运行容器, 都只要执行 docker-compose up. VIRTUAL_HOST 的值分别是 blog1.localhost 和 blog2.localhost, 主要是方便你在本地电脑测试, 当你部署到服务器时记得修改成你打算使用的正式域名.

需要注意的是, 官方文档表示, 如果你把 jwilder/nginx-proxy 拆分为两个容器的方式部署, 就需要单独新建一个叫 nginx.tmpl 的文件, 内容可以从 github 复制过来, nginx.tmpl 应该和 docker-compose.yml 在同一目录.

准备好 docker-compose.yml 和 nginx.tmpl 这两个文件后, 在同一目录下执行 docker-compose up -d, 接着就可以在浏览器通过 blog1.localhost 和 blog2.localhost 访问网站了, 省略 -d 这个选项可以在命令行工具实时显示访问动态.

第六步: 开启 ssl

现在还差 ssl 证书, 当然你可以不部署证书, 但不支持 https 可能会被浏览器阻挡访问. 部署方法很简单, 如果你的域名托管在腾讯云阿里云, 那你可以直接在后台申请 ssl 证书, 具体怎么操作不在这篇文章的讨论范围, 文章已经很长了阿..

证书申请通过之后, 它会给你一个下载链接, 下载下来, 里面有两个文件, 一个 key, 一个 crt, 按照域名 + 后缀的方式重命名, 比如 kejiweixun.com 这个域名的话, 就把证书命名为: kejiweixun.com.crt 和 kejiweixun.com.key, 然后把它放在一个叫 sslcerts 的文件夹中, 这个文件夹的名称你可以自定义. sslcerts 要和 docker-compose.yml 位于同一目录.

然后把前面的 docker-compose.yml 修改成:

version: "3.7"
services:
  blog1:
    image: kejiweixun.com/blog1
    container_name: blog1
    environment:
      - VIRTUAL_HOST=blog1.localhost
    expose:
      - "80"
    networks:
      - nginx-proxy
    restart: always
  blog2:
    image: kejiweixun.com/blog2
    container_name: blog2
    environment:
      - VIRTUAL_HOST=blog2.localhost
    expose:
      - "80"
    networks:
      - nginx-proxy
    restart: always
  nginx:
    image: nginx
    container_name: nginx
    networks:
      - nginx-proxy
    ports:
      - "80:80"
    restart: always
    volumes:
      - data:/etc/nginx/conf.d
      - certs:/etc/nginx/certs #新增
  dockergen:
    image: jwilder/docker-gen
    container_name: docker-gen
    command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    networks:
      - nginx-proxy
    restart: always
    volumes:
      - certs:/etc/nginx/certs #新增
      - data:/etc/nginx/conf.d
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
volumes:
  data:
  certs: #新增
    driver: local #新增
    driver_opts: #新增
      type: none #新增
      device: /Users/kejiweixun/Desktop/blogs/ssl\_certs #这里替换成你的 ssl\_certs 文件夹所在的位置, 绝对位置
      o: bind #新增
networks:
  nginx-proxy:
    name: nginx-proxy

新增的内容意思是, 声明一个叫作 certs 的 named volume, 然后把 certs 绑定至 sslcerts 文件夹, 相当于把 sslcerts 赋值给 certs 变量, 然后这个 certs 变量就可以用在 blog1 和 blog2 这两个容器, 这样设置完就可以通过 https 访问你的网站, http 也会自动 301 跳转到 https.

总结

以后如果有更多容器, 只要在 docker-compose.yml 文件中添加这个容器的运行参数即可. 对了, 还有一个叫 https-portal 的容器, 如果你不喜欢 jwilder/nginx-proxy, 可以看看 https-portal.