前言

这个博客建立之初,为了图方便以及便宜买的是阿里云的服务器。但随着需求的增加才知道阿里云安全组这么个东西,就是说服务器默认只开放22(SSH), 3389(RDP), 80(HTTP)和443(HTTPS)这几个端口。如果想要在服务器上整点别的活,就需要设置安全组放行,或者使用将要提到的端口复用。

Docker+Nginx初步

Docker安装

我使用的是

$ cat /proc/version

Linux version 5.15.0-86-generic (buildd@lcy02-amd64-086) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #96-Ubuntu SMP Wed Sep 20 08:23:49 UTC 2023

具体操作参考runoobaliyun,大致如下(分别为更新apt,下载依赖,设置仓库和安装docker)

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
sudo add-apt-repository \
   "deb [arch=amd64] https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/ \
   $(lsb_release -cs) \
   stable"
sudo apt-get install docker-ce docker-ce-cli containerd.io

之后pull下载Nginx以及其他服务的镜像。

docker-compose+Nginx配置

在进行反向代理之前,先尝试让Nginx正常工作。和直接使用Nginx相比,容器内部和服务器在网络和文件上都是隔离的,因此需要

  1. 通过端口映射让Nginx监听服务器端口,包括80和443端口
  2. 通过数据卷挂载让Nginx能够访问服务器文件,包括配置文件、博客静态文件和ssl证书
docker-compose vs dockerfile

网上的一些教程,如

  1. https://www.docker.com/blog/how-to-use-the-official-nginx-docker-image/
  2. https://medium.com/@mefengl/step-by-step-guide-to-deploying-nginx-using-docker-on-ubuntu-df0f520bcf2d
  3. https://www.nginx.com/blog/deploying-nginx-nginx-plus-docker/#Working-with-the-NGINX-Docker-Container

都是用的dockerfile,这种方式仅适用于构建单个镜像,而且只能匿名挂载,很不方便。就本文的目的而言其实并不需要构建镜像,直接使用docker-compose创建容器就完事了。而且试了下dockerfile为空甚至删掉dockerfile也没什么问题,因此后续主要使用docker-compose

另外如果按照网上的一些教程(如runoob)使用docker-compose调用dockerfile,此时会先执行dockerfile中的语句再执行docker-compose中的设置,即后者可能会覆盖前者。

数据卷挂载

先在服务器中选定文件夹作为根目录,在根目录创建docker-compose.yml以及nginx文件夹。再在nginx文件夹中创建confpublicssl文件夹分别用于存放配置文件、博客静态文件和ssl证书。

由于Nginx镜像是通用的,可以知道在容器内部

  1. 配置文件位于/etc/nginx/conf.d
  2. 静态文件位于/usr/share/nginx/html

配置文件如下

./docker-compose.yml:

version: '2'

services:
  nginx:
    build:
      context: ./nginx
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf:/etc/nginx/conf.d
      - ./nginx/public:/usr/share/nginx/html
      - ./nginx/ssl:/path/to/ssl
    restart: always

./nginx/conf/domain_name.conf:

server {
    listen 443 ssl;
    server_name domain_name;
    root /usr/share/nginx/html;
    index index.html;

    ssl_certificate /path/to/ssl/fullchain.pem;
    ssl_certificate_key /path/to/ssl/privkey.pem;
}

server {
    listen 80;
    server_name domain_name;
    location / {
        rewrite ^(.*)$ https://$host$1 permanent;
    }
}

另外由于ssl证书是周期更新的,需要修改更新脚本,使得每次更新之后抄送到./nginx/ssl

15 2 * */2 * certbot renew --post-hook "cp /etc/letsencrypt/live/omnibbfb.top/fullchain.pem /path/to/nginx/ssl && cp /etc/letsencrypt/live/omnibbfb.top/privkey.pem /path/to/nginx/ssl"

最后回到根目录创建容器并查看

$ docker-compose up -d --remove-orphans
[+] Running 2/2
 ⠿ Network temp_default    Created                                     0.1s
 ⠿ Container temp-nginx-1  Started                                     0.5s
$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                                                      NAMES
dafca187ed60   nginx:latest   "/docker-entrypoint.…"   4 seconds ago   Up 3 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp   temp-nginx-1

一个问题

问题如下,即创建容器正常但是查看容器为空

$ docker-compose up -d --remove-orphans
[+] Running 2/2
 ⠿ Network temp_default    Created                                     0.1s
 ⠿ Container temp-nginx-1  Started                                     0.5s
$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

这时候可以通过

docker-compose logs

查看日志

$ docker-compose logs
temp-nginx-1  | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
temp-nginx-1  | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
temp-nginx-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
temp-nginx-1  | 10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf is not a file or does not exist
temp-nginx-1  | /docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
temp-nginx-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
temp-nginx-1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
temp-nginx-1  | /docker-entrypoint.sh: Configuration complete; ready for start up
temp-nginx-1  | 2024/04/20 17:21:46 [emerg] 1#1: cannot load certificate "/etc/letsencrypt/live/omnibbfb.top/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/omnibbfb.top/fullchain.pem, r) error:10000080:BIO routines::no such file)
temp-nginx-1  | nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/omnibbfb.top/fullchain.pem": BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/omnibbfb.top/fullchain.pem, r) error:10000080:BIO routines::no such file)

发现报错!

猜测可能是容器运行错误导致关闭,但是docker-compose只负责创建容器,不会显示容器运行过程中的报错,导致乍一看没有什么问题。总之多看日志一定没什么坏处。

实现443端口复用

实现端口复用首先要用Nginx实现反向代理。

正向代理和反向代理

考虑这样一个关系:客户端 $\Longleftrightarrow$ 代理 $\Longleftrightarrow$ 服务器,那么

  1. 帮助客户端的就是正向代理(我是客户端,我请求服务,结果是服务器不一定知道客户端)
  2. 帮助服务器的就是反向代理(我是服务器,我提供服务,结果是客户端不一定知道服务器)

参考:终于有人把正向代理和反向代理解释的明明白白了!

Docker自定义网络

Nginx反向代理服务器之后,当客户端发送请求到服务器时,Nginx会进行分流,将请求转发到对应的后端服务上。但是Docker容器是彼此隔离的,此时需要创建自定义网络,处于同一网络的容器可以互通。注意这里并不需要使用docker network create创建,直接写在docker-compose.yml里就好了,比如

networks:
  nginx_net:
    name: nginx_net
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.0.0/16
          gateway: 192.168.0.1

services:
  nginx:
    ...
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    networks:
      - nginx_net

  another_service:
    ...
    container_name: another_service
    networks:
      - nginx_net

请注意

  1. gateway前面就是没有-的。
  2. another_service中不需要设置端口映射,其仅在内网中与Nginx交互。

自定义网络相关可以看这里

Nginx配置

在配置文件中加入server块处理其他上游服务

./nginx/conf/domain_name.conf:

server {
    listen 443 ssl;
    server_name sub.server_ip;

    include /etc/nginx/conf.d/ssl.conf;

    resolver 127.0.0.53;

    location / {
        proxy_pass http://another_service:3000;
    }
}

私以为这里是整个流程中逻辑上最为困难的部分了,因此需要详细说明

  1. 这里采用的是域名分流的方法,不同的服务使用不同的子域名,即sub.server_ipNginx收到请求后,会查看请求的Host头部字段,依此决定将请求路由到哪个server块进行处理。
  2. 起初申请ssl证书时使用了--standalone参数因此只对域名生效,子域名需要另外申请通配符ssl证书(certbot certonly --manual -d *.domain_name),同时修改更新脚本。
  3. 在内部网络中Nginx需要使用网络自带的DNS解析器解析后端服务器的ip,解析器的地址可以通过cat /etc/resolv.conf查看。
  4. another_service是后端服务器的名称,别忘了在docker-compose.yml中使用container_name进行指定。端口是这个服务默认的监听端口(内网端口而不是服务器端口),可以通过docker ps查看。

最后回到根目录创建网络以及容器即可

docker-compose up -d --remove-orphans
子域名解析

以上是需要在服务器上执行的全部操作,而由于使用了子域名,还需要去阿里云官网进行DNS解析,参考子域管理,这里不再赘述。

通过路径分流

另外也可以通过路径进行分流,大致如下(来自ChatGPT

server {
    listen 443 ssl;
    server_name yourdomain.com;
    
    ssl_certificate /path/to/ssl/certificate.pem;
    ssl_certificate_key /path/to/ssl/privatekey.pem;
    
    location /service1/ {
        proxy_pass http://service1:port/;
    }
    
    location /service2/ {
        proxy_pass http://service2:port/;
    }
}

$\ddot{\smile}$