Docker 部署 React 全栈应用(三)

Docker 部署 React 全栈应用(三)

技术杂谈小彩虹2021-08-25 16:09:56120A+A-

因字数限制,只能分开发,接上面 juejin.cn/post/690853…

三、部署篇

上面简单介绍了 docker 的一些用法,借用静态 HTML 文件与 nignx 镜像创建运行了一个容器,但是其远远不止这些,下面就通过部署本博客来作为例子再探探里面的一些知识点。

源码地址:https://github.com/Moon-Future/react-blog,可下载下来看着目录更清晰。

为了统一维护 docker 文件,以下将 docker 相关文件都放在各自目录下 docker 文件下,所以要特别主题构建上下文(context)的确定。

1. 部署前台 blog

端口映射 9000:9000,服务器端口:容器端口,若是线上服务器,要先在安全组里开通对应的端口号

在 blog 目录下创建 docker 目录,docker 目录下创建三个文件

  • .dockerignore:拷贝文件忽略列表
  • Dockefile
  • docker-compose.yml

.dockerignore

node_modules
.next

Dockefile

# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine

# 在容器中创建目录
RUN mkdir -p /usr/src/app

# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app

# 拷贝 package.json
COPY package.json /usr/src/app

# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app

# build
RUN npm run build

# 暴露端口 9000
EXPOSE 9000

# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]

Docker 镜像是分层的,下面这些知识点非常重要:

  • Dockerfile 中的每个指令都会创建一个新的镜像层,每个 RUN 都是一个指令 docs.docker.com/engine/refe…
  • 镜像层将被缓存和复用
  • 当 Dockerfile 的指令修改了,复制的文件变化了,或者构建镜像时指定的变量不同了,对应的镜像层缓存就会失效
  • 某一层的镜像缓存失效之后,它之后的镜像层缓存都会失效
  • 镜像层是不可变的,如果我们再某一层中添加一个文件,然后在下一层中删除它,则镜像中依然会包含该文件(只是这个文件在 Docker 容器中不可见了)。

所以我们先拷贝 package.json,然后 RUN npm i 安装依赖,形成一个镜像层,再拷贝其他所有文件,形成一个镜像层,之后如果代码有所变动,但是 package.json 没有变动,再次执行时,就不会再安装依赖了,可以节省很多时间。package.json 有变动,才会重新执行 RUN run i 安装依赖。

假如生成了镜像 imageA,此时要删除 imageA,重新生成,记住先生成新的镜像 imageB,这样才会复用 npm 包,如果先删除了 imageA,再新生成 imageB,则又会重新安装依赖。

在 blog 目录下运行以下命令可以生成镜像 react_blog:blog

$ docker build -f docker/Dockerfile . -t react_blog:blog

第一次运行安装依赖时有点慢(有个 sharp 特别慢...),刚开始我使用 node-sass 时,安装总是报错,后来索性就换成了 less,省心。如果想用 yarn 安装的话,这里 Dockerfile 里 npm 相关的命令也可以换成对于的 yarn 命令。

漫长的等待后终于 build 成功,下面一些信息就是 npm run build 生成的文件

image-20201217110814789

看看生成的镜像

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED         SIZE
react_blog   blog        fef06dfed97f   3 minutes ago   329MB
nginx        latest      ae2feff98a0c   31 hours ago    133MB
node         12-alpine   844f8bb6a3f8   3 weeks ago     89.7MB

然后来生成并运行容器

$ docker run -itd -p 9000:9000 --name react_blog_blog react_blog:blog

这里参数再说明一下:

  • -i 参数让容器的标准输入持续打开,--interactive
  • -t 参数让 Docker 分配一个伪终端,并绑定到容器的标准输入上, --tty
  • -d 参数让容器在后台,以守护进程的方式执行,--detach(Run container in background and print container ID)
  • --name 参数指定容器唯一名称,若不指定,则随机一个名称

-it 一般同时加上,-d 参数如果不加的话,运行容器成功时,会进入一个终端命令界面,要想退出的话只能 Ctrl + C,退出之后容器也就退出了,docker ps -a 可以看到容器状态是 Exited (0) ,可以使用 docker start container 再次开启。加上 -d 的话容器就会直接在后台运行,一般的话就加上 -d。大家可以试试,之后再删除容器就可以了。

image-20201217112605886

以上容器运行成功的话,在浏览器通过 服务器ip:9000 就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9000 就可以访问啦。

docker-compose.yml

version: '3'
services:
  web:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:blog
    ports:
      - 9000:9000
    container_name: react_blog_blog

上面咱们通过 docker builddocker run 等命令先生成容器,再生成并运行容器,是不是有点繁琐,命令不好记,输入也麻烦,这里我们就可以利用 docker-compose 来简化执行命令。

我们看一下文件内容:

  • web:服务名
  • build:构建相关,后面执行 docker-compose 命令路径要和 docker-compose.yml 同一路径,所以这里 context 构建上下文选择上一层源码目录,dockerfile 就是当前目录里的 Dockerfile
  • image:镜像名,如果有就直接使用,没有就通过上面的 Dockerfile 生成
  • ports:端口映射
  • container_name:容器名称,唯一。若不写,则为 当前目录_服务名_index,index 数字(从1累加),若这里为 docker_web_1

可以把上面用 Dockerfile 生成的容器删了 docker rm -f react_blog_blog,用 docker-compose up 生成试试

在 docker 目录下执行命令

$ docker-compose up -d

要想重新生成镜像可以 docker-compose up -d --build

以上便把 blog 前端页面部署好了,现在只是单独部署学习,后面会删了和后台与接口一起部署。

2. 部署后台 admin

端口映射 9001:9001,服务器端口:容器端口,若是线上服务器,要先在安全组里开通对应的端口号

现在来单独部署 admin,在 Docker 篇时,我们已经使用到 admin 来简单部署学习制作镜像和生成容器,这里依然先在 admin 目录下生成生成环境静态文件

$ npm run build

在 admin 下创建 docker 目录用来存放 docker 相关文件,docker 目录下创建以下文件:

Dockerfile

FROM nginx

# 删除 Nginx 的默认配置
RUN rm /etc/nginx/conf.d/default.conf

EXPOSE 80

注意这里和上面的一些区别,

  • 这里把 nginx 默认的配置删除了,之后我们自己配置一个
  • 没有 COPY 静态文件到容器,在 docker-compose.yml 通过挂在的方式实现

docker-compose.yml

version: '3'
services:
  admin:
    build: 
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:admin
    ports: 
      - 9001:80
    volumes: 
      - ../build:/www
      - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
    container_name: react_blog_admin

这里多了 volumes (卷) 项,参数是数组,对应 宿主机文件:容器内文件

  • ../build:/www,build 内的文件挂在到容器 /www 目录下
  • ./nginx.conf:/etc/nginx/conf.d/nginx.conf,nginx.conf 挂载到容器 /etc/nginx/conf.d/nginx.conf 这个文件

这样做的好处是,当宿主机上的文件变动后,容器内的文件也会自动变动,相应的容器内文件变动,宿主机文件也会变动。这样之后源代码变动,重新打包生成 build 后,只需要放到服务器对应目录下,容器类 /www 下的类容就会是最新的,而不需要一次次的去执行 Dockerfile 拷贝 build 文件到容器内,数据库的数据通常也是这样保存在宿主机内,而防止容器删除时丢失数据。

同理 nginx.conf 配置文件也是一样,不过改动 nginx 配置文件后,要重启以下容器才生效 docker restart container

来运行容器吧,在 docker 目录下执行命令

$ docker-compose up -d

查看容器是否运行成功

$ docker ps -a
CONTAINER ID   IMAGE          COMMAND             CREATED         STATUS          PORTS            NAMES
7db8ce1c6814   react_blog:admin   "/docker-entrypoint.…"   16 minutes ago   Up 16 minutes   0.0.0.0:9001->80/tcp   react_blog_admin

运行失败的可以 docker logs container 查看日志

运行成功的话,在浏览器通过 服务器ip:9001 就可以访问到页面啦,Mac 或者 Windows 本地的话 localhost:9001 就可以访问啦。

nginx.conf

server {
  listen 80;
  sendfile on;
  sendfile_max_chunk 1M;
  tcp_nopush on;
  gzip_static on;

  location / {
    root /www;
    index index.html;
  }
}

root 记得和上面挂在目录相同

3. 部署服务接口 service + Mysql

端口映射 9002:9002,服务器端口:容器端口,若是线上服务器,要先在安全组里开通对应的端口号

现在我们来部署服务接口,在 service 目录下创建 docker 目录,docker 目录下创建以下文件:

.dockerignore

node_modules
.github
article

article 目录用来存放博客内容文件

Dockerfile

FROM node:alpine

# 配置环境变量
ENV NODE_ENV production

# 这个是容器中的文件目录
RUN mkdir -p /usr/src/app 

# 设置工作目录
WORKDIR /usr/src/app

# 拷贝package.json文件到工作目录
# !!重要:package.json需要单独添加。
# Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。
# 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。
# 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。
COPY package.json /usr/src/app/package.json

# 安装npm依赖(使用淘宝的镜像源)
# 如果使用的境外服务器,无需使用淘宝的镜像源,即改为`RUN npm i`。
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷贝所有源代码到工作目
COPY . /usr/src/app

# 暴露容器端口
EXPOSE 9002

CMD npm start

docker-compose.yml

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    depends_on:
      - db
    environment:
      MYSQL_HOST: localhost
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ../article:/usr/src/app/article
    container_name: react_blog_service
  db:
    image: mysql
    # volumes:
    # - /db_data:/var/lib/mysql
    ports:
      - 33061:3306
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 8023
      # MYSQL_USER: root
      MYSQL_PASSWORD: 8023
      MYSQL_DATABASE: react_blog
    container_name: react_blog_mysql

注意这里有运行了两个服务 service、db

service 服务是后端接口:

  • deponds_on:运行时会先运行 deponds_on 列表里的服务,防止依赖项还没运行,自己会报错

  • command:从 MySQL8.0 开始,默认的加密规则使用的是 caching_sha2_password,此命令可以更改加密规则。不加可能会报错 Client does not support authentication protocol requested by server; consider upgrading MySQL client

  • environment:环境变量,这里会传入代码中,在代码 /config/secret.js(secret-temp.js) 里面可以会使用到

    /** * secret.js 模板 */
      
    module.exports = {
      // mysql 连接配置
      mysql: {
        host: process.env.MYSQL_HOST || 'localhost',
        port: process.env.MYSQL_PORT || '3306',
        user: process.env.MYSQL_USER || 'xxx',
        password: process.env.MYSQL_PASSWORD || 'xxx',
        database: process.env.MYSQL_DATABASE || 'xxxxxx',
      },
      // jwt
      tokenConfig: {
        privateKey: 'xxxxxxxxxxxxxxxxxxxxxxxxx',
      },
    }
    
  • volumes:这里我把文章写入宿主机了,挂载到容器里

db 服务是 Mysql 数据库:

  • volumes:数据设置存储在宿主机

  • ports:端口映射,宿主机通过 33061 端口可以访问容器内部 Mysql,我们之后就可以通过 Navicat 或其他数据库可视化工具来连接

  • environment:配置数据库

    • MYSQL_ROOT_PASSWORD 必须要带上,设置 ROOT 账号的密码
    • MYSQL_USER 容器登录 MySQL 用户名,注意,这里如果是 root 会报错 ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户
    • MYSQL_PASSWORD 容器登录 Mysql 密码,对用户名 MYSQL_USER,如果是 ROOT,密码就是 MYSQL_ROOT_PASSWORD,如果是其他,就是设置新密码
    • MYSQL_DATABASE 创建一个 react_blog 数据库,也可以不填,后面再进入容器或者 Navicat 创建,但是这里因为后端代码要连接到 react_blog 数据库,不创建的会连接会保存,所以还是加上。(实在不想加也可以后见创建好数据库后,才运行两个容器)

在 service/docker 目录下执行命令

$ docker-compose up -d

运行成功的话,看看 images 和 container

$ docker images
REPOSITORY   TAG         IMAGE ID       CREATED             SIZE
react_blog   service     89139d833458   About an hour ago   150MB
react_blog   admin       1b5d6946f1fe   32 hours ago        133MB
react_blog   blog        fef06dfed97f   35 hours ago        329MB
nginx        latest      ae2feff98a0c   2 days ago          133MB
mysql        latest      ab2f358b8612   6 days ago          545MB
node         12-alpine   844f8bb6a3f8   3 weeks ago         89.7MB

可以看到多了 Mysql 和 react_blog:blog 镜像

$ docker ps -a
CONTAINER ID   IMAGE             COMMAND       CREATED           STATUS        PORTS                       NAMES
5878940d7626   react_blog:blog   "docker..."   5 seconds ago     Up 4 seconds  0.0.0.0:9000->9000/tcp      react_blog_blog
3bff0060de19   react_blog:admin  "/docker…"    3 minutes ago     Up 18 seconds 0.0.0.0:9001->80/tcp        react_blog_admin
d8a899232e8c   react_blog:service "docker…"    About a           Exited (1) 5 minutes ago                  react_blog_service
a9da07ff5cae   mysql              "docker…"    About an hr       33060/tcp, 0.0.0.0:33061->3306/tcp        react_blog_mysql

可以看到多了 react_blog_service 和 react_blog_mysql 容器,其中 react_blog_service 容器运行失败了,显示没事失败的先别高兴,咱们来看看日志

$ docker logs react_blog_service

...
errno: "ECONNREFUSED"
code: "ECONNREFUSED"
syscall: "connect"
address: "127.0.0.1"
port: 3306
fatal: true
name: "ECONNREFUSEDError"
pid: 47
hostname: d8a899232e8c
...

可以看出是数据库连接失败了,在上面 docker-compose.yml 中我们定义的环境变量 MYSQL_HOST=localhost 传给后端代码来连接数据库,每个容器都相当一一个独立的个体,localhost 是 react_blog_service 自己的 ip (127.0.0.1),当然是访问不到 react_blog_mysql,这个问题我们在下一节再来解决,先来说说 Mysql。

上面可以看到 Mysql 容器已经成功运行,我们可以进入容器内部连接 Mysql,还记得怎么进入容器吗

$ docker exec -it react_blog_mysql /bin/sh
 $ ls
bin  boot  dev	docker-entrypoint-initdb.d  entrypoint.sh  etc	home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
$ mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.22 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 mysql> 

可以看到顺利连接 Mysql,输入 exit 可以退出容器。我们也可以使用可视化工具来连接,我这里使用 Navicat 来连接

image-20201218224818914

image-20201218225257487

注意这里的端口 33061,上面我们通过端口映射,通过宿主机端口 33061 可以访问到 Mysql 容器内端口 3306,所以就连接上啦。

在这个过程中,我的服务器(宿主机)上的 Mysql 出现了问题,连接时报错 2013 lost connection to mysql server at 'reading initial communication packet',我也不知道是什么原因引起的,解决方式是运行命令 systemctl start mysqld.service 启动 Mysql 服务,也不知是哪里影响到了,不过后面我会直接连接宿主机 Mysql,不使用容器,这样可以和其他项目统一管理数据,我任务比较方便,且数据也较安全。

4. 容器互联

上面留了一个问题,service 连接数据库失败,现在我们来尝试解决。参考 Docker 筑梦师系列(一):实现容器互联

4.1 Network 类型

Network,顾名思义就是 “网络”,能够让不同的容器之间相互通信。首先有必要要列举一下 Docker Network 的五种驱动模式(driver):

  • bridge:默认的驱动模式,即 “网桥”,通常用于单机(更准确地说,是单个 Docker 守护进程)
  • overlay:Overlay 网络能够连接多个 Docker 守护进程,通常用于集群,后续讲 Docker Swarm 的文章会重点讲解
  • host:直接使用主机(也就是运行 Docker 的机器)网络,仅适用于 Docker 17.06+ 的集群服务
  • macvlan:Macvlan 网络通过为每个容器分配一个 MAC 地址,使其能够被显示为一台物理设备,适用于希望直连到物理网络的应用程序(例如嵌入式系统、物联网等等)
  • none:禁用此容器的所有网络

默认情况下,创建的容器都在 bridge 网络下,如下如所示,各个容器通过 dokcer0 可连接到宿主机HOST,并且各自分配到 IP,这种情况下,容器间互相访问需要输入对方的 IP 地址去连接。

image-20201220153944034

查看 network 列表

$ docker network ls
NETWORK ID     NAME             DRIVER    SCOPE
a75e040b03ed   bridge           bridge    local
13545e6a3970   docker_default   bridge    local
5ec462838a1c   host             host      local
c726e6887f10   none             null      local

这里有 4 的 network,默认本来只有 3 个,没有 docker_default,我也是写到这里才发现创建了一个 docker_default 网络,查找官网(Networking in Compose)才发现,通过 docker-compose 来生成运行容器时,如果没指定 network,会自动创建一个 network,包含当前 docker-compose.yml 下的所有容器,network 名字默认为 目录_default ,这里目录就是 docker 恰好我们这个几个 docker-compose.yml 都是放在 docker 目录下,所以创建的几个容器都是在 docker_default 网络里。可以一下命令查看网络详细信息

$ docker network inspect docker_default

[
    {
        "Name": "docker_default",
        "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
        "Created": "2020-12-16T11:03:37.2152073+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.24.0.0/16",
                    "Gateway": "172.24.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "23891d43187e046eea25936dc0ab703964cc6c7213bb150ae9529da3e2e57662": {
                "Name": "react_blog_mysql",
                "EndpointID": "649857f928e0444500cfd296035869678bf26162d429a4499b262776b2a1d264",
                "MacAddress": "02:42:ac:18:00:03",
                "IPv4Address": "172.24.0.3/16",
                "IPv6Address": ""
            },
            "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
                "Name": "react_blog_admin",
                "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
                "MacAddress": "02:42:ac:18:00:02",
                "IPv4Address": "172.24.0.2/16",
                "IPv6Address": ""
            },
            "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
                "Name": "react_blog_blog",
                "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
                "MacAddress": "02:42:ac:18:00:04",
                "IPv4Address": "172.24.0.4/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "docker",
            "com.docker.compose.version": "1.25.1"
        }
    }
]

可以看到 docker_default 网关地址为 172.24.0.1 ,其他几个容器 IP 分别为 172.24.0.3172.24.0.2172.24.0.4,所以这里的情况是这样的

image-20201220154038077

上面说了默认网络 bridge 下容器见访问只能输入 IP 地址来连接,而自定义的网络还可以通过容器名来连接

On user-defined networks like alpine-net, containers can not only communicate by IP address, but can also resolve a container name to an IP address. This capability is called automatic service discovery.

Networking with standalone containers

这就可以避免每次生成容器 IP 会变的问题了。知道了这些,我们在 service 接口里就可已通过 react_blog_mysql 来连接 react_blog_mysql 容器了,service/docker/docker-compose.yml 修改如下:

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    restart: on-failure
    depends_on:
      - db
    environment:
      MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ./article:/usr/src/app/article
    container_name: react_blog_service
  db:
    image: mysql
    ports:
      - 33061:3306
    restart: on-failure
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_ROOT_PASSWORD: 8023
      MYSQL_PASSWORD: 8023
      MYSQL_DATABASE: react_blog
    container_name: react_blog_mysql

在此运行命令

$ docker-compose up -d --build

image-20201219011335466

可以看到服务容器已正常运行,docker logs react_blog_service 查看日志也没有报错,说明已经连接数数据库,在代码你我加了一个 get 测试接口,在浏览器输入 IP:9002/api/test/get 或者 localhost:9002/api/test/get,会返回一个 json 对象

{"message":"Hello You Got It"}

这里我试了 N 久,一直有问题,

  • Mysql 创建失败,environment 我加了一个 MYSQL_USER: root,结果一直报错 ERROR 1396 (HY000): Operation CREATE USER failed for 'root'@'%' ,根据 github.com/docker-libr… 可知,已经存在一个 root 用户,无法再创建,所以这个可以不带,就默认 root 用户登录,如果带的话就不要是 root,会新建一个账户。这里直接去掉 MYSQL_USER,使用 root 登录

  • service 创建失败,日志报错没连接上 Mysql,我试试了好久,最后发现重启一下 service docker start react_blog_service 就可以了,所以我觉得应该是 Mysql 创建好后,数据口等一些配置还没搞好,所以 service 还连接不上,就一直报错,等一会重新运行 service 就好了,所以这里加上了 restart 参数,报错就重新启动,这样就不用自己去重启了,等一会,看日志没问题,就是连接成功了。

    明明使用了 depends_on,为什么还会有这种问题呢,我也不太清楚,不过官网有这段示例:

    version: "3.9"
    services:
      web:
        build: .
        depends_on:
          - db
          - redis
      redis:
        image: redis
      db:
        image: postgres
    

    depends_on does not wait for db and redis to be “ready” before starting web - only until they have been started. If you need to wait for a service to be ready, see Controlling startup order for more on this problem and strategies for solving it.

    Depends_on 在启动 Web 之前不会等待 db 和 Redis 处于“就绪”状态-仅在它们启动之前。

    应该就这个原因了~

我们再来看看 docker_default 网络

$ docker network inspect docker_default

[
    {
        "Name": "docker_default",
        "Id": "13545e6a39708344b363b7fc16eefeb6775c37773222804ebd5b5fb6f28c38bb",
        "Created": "2020-12-16T11:03:37.2152073+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.24.0.0/16",
                    "Gateway": "172.24.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "3bff0060de19fc973039c07c1931e2c1efe30c6707bcd77d2ff7ea4dc01aaf63": {
                "Name": "react_blog_admin",
                "EndpointID": "25d8fa518b0ce27498f562372c3424aee174cb1d8fbf9f2445f1c6af8e6aab7f",
                "MacAddress": "02:42:ac:18:00:02",
                "IPv4Address": "172.24.0.2/16",
                "IPv6Address": ""
            },
            "5878940d7626a9fb20622cde4002075e390e5036036bafb99d80454d6cba594b": {
                "Name": "react_blog_blog",
                "EndpointID": "a3f8ee36eda09f524be7ea16a67a1e13e62cf558e5480218bb523f877d478e4a",
                "MacAddress": "02:42:ac:18:00:04",
                "IPv4Address": "172.24.0.4/16",
                "IPv6Address": ""
            },
            "83005eec8d50071a6c23a2be4af8552983c09c532e937f04d79f02f8eb68acc9": {
                "Name": "react_blog_mysql",
                "EndpointID": "265ed7793c98287a05ccf8997e81671287a02ee8ea464984996083a34abe10dd",
                "MacAddress": "02:42:ac:18:00:03",
                "IPv4Address": "172.24.0.3/16",
                "IPv6Address": ""
            },
            "937339a37ce726e704ec21b31b4028a97967a00de01438557e5a60d8538a51c8": {
                "Name": "react_blog_service",
                "EndpointID": "934d26f32a2b23e2cb4691020cb93d26c97b9647108047b492c3f7dd2be6faef",
                "MacAddress": "02:42:ac:18:00:05",
                "IPv4Address": "172.24.0.5/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "docker",
            "com.docker.compose.version": "1.25.1"
        }
    }
]

可以看到 react_blog_service 也已正常加入网络,IP 为 172.24.0.5

4.2 自定义 Network

docker_default 网络是根据目录来创建的,恰巧我们这几个项目 docker-compose.yml 文件都放在 docker 目录下,所以都在一个网络,如果名称变了就不在一个网络,并且之后项目可能还会有 docker 目录,全部都在一个网络也是不太好的,所以这里我们来自定义本次项目的网络。

blog/docker/docker-compose.yml

version: '3'
services:
  blog:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:blog
    ports:
      - 9000:9000
    networks: 
      - react_blog
    container_name: react_blog_blog
networks: 
  react_blog:

admin/docker/docker-compose.yml

version: '3'
services:
  admin:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:admin
    ports:
      - 9001:80
    volumes:
      - ../build:/www
      - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
    networks:
      - react_blog
    container_name: react_blog_admin
networks:
  react_blog:

service/docker/docker-compose.yml

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    depends_on:
      - db
    environment:
      - MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
      - MYSQL_USER=root
      - MYSQL_PASSWORD=8023
    volumes:
      - ./article:/usr/src/app/article
    networks:
      - react_blog
    container_name: react_blog_service
  db:
    image: mysql
    ports:
      - 33061:3306
    command: --default-authentication-plugin=mysql_native_password
    environment:
      - MYSQL_ROOT_PASSWORD=8023
      - MYSQL_USER=root
      - MYSQL_PASSWORD=8023
      - MYSQL_DATABASE=react_blog
    networks:
      - react_blog
    container_name: react_blog_mysql
networks:
  react_blog:
  • 与services 同级的 networks:创建一个新的 network,这里生成的 network 最终名称也会加上目录名,docker_react_blog。
  • 服务内部的 networks:加入哪些网络,参数带 “-” 说明是数组,可以加入多个网络,这里我们全部加入 react_blog,不分前后端了

注意:

这样在 dockor-compose.yml 里生成的 network 都会加上当前目录名,若想不带,可以自己先生成一个

$ docker network create my_net

然后在 dockor-compose.yml 里

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    depends_on:
      - db
    environment:
      - MYSQL_HOST=react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
      - MYSQL_USER=root
      - MYSQL_PASSWORD=8023
    volumes:
      - ./article:/usr/src/app/article
    networks:
      - my_net
    container_name: react_blog_service
  db:
    image: mysql
    ports:
      - 33061:3306
    command: --default-authentication-plugin=mysql_native_password
    environment:
      - MYSQL_ROOT_PASSWORD=8023
      - MYSQL_USER=root
      - MYSQL_PASSWORD=8023
      - MYSQL_DATABASE=react_blog
    networks:
      - my_net
    container_name: react_blog_mysql
networks:
  my_net:
  	external: true

加个 external 参数则使用已经创建的 network(my_net),不会再去创建或加上目录名。

我们再来重新创建容器,先删除全部容器

$ docker stop $(docker ps -aq) 
$ docker rm $(docker ps -aq)

在进入各个目录分别执行 docker-compose up -d,在运行第一个时会看到 Creating network "docker_react_blog" with the default driver 这句话,说明创建了一个新的 network,我们来看看

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
a75e040b03ed   bridge              bridge    local
13545e6a3970   docker_default      bridge    local
e1ceb437a4fd   docker_react_blog   bridge    local
5ec462838a1c   host                host      local
c726e6887f10   none                null      local
 $ docker network inspect docker_react_blog
[
    {
        "Name": "docker_react_blog",
        "Id": "e1ceb437a4fdc5de91e51ff8831e21b565c92754159ad7057de36758e548a92f",
        "Created": "2020-12-19T01:39:02.201644444+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "00da404f6f050b9b2f20e39bbb136fef614e8dfee85ec31bd6000bfd59cc2dab": {
                "Name": "react_blog_mysql",
                "EndpointID": "1cb966cc731eca3e9721e6d3edcfcac6152b66051faa934557f567e9e36c75c6",
                "MacAddress": "02:42:ac:12:00:04",
                "IPv4Address": "172.18.0.4/16",
                "IPv6Address": ""
            },
            "ad1480e48e8e7ed160b1d4bcf7eed77d74505aea7581d48d8931206772b5d805": {
                "Name": "react_blog_service",
                "EndpointID": "8866c3457382d6baa945da09aef40da54c7dfdea0f393485001c35bb37d201a0",
                "MacAddress": "02:42:ac:12:00:05",
                "IPv4Address": "172.18.0.5/16",
                "IPv6Address": ""
            },
            "b518d40b5021d3fdec7b7e62fbaa47b8a705a38346ccba2b9814174e46b67cd0": {
                "Name": "react_blog_admin",
                "EndpointID": "9a58ff20dc57d4d1fa6af83482051a68e80e22a5e37cf8e0cb3570b78102f107",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            },
            "db0050257a8e8a0fa430ea04b009ae819dbf04ef001cf1027ec2b5565403b48e": {
                "Name": "react_blog_blog",
                "EndpointID": "664794ed292871bc7fd8e1c4eaa56f682a6be5d653209f84158f3334a4f30660",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "react_blog",
            "com.docker.compose.project": "docker",
            "com.docker.compose.version": "1.25.1"
        }
    }
]

4.3 调用接口

现在还有一个问题,我们在代码中调用接口形式是 http://localhost:9002/api/xxx ,在 react_blog_blog 容器中调用接口 localhost 是本身自己,没有调到 react_blog_service 里面的接口。

针对 admin

在代码中,我们这样来调接口

const HOST = process.env.NODE_ENV === 'development' ? 'http://localhost:9002' : ''

const API = {
  getArticleList: HOST + '/api/getArticleList',
  getArticle: HOST + '/api/getArticle',
  addArticle: HOST + '/api/addArticle',
  delArticle: HOST + '/api/delArticle',

  getTagList: HOST + '/api/getTagList',
  addTag: HOST + '/api/addTag',
  delTag: HOST + '/api/delTag',

  register: HOST + '/api/register',
  login: HOST + '/api/login',
}

export default API

image-20201219160058561

会发现接口 404,我们通过 nginx 来代理接口请求

admin/docker/nginx.conf

server {
  listen 80;
  sendfile on;
  sendfile_max_chunk 1M;
  tcp_nopush on;
  gzip_static on;

  location /api {
    proxy_pass http://react_blog_service:9002;
  }

  location / {
    root /www;
    index index.html;
  }
}

以 /api 为开头的请求,我们都转发到 react_blog_service 容器 9002 端口,将 nginx.conf 拖到服务器,因为我们是将此文件挂载到容器内部的,所以这里只需要重启一下容器

$ docker restart react_blog_admin

再看看请求接口,可以看到请求 200 成功,返回数据,如果返回 500,说明数据库还没建表,将目录下 react_blog.sql 导入数据库就可以了。

image-20201219160458357

针对 blog

开始我以为通过环境变量(Next 中要存储在运行时变量里 Runtime Configuration)来传递请求 HOST (react_blog_service || localhost) ,但发现 react_blog_service 直接拼在前端接口里访问是不可行的(getServerSideProps 可行),所以最后还是改为 nginx 来代理请求,并且后面我们肯定还是要通过域名来访问网站的,所以还是需要 nginx,那么我们就为前台页面来加一个 nginx 容器。

1、创建环境变量

blog/docker/Dockerfile

# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine

# 在容器中创建目录
RUN mkdir -p /usr/src/app

# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app

# 拷贝 package.json
COPY package.json /usr/src/app

# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app

# build 阶段获取
ENV HOST react_blog_service ## 增加一个环境变量,在 build 阶段可获取到,一定放在 npm run build 前一行

# build
RUN npm run build

# 暴露端口 9000
EXPOSE 9000

# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]

代码中,设置运行是变量 blog/next.config.js

const withCSS = require('@zeit/next-css')
const withLess = require('@zeit/next-less')
module.exports = () =>
  withLess({
    ...withCSS(),
    // 改为 nginx 代理
    publicRuntimeConfig: {
      HOST: process.env.HOST || 'localhost', // 如果是 docker build,此处 process.env.HOST,否则就 localhsot,不影响本地运行
    },
  })

blog/config/api

import getConfig from 'next/config'
const { publicRuntimeConfig } = getConfig()

const SSRHOST = `http://${publicRuntimeConfig.HOST}:9002`
const HOST = `http://localhost:9002`

export const SSRAPI = {
  getArticleList: SSRHOST + '/api/getArticleList',
  getArticle: SSRHOST + '/api/getArticle',
}

export const API = {
  getArticleList: HOST + '/api/getArticleList',
  getArticle: HOST + '/api/getArticle',
}

这里有点麻烦,我不知道我的理解对不对,但试了多种情况只有这种本地和 docker 部署才都可以。

  • 如果是本地运行(不使用 docker),服务端获取数据(getServerSideProps)和页面中获取数据直接使用服务接口地址(localhost:9002)即可

  • 如果是 docker 运行,服务端获取数据(getServerSideProps)需要直接带上服务接口容器地址,无法通过 nginx 代理,页面中获取数据调用接口则职能通过 nginx 代理的方式

2、nginx 代理

修改 blog/docker/docker-compose.yml,增加一个 nginx 容器

version: '3'
services:
  blog:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:blog
    # ports:
    # - 9000:9000
    networks:
      - react_blog
    container_name: react_blog_blog
  nginx:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile-nginx
    image: react_blog:nginx
    ports:
      - 9000:80
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/nginx.conf
    networks:
      - react_blog
    container_name: react_blog_nginx
networks:
  react_blog:

blog/docker/Dockerfile-nginx

FROM nginx

# 删除 Nginx 的默认配置
RUN rm /etc/nginx/conf.d/default.conf

EXPOSE 80

blog/docker/nginx.conf

server {
  listen 80;
  sendfile on;
  sendfile_max_chunk 1M;
  tcp_nopush on;
  gzip_static on;

  location /api {
    proxy_pass http://react_blog_service:9002;
  }

  location / {
    proxy_pass http://react_blog_blog:9000;
  }
}

3、生成容器

因为 blog 的内容有变,所以需要重新生成镜像,使用 docker-compose up -d --build 会重新下载 npm node_modules,比较慢,所以还是先生成镜像。

在 blog 目录下执行

$ docker build -f docker/Dockerfile . -t react_blog:blog

在 blog/docker 下执行

$ docker-compose up -d

运行成功的话,再试试接口就可以获取数据啦。

5. 连接宿主机 Mysql

上面遇到一个问题,在上面过程中,我的服务器(宿主机)上的 Mysql 出现了问题,连接时报错 2013 lost connection to mysql server at 'reading initial communication packet',我也不知道是什么原因引起的,解决方式是运行命令 systemctl start mysqld.service 启动 Mysql 服务,也不知是哪理影响到了。因为之前其他项目都是单独部署的,没使用 docker,数据都在宿主机 Mysql 上,所以我还是跟倾向于统一管理,自适应宿主机一个 Mysql,下面来看看怎么实现吧。

这里有两种方式

方式一:network_mode: host

修改 service/docker/docker-compose.yml

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    restart: on-failure
    # depends_on:
    # - db
    environment:
      # MYSQL_HOST: react_blog_mysql # 此处 localhost 换为 mysql 容器名,在同一个自定义网络下,变会自动解析为 IP 连接
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ../article:/usr/src/app/article
    network_mode: host
    # networks:
    # - react_blog
    container_name: react_blog_service
  # db:
  # image: mysql
  # ports:
  # - 33061:3306
  # restart: on-failure
  # command: --default-authentication-plugin=mysql_native_password
  # environment:
  # MYSQL_ROOT_PASSWORD: 8023
  # MYSQL_PASSWORD: 8023
  # MYSQL_DATABASE: react_blog
  # networks:
  # - react_blog
  # container_name: react_blog_mysql
networks:
  react_blog:

service/docker 下执行命令

$ docker-compose up -d
 $ docker ps -a
CONTAINER ID   IMAGE                COMMAND                  CREATED          STATUS          PORTS    NAMES
af9e525e7d14   react_blog:service   "docker-entrypoint.s…"   28 seconds ago   Up 26 seconds            react_blog_service

可以看到 service 运行正常,且没有端口映射,docker inspect react_blog_service 也没有分配 IP,这种就相当于一个 Node 应用自己连接到宿主机 Mysql。但是对于页面接口请求来说,因为 react_blog_service 已不在 docker_react_blog ,所以就要使用宿主机 IP 地址来访问了。

nginx.conf

server {
  listen 80;
  sendfile on;
  sendfile_max_chunk 1M;
  tcp_nopush on;
  gzip_static on;

  location /api {
    # proxy_pass http://react_blog_service:9002;
    proxy_pass http://xxx.xx.xxx.x:9002; # xxx.xx.xxx.x 为宿主机(服务器)IP 
  }

  location / {
    proxy_pass http://react_blog_blog:9000;
  }
}

服务端渲染接口也是一样

# node 镜像
# apline 版本的node会小很多
FROM node:12-alpine

# 在容器中创建目录
RUN mkdir -p /usr/src/app

# 指定工作空间,后面的指令都会在当前目录下执行
WORKDIR /usr/src/app

# 拷贝 package.json
COPY package.json /usr/src/app

# 安装依赖
RUN npm i --production --registry=https://registry.npm.taobao.org

# 拷贝其他所有文件到容器(除了 .dockerignore 中的目录和文件)
COPY . /usr/src/app

# build 阶段获取,xxx.xx.xxx.x 为宿主机(服务器)IP
ENV HOST xxx.xx.xxx.x

# build
RUN npm run build

# 暴露端口 9000
EXPOSE 9000

# 运行容器时执行命令,每个 Dokcerfile 只能有一个 CMD 命令,多个的话只有最后一个会执行
CMD [ "npm", "start" ]

这种方式是不是很麻烦,还要暴露服务器 IP 地址,所以我选择方式二

方式二:

修改 service/docker/docker-compose.yml

version: '3'
services:
  service:
    build:
      context: ../
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    restart: on-failure
    # depends_on:
    # - db
    environment:
      MYSQL_HOST: 172.17.0.1
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ../article:/usr/src/app/article
    networks:
      - react_blog
    container_name: react_blog_service
  # db:
  # image: mysql
  # ports:
  # - 33061:3306
  # restart: on-failure
  # command: --default-authentication-plugin=mysql_native_password
  # environment:
  # MYSQL_ROOT_PASSWORD: 8023
  # MYSQL_PASSWORD: 8023
  # MYSQL_DATABASE: react_blog
  # networks:
  # - react_blog
  # container_name: react_blog_mysql
networks:
  react_blog:

这里 MYSQL_HOST 为 172.17.0.1,上面也说了,容器可以通过此 IP 来连接到宿主机,所以这就连接上宿主机的 Mysql 了,其他的地方就不需要改了。

6. 一个 docker-compoer.yml

前面用了 3 个 docker-compose.yml 来启动各自的项目,还是挺繁琐的,我们来写一个汇总的,一个命令运行所以,当然后面某一个项目需要重新跑,也可以进入各自目录去运行自己的 docker-compose.yml

在项目根目录创建 docker/docker-compose.yml,创建 docker 目录,是为了创建的 network 和单个项目运行是创建的一致

version: '3'
services:
  blog:
    build:
      context: ../blog
      dockerfile: ./docker/Dockerfile
    image: react_blog:blog
    networks:
      - react_blog
    container_name: react_blog_blog
  nginx:
    build:
      context: ../blog
      dockerfile: ./docker/Dockerfile-nginx
    image: react_blog:nginx
    ports:
      - 9000:80
    volumes:
      - ../blog/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
    networks:
      - react_blog
    container_name: react_blog_nginx
  admin:
    build:
      context: ../admin
      dockerfile: ./docker/Dockerfile
    image: react_blog:admin
    ports:
      - 9001:80
    volumes:
      - ../admin/build:/www
      - ../admin/docker/nginx.conf:/etc/nginx/conf.d/nginx.conf
    networks:
      - react_blog
    container_name: react_blog_admin
  service:
    build:
      context: ../service
      dockerfile: ./docker/Dockerfile
    image: react_blog:service
    ports:
      - 9002:9002
    restart: on-failure
    environment:
      MYSQL_HOST: 172.17.0.1
      MYSQL_USER: root
      MYSQL_PASSWORD: 8023
    volumes:
      - ../service/article:/usr/src/app/article
    networks:
      - react_blog
    container_name: react_blog_service
networks:
  react_blog:

停止并删除之前创建的所有容器

$ docker stop $(docker ps -aq)
$ docker rm $(docker ps -aq)

进入 /docker 目录执行,

$ docker-compose up -d
Building nginx
Step 1/3 : FROM nginx
 ---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Running in bb163c42c6b5
Removing intermediate container bb163c42c6b5
 ---> 282cb303dddf
Step 3/3 : EXPOSE 80
 ---> Running in 9b77ebd39952
Removing intermediate container 9b77ebd39952
 ---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:nginx
WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building admin
Step 1/3 : FROM nginx
 ---> ae2feff98a0c
Step 2/3 : RUN rm /etc/nginx/conf.d/default.conf
 ---> Using cache
 ---> 282cb303dddf
Step 3/3 : EXPOSE 80
 ---> Using cache
 ---> fbb18dda70af
Successfully built fbb18dda70af
Successfully tagged react_blog:admin
WARNING: Image for service admin was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating react_blog_admin   ... done
Creating react_blog_service ... done
Creating react_blog_blog    ... done
Creating react_blog_nginx   ... done
 $ docker ps -a
CONTAINER ID   IMAGE      			COMMAND    CREATED          STATUS         PORTS                    NAMES
1fbb15abdd30   react_blog:service   "docker"   13 seconds ago   Up 6 seconds   0.0.0.0:9002->9002/tcp   react_blog_service
fbee53e25c3a   react_blog:admin     "/docker"  13 seconds ago   Up 6 seconds   0.0.0.0:9001->80/tcp     react_blog_admin
70cb25f87d14   react_blog:blog      "docker"   13 seconds ago   Up 6 seconds   9000/tcp                 react_blog_blog
aa9fbf2afea4   react_blog:nginx     "/docker"  13 seconds ago   Up 6 seconds   0.0.0.0:9000->80/tcp     react_blog_nginx

运行成功~

7. 域名

我现在是通过宿主机的 nginx 来代理域名访问 IP:9000,然后访问到 react_blog_nginx 容器,本想是直接在 react_blog_nginx 中做代理,但是试了没成功。想了想,访问 react_blog_nginx 是通过端口映射,宿主IP:9000 访问到的,如果在 react_blog_nginx 内部配置域名,总感觉是无法访问,这点还没想过,这几天再试试。

结语

终于写完了,写之前已经学习尝试了好久,以为很有把握了,结果在写的过程中又遇到一堆问题,一个问题可能都会卡好久天,各种百度,Google,油管都用上啦,总算解决了遇到的所有问题,当然这些问题可能只满足了我现在的部署需求,其中还有很多知识点,没有接触到,不过没关系,我就是想成功部署前端项目就可以了。

以上便是 docker 部署前端项目的所有笔记,内容比较啰嗦,希望能帮助后来的同学少走一点坑,因为有些是自己的理解,可能会有错误,还请大家指正,互相学习,over。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们