本文包含 Nginx 部署 Web 应用、Docker 部署 Web 应用、Jenkins 、CI/CD 部署 Web 应用 等内容。快速使用也可以。深入了解
需要了解 NodeJs 知识:NodeJs 环境配置
需要了解 Docker 知识:Docker 相关知识
需要了解 Jenkins 知识:Jenkins 相关知识
注意:此处部署是在 Ubuntu 操作系统中进行。如果安装慢,可以修改一下镜像源。
目前主流框架的渲染方式有: SPA 客户端渲染;SSR 服务端渲染

# 知识梳理

  • nginx 中 location 后面的值最后增加 / 和 proxy_pass 值后面携带 / 会在代理时移除代理标识,否则反之。
  • 关于 nginx 中 try_files 和 重定向 return 与 rewrite 的关系问题,重定向尽量在 location 中写。
指令是否终止处理是否改变 URI是否影响浏览器地址执行时机URL 编码(# -> %23)说明
return✅ 是(立即)❌ 否(但可跳转)✅ 是(如果是 3xx)❌否重写 URL,立即返回, 不会被 URL 编码 # -> %23
rewrite ... last❌ 否✅ 是(内部)❌ 否✅是重写 URL,然后重新进入 try_files
rewrite ... break❌ 否✅ 是(内部)❌ 否✅是重写 URI,但不再执行后续 rewrite
rewrite ... permanent✅ 是✅ 是✅ 是✅是返回 301,终止处理,不进 try_files
try_files❌ 否(但处理完请求)✅ 是(内部跳转)❌ 否❌否它是内部处理,地址不变

# Linux 系统

# Nginx 部署

Nginx 部署 Web 项目时,如多个项目使用 443 端口,只需要配置多个 server 并且使用不同的 server_name 即可。

# SPA 应用部署

# 安装 Nginx

sudo apt update
sudo apt install nginx

# 启动 Nginx

sudo systemctl status nginx
or
service nginx start

# 测试 Nginx

# 浏览器查看

浏览器访问 服务器 IP,就可以看到 Nginx 的页面。注意要在防火墙开通 80 端口。

# 命令查看
sudo systemctl status nginx
or
service nignx status

# 配置 Nginx

此时,防火墙要开通将要使用的端口。

default.conf
server {
        # listen 80; # 端口自定义
        # listen [::]:80;
        listen 443 ssl http2; # 有证书时使用
        listen [::]:443;
        gzip  on;
        gzip_types text/plain application/x-javascript application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
        
        server_name [xxxx.com 或者 _];     # 域名
        
        root /项目文件夹路径;  # 项目文件夹
        index index.html;       # 项目 index 页
        # SSL 证书配置 有证书就配置,无证书就不配置
        ssl_certificate ./cert-file-name.pem;
        #需要将 cert-file-name.pem 替换成已上传的证书文件的名称。
        ssl_certificate_key ./cert-file-privkey.pem;
        #需要将 cert-file-privkey.key 替换成已上传的证书私钥文件的名称。
        ssl_session_timeout 5m;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
        #表示使用的加密套件的类型。
        ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
        #表示使用的 TLS 协议的类型。
        ssl_prefer_server_ciphers on;
        # 后端部分 反向代理
        location /xxx/ {
           proxy_next_upstream http_500 http_502 http_503 http_504 error timeout invalid_header;
           proxy_set_header Host $host;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           # 请求后端的代理地址服务器的 IP 
           proxy_pass http://IP:PORT/; 
           # Docker 服务端在宿主机中时 ip 是 ifconfig 命令输出中的 docker0-inet/eth0-inet 的数据,通过宿主机 / 网桥进行通信。 
           # proxy_pass http://IP:PORT/;
           # Docker 服务端和前端容器在同一个网络,则使用此格式。容器之间的通信。
           # proxy_pass CONTAINER NAME:PORT/; 
           expires 0;
        }
        #反向代理可以有多个
        # 前端部分 内部重定向
        location / {
           add_header Cache-Control "no-store, no-cache";  #用于控制服务端更新,用户浏览器不同步问题
           add_header Pragma no-cache;
           try_files $uri $uri/ /index.html;
        }
}

# 检测配置

l
nginx -t

# 重启 Nginx

# 方式一
sudo systemctl restart nginx
or
sudo systemctl reload nginx
or
service nginx restart
# 方式二
sudo systemctl stop nginx
sudo systemctl start nginx
or
service nginx stop
service nginx start

# SSR 应用部署

暂时没有尝试,使用后再补充。

# Docker 部署

建议使用一个 Nginx 容器部署多个项目,多容器会出现端口冲突情况如 443 端口。

# SPA 应用部署

# 配置文件部署方式(推荐)

要注意路径关系,我通常会把配置文件和项目文件放在同一个文件夹下。

# 构建 Dockerfile

构建基础镜像

Dockerfile
FROM nginx
# 构建 compose.yml
compose.yml
version: '3'
services:
    frontend: # 服务名称,用于在 Docker Compose 中标识该服务。(可自定义)
        build: . # 指定 Dockerfile 的路径
        container_name: [CONTAINER NAME] # 自定义容器名字
        image: [IMAGE NAME:TAG] # 自定义镜像名称:标签
        ports: # 注意:需要保持 Nginx 配置文件中的端口修改为与容器映射的端口一致。
            - "443:443" # "宿主机端口号:容器端口号" 有证书就用
            - "80:80" # "宿主机端口号:容器端口号"
        volumes:
            - 宿主机项目资源文件夹路径:容器项目资源文件夹路径(可自定义)  # 容器内和宿主机内挂载的项目文件资源会同步变化
            - 宿主机Nginx资源文件路径:容器Nginx资源文件路径(可自定义) # 容器内和宿主机内挂载的项目文件资源会同步变化
            - 宿主机SSL资源文件夹路径:容器SSL资源文件夹路径(可自定义) # 容器内和宿主机内挂载的项目文件资源会同步变化 有证书就用
        restart: unless-stopped # 容器自动重启

注意:挂载证书的时候,证书文件一定不可有符号链接。有符号链接会导致容器内无法访问证书文件。可以通过命令 ls -l 查看文件是否存在符号链接。 符号链接 Eg: cert.pem -> ../../archive/www.expamle.com/cert.pem
解决方案: 将证书文件复制到项目文件夹中,然后挂载到容器中。

# 构建 Nginx 配置文件

配置文件如上所示。

# 构建 自定义 Image 和 容器
# 构建并后台启动
docker compose up -d
# 构建并启动  
docker compose up

命令应在项目文件夹中执行

此时,浏览器已经可以访问了。

# 逐步部署方式

# 准备工作
# 上传项目文件到服务器

主机中创建项目文件夹,将打包后的应用上传到项目文件夹中或者上传源码再进行应用构建。两种方式均可。由于,本文主要讲解部署,就不过多描述此步骤了。

注意:这里要包含 Nginx 配置文件,如果未包含,则需要手动创建一个 Nginx 配置文件再上传到项目文件夹。

# Nginx 配置文件

这个文件可以创建,也可以直接使用 Nginx 中已经配置好的文件。但是,如果容器映射了 nginx 配置,那么系统中的 nginx 和 docker Nginx 镜像的的配置就不能有重复的端口,否则会出现冲突。

配置文件如上所示。

# 安装 Nginx 镜像
docker pull nginx
# 方式一

创建容器并运行。

docker run -itd -p [CONTAINER PORT:宿主机 PORT] --name [CONTAINER NAME] -v "宿主机项目文件路径:容器中挂载项目文件路径" -v "宿主机Nginx配置文件路径:容器中挂载Nginx配置文件路径" [CONTAINER NAME/ID] /bin/bash

命令参数说明:

  • -i: 交互式操作。
  • -t: 终端。
  • -d: 设置容器在在后台一直运行。
  • -p: 端口进行映射,将本地 8080 端口映射到容器内部的 80 端口。
  • -v: 将本地的文件目录映射到容器内的,映射文件会更具主机项目资源变化发生变化的
  • --name: 设置容器名字.
  • nginx: nginx 镜像。
  • /bin/bash:放在镜像名后的是命令,这里我们希望有个交互式 Shell,因此用的是 /bin/bash。
# 方式二

逐步创建容器;进入容器;启动程序。

# 创建容器
docker create [CONTAINER NAME/ID]
# 此时不可以使用
docker start [CONTAINER NAME/ID]
docker create -itd -p 容器端口:项目端口 --name [CONTAINER NAME/ID] -v "宿主机项目文件路径:容器中挂载项目文件路径" -v "宿主机Nginx配置文件路径:容器中挂载Nginx配置文件路径" [CONTAINER NAME/ID] /bin/bash
# 此时可以使用
docker start [CONTAINER NAME/ID]
# 进入容器
docker exec -it [CONTAINER NAME/ID] /bin/bash
# 启动 Nginx
# 启动 Nginx
service nginx start
# 停止 Nginx
service nginx stop
# 重启 Nginx
service nginx restart

此时,浏览器已经可以访问了。

# SSR 应用部署

暂时没有尝试,使用后再补充。

# 宿主机、容器之间网络关系

前端容器 nginx 配置文件中代理地址问题。

  1. 前端容器 (A) - 宿主机服务端:此时,可以使用的代理地址有, ifconfig 查询网络信息。
    • 其中,docker0 的 inet 的值 + 端口号。 数据流动:浏览器 -> 宿主机 -> 容器A -> 宿主机 -> 宿主机服务端 -> 宿主机 -> 容器A -> 浏览器
    • 或者,eth0 的 inet 的值 + 端口号。数据流动。 浏览器 -> 宿主机 -> 容器A -> 宿主机 -> 宿主机服务端 -> 宿主机 -> 容器A -> 浏览器
    • 注意:此时宿主机的防火墙要开启对应端口。
    • 也可以将容器的网络模式 network_mode: host , 就可以通过 localhost+端口号 进行访问。 数据流动:浏览器 -> 宿主机 -> 容器A -> 宿主机 -> 宿主机服务端 -> 宿主机 -> 容器A -> 浏览器
  2. 前端容器 (A) - 服务端容器 (B):此时,有 3 种处理方式:
    • 情况一:默认所有容器都在 docker0 网桥下,所以,代理地址可以使用 docker0的inet的值+端口号 或者 服务端容器名称+端口号 进行访问。(理论上是可行的)。 数据流动:浏览器 -> 宿主机 -> 容器A -> 容器B -> 容器A -> 浏览器
    • 情况二:两个容器不在同一个网络中,需要服务端容器映射端口,代理地址只能使用 服务器IP+端口号数据流动:浏览器 -> 宿主机 -> 容器A -> 宿主机 -> 容器B -> 宿主机 -> 容器A -> 浏览器
    • 情况三:自定义一个网络,将容器添加到同一个网络下,代理地址使用 服务端容器名称/容器IP+端口号 ,此时,如果服务不对外,可以不开发对应端口的防火墙。 数据流动:浏览器 -> 宿主机 -> 容器A -> 容器B -> 容器A -> 浏览器

# 部署 WEB 相关 Docker 命令

以下命令为此过程常用命令,无先后顺序。前提条件是已经安装了 Docker。

# 查找 Docker Nginx 镜像
docker search nginx
# 安装 Docker Nginx 镜像
docker pull nginx
# 查看镜像列表
docker images
# 删除镜像
docker rmi 镜像名/镜像ID
# 查看正在运行的容器
docker ps
# 查看所有容器
docker ps -a
# 删除容器
docker rm [CONTAINER NAME/ID]
# 启动停止的容器
docker start [CONTAINER NAME/ID]
# 停止容器
docker stop [CONTAINER NAME/ID]
# 重启容器
docker restart [CONTAINER NAME/ID]
# 进入后台运行的容器
docker exec -it [CONTAINER NAME/ID] /bin/bash
# 退出容器
exit

# Jenkins 部署 Web 应用

# SPA 应用自动化部署

# 创建自由风格 (Freestyle project)

本方案只描述如何从 Gitee 仓库 pull 代码;更新项目依赖;打包项目;至于项目部署请看 流水线 (Pipeline) 部分

  1. 新建项目;
  2. 填入项目名称;
  3. 选择自由风格 (Freestyle project);
  4. General 部分
    • 填入项目描述;
    • 选择 GitHub 项目 > 填入 GitHub 项目地址;
    • 选择参数化构建过程(以下是我是用的参数)
      • 布尔值参数 (install、build、deploy)
      • 选项参数 (暂时没怎么用)
      • 字符参数 (暂时没怎么用)
  5. 源码管理部分
    • 选择 Git
    • 添加仓库地址 (我使用的是 gitee 的仓库地址、GitHub 的仓库地址我没有使用成功)
    • 添加用户凭证 (账号密码 或 密钥方式)
    • Git 地址报错问题
      • 系统管理 -> Git installations -> 填入 git 的路径 (可通过命令 whereis git 查找 git 路径, 我这里的路径是 /usr/bin/git)
  6. Build Steps 部分
    • 执行 shell
    shell脚本内容
    # !/bin/bash
    if ${install}; then
        echo "执行 npm install";
        npm install;
    else
        echo "不执行 npm install";
    fi
    if ${build}; then
        echo "执行 npm run build";
        npm run build;
    else
        echo "不执行 npm run build";
    fi
  7. 应用
  8. 构建

# 创建流水线 (Pipeline)

  1. 新建项目;
  2. 填入项目名称;
  3. 选择流水线 (Pipeline);
  4. General 部分
    • 填写项目描述
    • GitHub 项目填写仓库 SSH 地址
    • 参数化构建过程(建议与 pipeline 中 parameters 的变量名一致,方可同步数据变化,方便脚本中使用)
      • 布尔值参数 PULL 更新代码
      • 布尔值参数 INSTALL 更新依赖
      • 布尔值参数 BULID 项目打包
      • 布尔值参数 DEPLOY 项目部署
      • 选项参数 BRANCH master 选择代码分支
      • 选项参数 ENVIRONMENT production test 选择部署环境
      • 选项参数 DEPLOYTARGET local remote 选择部署目标
  5. 流水线部分
    • 定义 Pipeline script
Pipeline脚本
pipeline {
    agent any
    parameters {
        # 定义参数
        choice(name: 'DEPLOYTARGET', choices: ['local', 'remote'], description: '选择部署目标') # 选择参数    
        choice(name: 'BRANCH', choices: ['master'], description: '选择代码分支') # 选择参数
        choice(name: 'ENVIRONMENT', choices: ['production', 'test'], description: '选择部署环境') # 选择参数    
        booleanParam(name: 'PULL', defaultValue: true, description: '更新代码') # 布尔参数
        booleanParam(name: 'INSTALL', defaultValue: true, description: '更新依赖')
        booleanParam(name: 'BULID', defaultValue: true, description: '项目打包')
        booleanParam(name: 'DEPLOY', defaultValue: true, description: '项目部署')
        # password (name: 'SECRET_KEY', defaultValue: '', description: 'Enter a secret key') # 密码参数
        # run (name: 'PREVIOUS_BUILD', description: 'Select a previous build', project: 'my-job') # 运行参数
        # file (name: 'UPLOAD_FILE', description: 'Select a file to upload') # 文件参数
        # text (name: 'CONFIG_FILE', defaultValue: '', description: 'Enter configuration as text') # 文本参数
        # string (name: 'ENVIRONMENT', defaultValue: 'dev', description: 'Specify the environment') # 字符串参数
        
    }
    stages {
        stage('PULL') {
            steps {
                script {
                    if (params.PULL){
                        # git credentialsId: 'Git', url: 'https://gitee.com/xxx.git' # 默认 master 分支
                        git branch: params.BRANCH, credentialsId: 'Git', url: 'https://gitee.com/xxx.git' # 指定分支
                    }else{
                        echo "This is PULL value ${params.PULL}"
                    }
                }
            }
        }
        stage('INSTALL') {
            steps {
                script {
                    if (params.INSTALL){
                        sh "npm install"
                    }else{
                        echo "This is INSTALL value ${params.INSTALL}"
                    }
                }
            }
        }
        stage('BULID') {
            steps {
                script {
                    if (params.BULID){
                        switch(params.ENVIRONMENT){
                            case "production":
                                 sh "npm run build" # 自己手动添加 mode 即可,我通常是使用 webpack 或 vite
                                 break;
                            default:
                                # test
                                sh "npm run build" # 自己手动添加 mode 即可,我通常是使用 webpack 或 vite
                        }
                       
                    }else{
                        echo "This is BULID value ${params.BULID}"
                    }
                }
                
            }
        }
        stage('DEPLOY') {
            steps {
                script {
                    if (params.DEPLOY){
                        switch(params.DEPLOYTARGET){
                            case "local":
                                echo "部署到本地服务器";
                                sh 'rm -rf /var/www/frontend/xxx/*'
                                sh 'mv /var/lib/jenkins/workspace/xxx/dist/* /var/www/frontend/xxx/'
                                break;
                            default:
                                # remote 
                                echo "部署到远程服务器";
                                # sh 'ls /var/lib/jenkins/workspace/xxx/dist/**'
                                sshPublisher(
                                    publishers: [
                                        sshPublisherDesc(
                                            configName: '这里填写SSH Servers中的Name', 
                                            transfers: [
                                                sshTransfer(
                                                    cleanRemote: false, 
                                                    excludes: '', 
                                                    execCommand: 'sudo chmod -R 777 /var/www/frontend/xxx/ && rm -rf /var/www/frontend/xxx/assets && rm -rf /var/www/frontend/xxx/index.html && rm -rf /var/www/frontend/xxx/favicon.ico && sudo mv /var/www/frontend/xxx/dist/* /var/www/frontend/xxx && sudo rm -rf /var/www/frontend/xxx/dist',
                                                    execTimeout: 120000, 
                                                    flatten: false, 
                                                    makeEmptyDirs: false, 
                                                    noDefaultExcludes: false, 
                                                    patternSeparator: '[, ]+', 
                                                    sourceFiles: 'dist/**', # 源文件
                                                    # removePrefix: 'dist/**', # 移除源文件夹
                                                    remoteDirectory: '/', # 远程目标文件夹
                                                    # remoteDirectorySDF: false, 
                                                    )
                                                ],
                                            usePromotionTimestamp: false, 
                                            useWorkspaceInPromotion: false, 
                                            verbose: true # 打印 log
                                        )
                                    ]
                                )
                                echo 'Credentials SUCCESS'
                                # sshServer: [
                                #     sshServerInfo(
                                #         host: 'IP 地址或主机名 ', # 远程服务器的 IP 地址或主机名
                                #         port: 22 # SSH 端口号,如果不是默认端口 (22), 请修改
                                #     )
                                # ]
                        }
                    }else{
                        echo "This is DEPLOY value ${params.DEPLOY}"
                    }
                }
            }
        }
    }
}
  1. 应用
  2. 构建

# 部署目标 (DEPLOYTARGET)

# 部署到当前服务器 (local)

解决方案如下:

  • 方案一 SSH
ssh root@IP地址
root password
sudo chmod -R 777 /var/www/frontend/xxx/ # 也要设置文件权限
rm -rf /var/www/frontend/xxx/*
mv /var/lib/jenkins/workspace/xxx/dist/* /var/www/frontend/xxx/
  • 方案二 更改 Jenkins 权限
    部署到本地服务器,我是需要移动打包好的项目文件,所以 jenkins 用户出现权限不足的问题。(私人服务器就随便了,不过还是更加推荐远程部署方式)

    Ubuntu 命令

  • 查看用户列表
    cat /etc/passwd | cut -d : -f1

  • 查看目标文件夹的所属组
    ls -ld /var/www/frontend/xxx/

  • 添加 Jenkins 用户到目标文件夹所属的组
    sudo usermod -aG 组的名字 jenkins

  • 更改目标文件夹的权限,使得 Jenkins 用户有写入权限(不包含目录下的文件夹)
    sudo chmod o+w /var/www/frontend/xxx/

  • 递归更改目标文件夹及其下所有文件和文件夹的权限(包含目录下的所有文件夹和文件)
    sudo chmod -R 777 /var/www/frontend/xxx/

如果你想确保目标文件夹下的所有文件和文件夹都具有读写权限,可以使用以下命令:

  • 递归更改目标文件夹及其下所有文件和文件夹的权限
    sudo chmod -R 777 /var/www/frontend/xxx/
    这里的 -R 选项表示递归,777 表示给予读写权限(4 表示读,2 表示写,1 表示执行,所以 4+2+1=7)。

  • 方案三 创建公开的文件夹
    直接在更目录创建文件夹,不归属任何账户。直接移动打包好的文件即可。

# 部署到远程服务器 (remote)
  • 安装 SSH 插件:
    在 Jenkins 中,通过 "系统管理 (Manage System)" -> "插件管理 (Manage Plugins)" -> "Available plugins" 标签页找到并安装 "Publish Over SSH" 插件。
  • 配置 SSH 插件:
    在 Jenkins 中,通过 "系统管理 (Manage System)" -> "系统配置 (Configure System)",找到 "Publish over SSH" 部分 -> "SSH Servers",配置远程服务器的凭据。
    凭据中的参数:
    1. Name: 自己随便定义名字
    2. Hostname: 服务器 IP 地址
    3. Username: 服务器用户名
    4. Remote Directory: 远程服务器目标文件
    5. 点击高级,设置 Use password authentication, or use a different key 中的 Passphrase / Password
    6. 保存即可
  • 修改 Jenkins Pipeline 脚本:
    在 Jenkins Pipeline 中,使用 sshPublisher 步骤来执行远程命令,将打包好的项目传输到远程服务器。

# SSR 应用自动化部署

暂时没有尝试,使用后再补充。

# Window 系统

# 命令

# 查看 Nginx 本版
nginx -v 	
# 重新加载配置
nginx -s reload 
# 检查 Nginx conf 的配置		
nginx -t		
# 启动 Nginx
start nginx  	
# 强制停止
nginx -s stop    
# 优雅退出
nginx -s quit    
# kill nginx 进程
taskkill /F /IM nginx

# 下载 Nginx 压缩包

传送门

# 使用

将压缩包放在一个路径没有中文和空格的路径下

# 配置

这里需要注意的地方就是 worker_processes: autoinclude xxx/*.conf

#user  nobody;
worker_processes  auto; #这里改成auto
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;
events {
 worker_connections  1024;
}
http {
    include       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  logs/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    include xxx/*.conf; # 这里指定自定义 config,但是不要与 nginx.conf 在同一个层级下
    #gzip  on;
    server {
        listen       80;
        server_name  localhost;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            root   html;
            index  index.html index.htm;
        }
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }
}

过去无法挽回,未来可以改变,有的人成日殚精竭虑,却掀不起什么风浪,有的人却因一念之差,让世界天翻地覆,这就是命运权重。

阅读次数

请我喝[茶]~( ̄▽ ̄)~*

NIDLH 微信支付

微信支付

NIDLH 支付宝

支付宝