参考文章:我终于会用 Docker 了 (nest+prisma+psotgresql+nginx+https)

# 前言

最近我的个人项目:Picals 的后端(使用 nest 编写)第一版基本已经开发完毕,本地跑起来基本都没有什么问题,因此下一步就是 将应用部署到我自己的服务器上 ,然后将它跑起来,给正式的前端部署做准备。

由于之前从来没有进行过对 nest 项目的后端部署,因此算是一个比较新的经验,踩了非常多的坑,最后决定使用 Docker 来对部署环境进行一键集成。

# 第一步:打包 nest 应用

# 打包部署的一般流程

当把 nest 应用开发完成之后,接下来就是对 nest 应用进行打包,打包完成之后的 dist 才是真正要放到服务器上面运行的。

要注意的是,这个 dist 目录仅仅只是把 nest 中 ts 编写的部分给编译成了 js,并且基于原本的 ts 文件生成了 map 文件用于调试和定位源代码位置。运行 dist 目录所需的 npm 包,还是在同级目录下的 node_modules 目录中的。

所以说,单纯的在本地打包好把 dist 目录送到服务器上进行运行是不行的。正常的流程应该是:

  1. 在本地开发完成后,运行 pnpm build ,观察打包是否产生错误,有错误去排查出错误之后重新打包;
  2. 若没有错误,去服务器上面,打开终端,cd 到在存放后端应用的目录下,进行 git clone <仓库地址> 把整个 nest 应用的代码拉下来;
  3. 在这个目录之下运行 pnpm install 之后再运行 pnpm build ,观察是否有错误;
  4. 最后使用 pm2 跑 dist 目录即可。

# 对于 .env 文件的处理

以上是一般的打包配置流程,但是落了最关键的一步: 配置服务端的 .env 文件。 以下是我总结的在 nest 应用中对 .env 文件的处理,仅供参考。

在开发环境,在 项目根目录(不是 src 目录) 下新建 .env 文件,往其中填写项目需要配置的环境信息。并且复制粘贴一份 .env.example 用于传 github ,在其中仅保留相关的字段信息,一些秘密的值一律用 xxxx 替代。并且将 .env 文件加入到 .gitignore 中。

关于在 nest 应用中如何读取 .env 文件中的值注入到对应的位置,可以参考使用 @nestjs/config 在 NestJs 中实现项目配置。一般 nest 项目对于 .env 的最佳实践基本都是使用这个包来实现的。

这个时候,我们打包后会发现 .env 文件不在 dist 目录下, 自然应用是无法正常的跑起来的,会报 “无法连接数据库” 的错误。

这个时候,我尝试过将 .env 文件放到 src 目录下,打包观察是否成功的被打入 dist,结果发现 并没有被打入 ,而我看的很多教程都是有被成功打入的。

那么,我自己手写一个脚本,在打包的时候将 .env 文件复制到 dist 目录下不就可以了吗?

首先,安装一个可以使用 js 来编写 shell 命令的 npm 包:shelljs。

pnpm add shelljs @types/shelljs -D

在根目录新建一个文件: copy.ts ,代码如下:

import * as shelljs from "shelljs";
shelljs.cp("-R", ".env", "dist");

这段代码的用途是将当前目录中的 .env 文件复制到名为 dist 的目录中。

并且对应的配置 package.json 的 build 命令:

"scripts": {
-   "build": "nest build",
+   "copy": "ts-node copy.ts",
+   "build": "nest build && npm run copy",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "format": "prettier --write \"src/**/*.ts\"",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
},

这样之后,再运行 pnpm build 命令,nest 应用就会正常的将 .env 文件复制到 dist 目录下了。

最后还有一个问题,那就是如果是使用了 @nestjs/config 这个包对 nest 的环境进行配置的话,那么会指定这个 .env 文件的位置:

配置 .env 文件的位置

而 nest 默认的打包是直接将 src 目录下的内容编译打包到 dist 下面的,而这和我们所配置的路径不符,因为如果切换为生产环境使用 dist 来启动服务,这个 .env 文件是找不到的。

那么,我们可以按照原本的目录组织形式,将打包产物同样的输出到 dist/src 下面,将 .env 文件复制到根目录下面,这样就可以保持两者的相对路径一致了(稍微提一下 nest 通关秘籍中的做法,它的打包做法是在生产环境和开发环境分别配置 .env 的路径,这样会导致开发的时候需要手动修改路径再提交的问题)。

我们可以修改一下 tsconfig.json 中的输出配置:

修改打包的输出路径

这样子之后,打包完成的产物就应该如图所示:

打包完成后的 dist 结构

这样子就可以确保环境变量的路径不会出错,并且在生产环境和开发环境都可以正常进行配置读取了。

# 第二步:使用 Docker 对 nest 应用进行部署

这一节是参考神光的 nest 通关秘籍中的部署教程,我在这里将其简单的进行一些我个人的总结。

总的来说,需要在 nest 项目的根目录下配置三个文件:

  1. docker-compose.yml
  2. Dockerfile
  3. .dockerignore

并且需要对 .env 中数据库相关的配置进行同步更改。

# 编写 docker-compose.yml 文件

docker-compose.yml 是一个 YAML 文件,用于定义和管理多容器 Docker 应用程序。通过 Docker Compose,可以在一个文件中定义多个服务(容器),以及它们之间的依赖关系、网络配置、卷挂载等。这样可以方便地启动和管理整个应用栈,而不需要手动启动每一个容器。

# 文件模板

对于一般的 MySQL + Redis 项目(比如我这个 ),可以参考一下我的配置,如果使用其他的数据库实际上也是大同小异,改一下配置项就可以:

version: '3.8'
services:
  nest-app:
    build:
      context: ./
      dockerfile: ./Dockerfile
    depends_on:
      - mysql-container
      - redis-container
    ports:
      - 3005:3005
    networks:
      - common-network
  mysql-container:
    image: mysql
    volumes:
      - /www/apps/docker-data/mysql:/var/lib/mysql
    environment:
      MYSQL_DATABASE: picals
      MYSQL_ROOT_PASSWORD: xxxxxx
    networks:
      - common-network
  redis-container:
    image: redis
    volumes:
      - /www/apps/docker-data/redis:/data
    networks:
      - common-network
networks:
  common-network:
    driver: bridge

# 配置解析

# 版本

version: "3.8"

这是 Docker Compose 文件的版本。 3.8 是 Docker Compose 文件格式的版本,确保 Docker 引擎和 Docker Compose 版本兼容。

# 服务(Services)

Nest.js 应用服务(nest-app)

nest-app:
  build:
    context: ./
    dockerfile: ./Dockerfile
  depends_on:
    - mysql-container
    - redis-container
  ports:
    - 3005:3005
  networks:
    - common-network
  1. build:

    • context: ./ 表示构建镜像的上下文是当前目录。
    • dockerfile: ./Dockerfile 表示使用当前目录下的 Dockerfile 文件构建镜像。
  2. depends_on:

    • mysql-containerredis-container 表示 nest-app 容器依赖这两个容器,确保它们先启动。
  3. ports:

    • 3005:3005 表示将宿主机的 3005 端口映射到容器的 3005 端口。提醒一下,如果自己的 nest 服务跑的不是 3005,需要把前面的端口改成自己实际的端口。
  4. networks:

    • common-network 指定该服务加入到 common-network 网络中。

MySQL 容器(mysql-container)

mysql-container:
  image: mysql
  volumes:
    - /www/apps/docker-data/mysql:/var/lib/mysql
  environment:
    MYSQL_DATABASE: picals
    MYSQL_ROOT_PASSWORD: xxxxxx
  networks:
    - common-network
  1. image:

    • 使用官方的 mysql 镜像。
  2. volumes:

    • 将宿主机上的 /www/apps/docker-data/mysql 目录挂载到容器内的 /var/lib/mysql 目录,以持久化 MySQL 数据。
  3. environment:

    • MYSQL_DATABASE : 初始化数据库名为 picals
    • MYSQL_ROOT_PASSWORD : 设置 MySQL root 用户的密码为 xxxxxx
  4. networks:

    • common-network 指定该服务加入到 common-network 网络中。

Redis 容器(redis-container)

redis-container:
  image: redis
  volumes:
    - /www/apps/docker-data/redis:/data
  networks:
    - common-network
  1. image:

    • 使用官方的 redis 镜像。
  2. volumes:

    • 将宿主机上的 /www/apps/docker-data/redis 目录挂载到容器内的 /data 目录,以持久化 Redis 数据。
  3. networks:

    • common-network 指定该服务加入到 common-network 网络中。

# 网络(Networks)

networks:
  common-network:
    driver: bridge
  1. common-network:
    • 定义了一个名为 common-network 的网络,并指定使用 bridge 驱动。这使得所有加入该网络的容器可以相互通信。

# 总结

  • 当前这个 docker-compose.yml 文件定义了三个服务: nest-appmysql-containerredis-container
  • 这些服务通过 common-network 网络互相连接。
  • nest-app 服务依赖 mysql-containerredis-container ,并将其暴露在端口 3005 上。
  • mysql-containerredis-container 分别挂载了宿主机上的目录以持久化数据。

# 更改 .env 的配置项

在上面的 docker-compose-yml 文件中,我们已经指定了 mysql 和 redis 数据库的容器为 mysql-container 和 redis-container,因此我们需要把生产环境中的 .env 文件的相关数据库的 HOST 从 localhost 更改为这两个:

# 开发环境配置
NODE_ENV=development
# Redis相关
REDIS_HOST=redis-container
REDIS_PORT=6379
REDIS_DB=0
# nodemailer相关
NODEMAILER_HOST=xxxx
NODEMAILER_PORT=xxxx
NODEMAILER_NAME=xxxx
NODEMAILER_AUTH_USER=xxxx
NODEMAILER_AUTH_PASS=xxxx
# mysql相关
MYSQL_HOST=mysql-container
MYSQL_PORT=xxxx
MYSQL_USER=xxxx
MYSQL_PASS=xxxx
MYSQL_DB=xxxx
# nest服务配置
NEST_PORT=0721
# jwt配置
JWT_SECRET=xxxx
JWT_ACCESS_TOKEN_EXPIRES_TIME=30m
JWT_REFRESH_TOKEN_EXPIRES_TIME=7d
# 其他测试环境配置
CAPTCHA_SECRET=111111
# 跨域配置
CORS_ORIGIN=xxxx
# 腾讯云 COS 配置
COS_SECRETID=xxxx
COS_SECRETKEY=xxxx
COS_BUCKET=xxxx
COS_DOMAIN=xxxx

# 编写 Dockerfile

Dockerfile 是一个文本文件,包含一系列指令,用于构建一个 Docker 镜像。每一条指令在执行后,都会在镜像上添加一层,最终形成一个可以运行的 Docker 镜像。

# 文件模板

同样地,我也给出我的 Dockerfile 方案,仅供参考:

FROM node:18.0-alpine3.14 as build-stage
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:18.0-alpine3.14 as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install --production
EXPOSE 3005
CMD ["node", "/app/main.js"]

# 配置解析

# 第一阶段:构建阶段(build-stage)

FROM node:18.0-alpine3.14 as build-stage
  • 使用 node:18.0-alpine3.14 作为基础镜像。 alpine 是一个轻量级的 Linux 发行版,有助于减小镜像的体积。
  • as build-stage 将该阶段命名为 build-stage ,以便在后续阶段中引用。
WORKDIR /app
  • 设置工作目录为 /app
COPY package.json .
  • 将本地的 package.json 文件复制到容器的工作目录中。
RUN npm config set registry https://registry.npmmirror.com/
  • 设置 npm 的镜像源为 https://registry.npmmirror.com/ ,加速依赖包的下载。
RUN npm install
  • 安装项目依赖。
COPY . .
  • 将当前目录下的所有文件复制到容器的工作目录中。
RUN npm run build
  • 运行构建命令,生成生产环境的代码。构建后的文件通常放在 dist 目录中。

# 第二阶段:生产阶段(production-stage)

FROM node:18.0-alpine3.14 as production-stage
  • 使用 node:18.0-alpine3.14 作为基础镜像,并将该阶段命名为 production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
  • build-stage 阶段复制构建后的 dist 目录和 package.json 文件到当前阶段的 /app 目录中。
WORKDIR /app
  • 设置工作目录为 /app
RUN npm config set registry https://registry.npmmirror.com/
  • 再次设置 npm 的镜像源为 https://registry.npmmirror.com/
RUN npm install --production
  • 安装生产环境所需的依赖, --production 参数确保只安装 dependencies 而不是 devDependencies
EXPOSE 3005
  • 暴露容器的 3005 端口,使得应用可以通过该端口访问。
CMD ["node", "/app/main.js"]
  • 设置容器启动时运行的命令,即使用 node 运行 /app/main.js 文件。

# 总结

Dockerfile 使用了多阶段构建(Multi-stage Build)来优化镜像大小和构建效率。具体内容如下:

  1. 构建阶段(build-stage)

    • 设置工作目录并安装项目依赖。
    • 复制项目文件并运行构建命令,生成生产环境代码。
  2. 生产阶段(production-stage)

    • 从构建阶段复制构建后的代码和 package.json 文件。
    • 安装生产环境依赖。
    • 暴露 3005 端口并设置容器启动命令。

# 编写 .dockerignore

和 .gitignore 一样,添加要在构建的时候被忽略掉的文件:

node_modules/
.vscode/
.git/

# 进行服务器端的 Docker 部署运行

经过上述步骤,我们配置好了 Docker 容器运行的必备条件。当然,在服务器端也得同时配置好 Docker 服务以及 docker-compose,不然 Docker 部署就无从谈起。

并且需要放行 6379、3306、3005 等的端口以供数据库服务以及 docker 服务正常运行。

我们直接 cd 到 nest 项目的根目录下,直接输入 docker-compose up 来对整个 Docker 进行构建即可。

之后就会看到 Docker 在控制台中输出一系列的构建信息,非常的美观优雅。这里没截图,有点可惜,感兴趣的可以自己去试试,Docker 的终端是真的很美观

等待构建完毕,会打印出 nest 服务启动成功的一系列输出,我们此时就可以正常通过 ip 地址 + 端口号的形式来访问 nest 服务了。如果你的服务器有使用宝塔面板,那么可以在宝塔面板的 Docker 标签页中看到对应的容器以及镜像信息:

容器信息

拉取的镜像信息

# 第三步:使用 nginx 反向代理后端服务,启用 https

# 前置工作:域名 + SSL 证书

关于后端 https 的启用,需要使用域名以及相关的 ssl 证书。域名的话,可以去腾讯云、阿里云或者一些莫名其妙的国外供应商去找一些莫名其妙的域名,这里就不多赘述了。SSL 证书的话,也是各凭本事进行准备吧。

顺带一提,我本人是直接腾讯云买了个便宜的 .cn 域名,专门给我服务器用后端服务。并且证书也是免费申请的。

# 修改 docker-compose.yml 文件

由于我们是使用 Docker 来部署 Nest.js,那么 Nginx 的运行也理应配套放在 Docker 里面进行,不过需要另外起一个镜像和容器。因此需要在 docker-compose.yml 文件中进行 Nginx 镜像的一系列配置。

以下给出我本人的 docker-compose.yml 文件的最终版本:

version: "3.8"
services:
  nest-app:
    container_name: nest-app
    build:
      context: ./
      dockerfile: ./Dockerfile
    depends_on:
      - mysql-container
      - redis-container
    ports:
      - 3005:3005
    networks:
      - common-network
  mysql-container:
    image: mysql
    volumes:
      - /www/apps/docker-data/mysql:/var/lib/mysql
    environment:
      MYSQL_DATABASE: picals
      MYSQL_ROOT_PASSWORD: xxxxxx
    networks:
      - common-network
  redis-container:
    image: redis
    volumes:
      - /www/apps/docker-data/redis:/data
    networks:
      - common-network
  nginx:
    image: nginx:stable-alpine # 指定服务镜像
    container_name: nginx # 容器名称
    restart: always # 重启方式
    ports: # 映射端口
      - "8000:8000"
      - "721:721"
    volumes: # 挂载数据卷
      - /www/server/nginx/conf.d:/etc/nginx/conf.d
      - /www/server/nginx/logs:/var/log/nginx
      - /www/server/nginx/cert:/etc/nginx/cert
    depends_on: # 启动顺序
      - nest-app
networks:
  common-network:
    driver: bridge

好的,重点来了。在之前文件的基础之上,我们指定了 nest 应用的容器名称为 nest-app,并且在 redis-container 下面新增了一个 nginx 服务。

我们具体看一下它配了什么:

  • image: 使用 nginx:stable-alpine 镜像,这是一种体积小、性能稳定的 Nginx 镜像版本。实际运行时,只有不到 50mb 的大小,非常轻量。

  • container_name: 定义容器名称为 nginx ,方便在 Docker 中管理和识别。

  • restart: always: 容器的重启策略设置为 always ,确保无论容器是因为什么原因停止,都会自动重启,保证服务的高可用性。

  • ports:

    • '8000:8000' : 将宿主机的 8000 端口映射到容器的 8000 端口。

    • '721:721' : 将宿主机的 721 端口映射到容器的 721 端口。

  • volumes:

    • /www/server/nginx/conf.d:/etc/nginx/conf.d : 挂载宿主机的 Nginx 配置文件目录到容器内,这样可以方便地修改 Nginx 配置文件而不需要进入容器。
    • /www/server/nginx/logs:/var/log/nginx : 挂载宿主机的 Nginx 日志目录到容器内,这样可以方便地查看 Nginx 的运行日志。
    • /www/server/nginx/cert:/etc/nginx/cert : 挂载宿主机的证书目录到容器内,这样可以管理和使用 SSL/TLS 证书。
  • depends_on: 确保 nginx 服务在 nest-app 服务启动之后再启动,以确保应用服务已经运行。

这里的端口需要说明一下。在 Docker 的配置文件中, aaa:bbb 的格式是指 将宿主机的 aaa 端口映射到容器的 bbb 端口 。这也就是说,如果你访问宿主机(就是你自己这台服务器本身)的 aaa 端口,这个端口是被 docker 服务所占据的,docker 会自动的把这个端口的请求全部转发到容器内部的 bbb 端口来进行具体的响应。

我这里的 8000:8000721:721 端口分别是原本 nginx 服务的 80 和 443 端口。这里是为了和我自己服务器原本的 nginx 服务做区分(因为原本的 nginx 端口已经被我用来部署个人博客了),所以在此处选用了两个不常见的端口来进行部署。

然后是这里的 volumes,挂载卷。这里的路径地址 全都是我自己的服务器上存放 nginx 服务的真实地址,后者用 : 隔开的部分就是要被映射到容器内部的目标路径。

# docker nginx 容器挂载文件配置说明

这里总共需要从服务器上挂载三个部分:配置文件、日志、证书。

# 配置文件(default.conf)

注意,此处的是 conf.d 目录,而不是 conf 目录;采用的配置文件也是 default.conf 而非 nginx.confnginx:stable-alpine 这个镜像内部也采用的是这个目录作为服务启动的配置文件。我才不会说我之前就是因为没搞清楚这点而踩了大坑呢如果你的 nginx 服务的路径下面没有这个目录,那么 你就自己新建个文件夹,取名为 conf.d 即可

cd 到 conf.d 目录后,新建文件 default.conf ,往里面写你的服务配置信息。这里我给一下我的模板,需要借鉴的可以采用:

server {
	# 一般为 443,具体根据你的需求进行更改
    listen       721 ssl http2;
    server_name  example.com;
    root html;
    index index.html index.htm;
    ssl_certificate      /etc/nginx/cert/example.com_bundle.pem;
    ssl_certificate_key  /etc/nginx/cert/example.com.key;
    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers  ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers  on;
    location / {
    	# 这里的 nest-app 是我的 nest 容器的名称,3005 是我先前指定的运行端口
        proxy_pass http://nest-app:3005;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 15M;
        client_body_buffer_size 128k;
    }
}
server {
	# 一般为 80,具体根据你的需求进行更改
    listen       8000;
    server_name  example.com;
    return 301 https://$host$request_uri;
    location / {
        root   html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

这里需要把 example.com 换成具体的你自己的详细域名。

顺带一提, default.conf 和一般的 nginx 标准配置文件 nginx.conf 两者的作用是不一样的。借一下 GPT 的回答:

/etc/nginx/nginx.conf 是 Nginx 的主配置文件。它通常包含全局配置和一些基础设置,如工作进程数、错误日志路径、以及包含其他配置文件的指令。这个文件定义了 Nginx 服务器的总体行为和全局配置。

示例配置结构如下:

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
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;
    include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/default.conf 文件通常包含具体的 虚拟主机配置 。默认情况下用于定义 Nginx 默认的站点配置。如果你在没有指定服务器块配置的情况下启动 Nginx,它会加载并使用这个默认的配置文件。

示例配置结构如下:

server {
 listen       80;
server_name  localhost;
 location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
}
   
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
}
   }

这两个文件之间主要有三种不同的区别:

  1. 用途不同
  • nginx.conf:定义全局设置和包含其他配置文件的指令。控制 Nginx 的核心行为。
  • default.conf:定义默认虚拟主机的设置。通常用于具体站点的配置。
  1. 作用范围不同

    • nginx.conf:作用于整个 Nginx 服务器的全局配置。
    • default.conf:作用于单个或默认站点的配置。
  2. 结构和内容不同

  • nginx.conf:包含全局指令,如用户权限、工作进程数、日志文件位置等。
  • default.conf:包含特定虚拟主机的指令,如监听端口、服务器名称、根目录、错误页面等。

通常, nginx.conf 文件会包含一条指令 include /etc/nginx/conf.d/*.conf; ,将 conf.d 目录下的所有配置文件包含进来,这样可以将站点的配置文件分离出来,使管理多个站点配置变得更加方便和模块化。

简单来记就是, default.conf 文件只有一个 server 块,其他都没有; nginx.conf 的 server 块是放在 http 块下方的,并且有很多很多额外的配置。

# 日志文件

就是记录 nginx 运行的状况的目录及文件。将其复制过去,能够记录这个 nginx 容器在运行时的打印输出,方便排查错误。

# 证书

其实在上方的 default.conf 中就可见一斑,所需要的是 .pem.key 两种后缀格式的文件。这个证书文件只要你在你的服务商申请了 SSL 证书并成功下发后,就会提供一个专门的下载链接。选择 nginx 的版本进行下载后解压,可以找到对应的两个后缀的文件。将它们分别上传到你的服务器的对应目录下就可以了, docker-compose.yml 文件会自动地将证书目录复制到容器中的对应目录处。并且 default.conf 中证书的位置写的是容器中的证书位置,不要写成服务器端的证书位置了。

以上就是 nginx 容器的具体配置情况。如果按照我的步骤进行操作,一般来说是没有什么问题的(因为这些坑我都自己踩了)。对于 nginx 的端口号,如果你的服务器的 443 和 80 没有被其他的端口占用,或者说你想从其他另外的端口映射到容器内的 443 和 80,你可以按照你的需求进行更改。

# 第四步:Docker 部署最佳实践(CI/CD)

好的,经历了上面三个步骤,你应该或许可以成功的把一个 nest 服务使用 docker 跑起来了。

但是问题是,如果代码发生了变更,要怎么去处理容器和镜像呢?

# 编写 Docker 重新构建脚本

一般来说,如果是用 github 来管理项目,那么当主分支被合并后,需要在服务端处进行拉取,并进行 整个镜像容器的重新构建,之后再重新的启动最新的容器即可。

因此,我们可以写一个 .sh 文件,专门用来处理这个部署脚本。

在 nest 项目根目录下新建 scripts 目录,在其下新建 setup.sh 文件后,向其中编写以下内容:

#!/usr/bin/env bash
#image_version=`date +%Y%m%d%H%M`;
# 关闭容器
docker-compose stop || true;
# 删除容器
docker-compose down || true;
# 构建镜像
docker-compose build;
# 启动并后台运行
docker-compose up -d;
# 查看日志
docker logs nest-app;
# 对空间进行自动清理
docker system prune -a -f

这个脚本总共执行了下面六步操作:

  1. 停止当前运行的容器。
  2. 删除所有的容器。
  3. 构建镜像。
  4. 在后台启动服务。
  5. 查看 nest 应用服务的日志。
  6. 清理未使用的 Docker 对象。

编写完毕后,我们只需要在每次主分支合并后,在服务器端对最新的代码进行拉取,然后 直接运行这个脚本命令就可以了。整个应用就会在这个脚本的控制下,重新的一步步执行,最终成功的将服务跑起来。

顺带一提,如果遇到这个脚本受权限制约无法执行的情况,需要手动的去更改一下这个文件的权限:可被执行

至于是哪个用户的权限,需要看你平时是用什么身份去运行终端的。我平时是 root 超级用户。

而且这个修改权限的操作似乎也是算入对于文件的修改操作的,也就是说你改完权限后需要将其反向提交到 github,由开发者进行拉取。

# 一些 Docker 部署时的常用命令清单

列举一些 Docker 在部署时的一些命令,便于后续查阅:

# Docker

# 构建和管理镜像

  • docker build -t <tag_name> <path> :根据 Dockerfile 构建镜像,并使用 <tag_name> 给镜像打标签。
  • docker images :列出所有本地存储的 Docker 镜像。
  • docker rmi <image_id> :删除指定的镜像。

# 管理容器

  • docker run -d --name <container_name> <image_name> :使用指定镜像在后台启动一个容器,并命名为 <container_name>
  • docker ps :列出当前正在运行的所有容器。
  • docker ps -a :列出所有容器,包括停止的容器。
  • docker stop <container_id> :停止指定的容器。
  • docker start <container_id> :启动已停止的容器。
  • docker restart <container_id> :重启指定的容器。
  • docker rm <container_id> :删除指定的容器。

# 容器日志和监控

  • docker logs <container_id> :查看指定容器的日志。
  • docker stats :显示所有容器的实时资源使用情况。

# 网络和卷管理

  • docker network ls :列出所有 Docker 网络。
  • docker network create <network_name> :创建一个新的 Docker 网络。
  • docker volume ls :列出所有 Docker 卷。
  • docker volume create <volume_name> :创建一个新的 Docker 卷。

# 清理系统

  • docker system prune -a -f :删除所有未使用的容器、网络、镜像(包括悬空镜像)和构建缓存。
  • docker volume prune -f :删除所有未使用的卷。

# Docker Compose

# 启动和管理服务

  • docker-compose up :启动定义在 docker-compose.yml 文件中的所有服务。
  • docker-compose up -d :在后台启动所有服务。
  • docker-compose down :停止并删除所有服务和网络。
  • docker-compose stop :停止所有服务。
  • docker-compose start :启动已停止的服务。
  • docker-compose restart :重启所有服务。

# 构建和管理服务

  • docker-compose build :根据 docker-compose.yml 文件构建或重新构建服务。
  • docker-compose build --no-cache :不使用缓存构建服务。
  • docker-compose logs :查看所有服务的日志。
  • docker-compose logs <service_name> :查看指定服务的日志。
  • docker-compose ps :列出所有服务的状态。
  • docker-compose exec <service_name> <command> :在运行中的服务容器内执行命令。
  • docker-compose run <service_name> <command> :在新容器中运行命令。

# github workflows CI/CD

如果你成功编写了 scripts/setup.sh ,那么你现在的开发部署流程是:

  1. 手动上传代码分支;
  2. 提交 pr 请求;
  3. 手动操作合并 master 分支;
  4. 远程连接到你服务器的终端;
  5. cd 到你目前项目所在目录;
  6. 进行 git 拉取请求;
  7. 手动执行 setup.sh 脚本命令。

可以看到,4~7 步都是服务器端所要执行的机械操作。我们可以进一步的将部署流程简化,也就是说,我们可以仅仅执行 1~3 步,由 github 自动检测 master 分支的提交,然后自动在服务器上执行我们编写好的脚本。

那么怎么来实现这一点呢?自然是借助 github workflow 来实现。

配置工作流的步骤其实很简单。首先在你的 nest 项目根目录下新建目录及文件 .github/workflows/deploy.yml ,然后往其中编写如下内容:

name: Deploy-nest
on:
  push:
    branches:
      - master
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: "20"
      - name: Run setup script
        run: bash ./scripts/setup.sh
      - name: Deploy to server
        env:
          SSH_PRIVATE_KEY: $<!--swig0-->
          SSH_HOST: $
          SSH_USER: $
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan $SSH_HOST >> ~/.ssh/known_hosts
          ssh $SSH_USER@$SSH_HOST "cd /path/to/your/project && git pull origin main && npm install && bash ./scripts/setup.sh"

这个工作流会在 main 分支被合并时触发,自动在服务器上拉取最新代码,执行 npm install ,然后运行 scripts/setup.sh 文件。

此处的 runs-on 是 github workflow 预设的一个值,主要是用来指定用户运行的系统环境。你可以改成你自己服务器上的系统版本。

这里是每个步骤的详细说明:

  1. Checkout code: 使用 actions/checkout@v2 来检出仓库代码。
  2. Set up Node.js: 设置 Node.js 环境,指定版本为 20。
  3. Run setup script: 运行 scripts/setup.sh 脚本进行 Docker 部署。
  4. Deploy to server: 使用 SSH 部署到服务器。你需要在 GitHub 仓库的 Secrets 中设置 SSH_PRIVATE_KEY , SSH_HOST , 和 SSH_USER 这三个秘密变量。
  • SSH_PRIVATE_KEY : 你的 SSH 私钥,用于连接服务器。
  • SSH_HOST : 服务器的地址。
  • SSH_USER : 用于登录服务器的用户名。

将这些 Secret 添加到 GitHub 仓库的设置中:

  1. 打开你的 GitHub 仓库。
  2. 点击 Settings
  3. 在左侧菜单中找到 Secrets ,然后点击 Actions
  4. 点击 New repository secret 按钮,分别添加 SSH_PRIVATE_KEY , SSH_HOST , 和 SSH_USER

# 后记

至此,算是把之前没有填好的坑基本上都填上了。希望能给大家带来一点点使用 Docker 部署 Nest.js 应用一点点参考的价值~~!