docker基础1

docker基础1

Linux基础

因为我在博客:Linux基础 中详细介绍了一些基本命令,因此这里只展示summary的内容。

在docker中运行 ubuntu:

docker run ubuntu ,会自动监测是否存在 ubuntu 容器,没有的话会去 docker hub 拉去该容器。

然后我们还要运行 docker run -it ubuntu 才能启动。

Managing packages

  • apt update
  • apt list
  • apt install nano
  • apt remove nano

Manipulating files and directories

Editing and viewing files

Searching for text

Finding files and directories

Managing environment variables

Managing processes

Managing users and groups

File permissions

Building Images

在这一章,我们要学习

  • 创建 Docker文件
  • 对images 进行版本控制
  • 共享 images
  • 保存、加载images

Images and Containers

学习博客:https://www.jianshu.com/p/2a0aaf76734a?utm_campaign=hugo

首先我们要了解 Images 和 Containers的区别。

Image

镜像(Image)就是一堆只读层(read-only layer)的统一视角,下面的这张图能够帮助读者理解镜像的定义。

它包含了:

  • A cut-down OS
  • Third-party libraries
  • Application files
  • Environment variables

从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是Docker内部的实现细节,并且能够 在主机(运行Docker的机器)的文件系统上访问到。统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。 我们可以在图片的右边看到这个视角的形式。

Container

容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。

Container(容器)则像是我们开的虚拟机一样,它提供了:

  • An isolated environment
  • Can be stopped & restart
  • Container只是一个进程!但是它是一个特殊的进程,因为他有Image提供的自己的文件系统

要点:容器 = 镜像 + 可读层。并且容器的定义并没有提及是否要运行容器

综上我们可以这样来画出 Container和Images 的示意图:

如果我们在两个终端分别运行 ubuntu,会发现他们的ID是不一样的,而且它们所属的文件系统(容器)也是不一样的。因此它们互相独立,不能访问对方的文件

可以这样理解:容器是运行着的镜像

Sample Web Application

我们拿到一个React项目,要在一台电脑上运行,需要三步

  • Install Node 下载 Node.js
  • npm install 下载该项目的依赖
  • npm start 启动该项目

Dockerfile Instructions

启动一个Docker项目,首先就是要创建一个 Dockerfile, Dockerfile 包含了创建一个镜像的所有指令。

一个 Dockerfile 主要包含下面几种指令:

  • FROM 说明 base image,从基础镜像开始搭建
  • WORKDIR 项目工作的文件夹
  • COPY and ADD # to copy files/directories
  • RUN # to run commands
  • ENV # to set environment variable
  • EXPOSE # to document the port the container is listen
  • USER # to set the user running the app
  • CMD # to set the default command/pro
  • ENTRYPOINT # to set the default command/prog

Choosing the Right Base Image

在mac在某个文件夹打开终端可以用 alfred 中的 open iterm 命令。然后输入 code . 就可以使用vscode 打开此文件夹,这种工作流是比较快的。

我们在根目录下创建 Dockerfile 文件。

首先我们要确定该项目的 base image,选择一个正确的docker image 十分重要。

关于什么技术栈使用什么base image,我们可以上 https://docs.docker.com/samples/ 上找(我更喜欢dash 配合 alfred直接搜索)

比如说我想写一个关于 ASP.NET Core 的项目,我就可以点进第三个查看 ,里面有示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
# 注意,这里的代码网址可能一直在更新,因此我们要自己去找去问
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "aspnetapp.dll"]

在这个ReactJS 项目中,我们自然使用node,但是使用哪个版本的node十分重要,我们不要这样写FROM node:latest 因为这样会导致随时间推移版本号不一致,导致维护项目变得十分困难。

我们在dockerhub 上可以找到很多node image:

在中间一列,分别是操作系统和CPU架构。我们要选择当前CPU架构的版本进行下载。但事实上当我要下载这个image的时候,docker会自动帮我选择适合我电脑CPU的镜像版本。

为了能让我们的项目编译的更快,我们这里使用 alpine版本的node,因为只要几十M。

1
FROM node:14.16.0-alpine3.13

选择好 base image之后,我么可以编译项目了:

接下来,我们使用 docker run -it react-app 运行该项目,发现我们进入了node当中,可以直接运行js代码的那种。但这并不是我们想要的,我们希望的是进入一个shell或者bash,同时又有该node环境。为了实现这个效果我们可以将命令这样写:

docker run -it react-app sh代表进入shell,注意了这里不能将sh 替换成bash,因为alpine很小,没有bash。

Copying Files and Directories

COPYADD 的作用基本相同,只不过 ADD 相较于COPY多了一些功能:

当我们执行docker build -t react-app .命令的时候,docker客户端就会把这些内容交给docker engine去构建。docker engine 会一一执行 Dockerfile中的命令。但是Docker Engine 是没有权限访问当前文件下的其他文件的。因此我们要使用 COPY和ADD 指令。

COPY 顾名思义,就是将当前文件夹中的文件复制到我们创建的镜像当中去;ADD 则是将文件添加到镜像当中。

我们可以一一加入文件夹中的文件: COPY package.json /app/ ,其中 /app/ 是复制到的目的地,需要绝对路径

注意,如果要添加多个文件,需要注意大小写,且 /app/后面的斜杠一定要加

1
2
FROM node:14.16.0-alpine3.13
COPY package.json README.md /app/

当然也可以使用相对路径,这时候我们就要先用WORKDIR 来规定工作区域: 这里我规定 WORKDIR 是/app 那么接下来我使用命令 COPY . . 第一个 . 代表将文件夹下的所有内容都复制。第二个. 是目的地,即代表当前的文件夹/app

1
2
3
FROM node:14.16.0-alpine3.13
WORKDIR /app
COPY . .

此外,还可以用列表的形式来写 COPY 命令,但是不常用:

1
2
3
FROM node:14.16.0-alpine3.13
WORKDIR /app
COPY ["hello world.txt","."]

ADD 特有功能

  1. ADD 可以加入URL

比如说我有一个json文件是放在网络服务器上的,因此我就可以通过 ADD http://.../file.json . 将这个文件添加到镜像当中。

  1. ADD 可以添加压缩文件

ADD file.zip . 使用这个命令后,docker会自动解压这个压缩文件,并将其中的内容加到镜像文件当中

构建:

我们看到这个镜像一下子从二十多MB跳到一百多MB了,这是因为我们npm install 下载了大量node modules

Excluding Files and Directories

问题出现了,当我们的项目越做越大的时候,如果按照之前的方法,就要把几百兆的modules传给一个虚拟化的Linux机器,这样性能很低。 而且,这些依赖都已经记录在 package.json 中。因此我们没有必要将 node_moduls 拷贝到镜像当中,而是在linux中自己使用npm install安装

这样做的方法有两个好处:

  1. 可以大大缩小 build context 的体积
  2. 镜像构建速度也能提升很多。

那么怎么才能将特定的文件排除在外呢? 我们想到,在git中我们可以使用 .gitignore文件来屏蔽一些文件,那么在docker中,也有这样一个 .dockerignore的文件,用来屏蔽不需要添加到镜像中的文件。

我们看到,屏蔽了node_modules之后,构建镜像的速度变得很快,在app文件夹中也没有 node_modules文件夹了。

Running Commands

现在我们要在 dockerfile中添加RUN 命令,以便在构建的时候下载项目所需要的依赖。这样就不用构建完之后再手动npm install 了。

Setting Environment Variables

现在我们来设置 环境变量。比如说,当我们用前端和后端沟通的时候,使用环境变量可以为我们节省很多代码。

语法格式: ENV API_URL=http://api.myapp.com/

在启动镜像之后,首先我们可以用printenv来打印所有的环境变量

然后我们可以通过两种方法查询特定的环境变量

printenv API_URL 或者 echo $API_URL

1
2
3
4
/app # printenv API_URL
http://api.myapp.com/
/app # echo $API_URL
http://api.myapp.com/

Exposing Ports

我们需要为运行的程序设置一个端口,在Dockerfile中可以这么写:EXPOSE 3000

但是注意了,这只是将程序挂载到 container 中的3000端口,并不是我本地电脑的3000端口,虽然都是localhost

Setting the User

在默认情况下,Docker会以root用户来运行程序。虽然说root用户的权限最高,但是也会存在不少安全漏洞。因此,为了运行这个项目,我们需要创建一个普通用户,其权限比较低。

首先我们要创建一个组,addgroup app

然后我们创建一个 system user 并将其加入刚刚创建的组中 adduser -S -G app app ,这句命令的意思就是创建一个叫做app的user,并把它加入到名叫 app的组中。

我们还可以把这两个命令放在一个命令中: addgroup app && adduser -S -G app app

Dockerfile中,我们可以这样写创建用户、切换用户:

1
2
RUN addgroup app && adduser -S -G app app
USER app

我们重新编译之后,输入 whoami ,这时候用户已经切成了app.

ls -l 可以列出根目录下的一些文件信息,我们发现它们的所有者都是root,app用户是没有权限修改这些文件的,这就消除一些人修改文件的想法,增加了项目的安全性。

Defining Entrypoints

在terminal中直接启动项目

现在我们要不进入interactive模式,直接启动react-app,可以使用docker run react-app npm start ,其中跟在后面的 npm start 是启动后我们要在shell中输入的指令。

结果如下,我们发现我们并没有权限创建一个名为 .cache的文件。

这是因为在Dockerfile中,我设置和切换用户写在最后两行,但是前面的指令都是root来执行的,因此我们没有权限在root创建的文件夹中进行操作。要解决这个问题,修改Dockerfile的顺序即可:

1
2
3
4
5
6
7
8
FROM node:14.16.0-alpine3.13
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /app
COPY . .
RUN npm install
ENV API_URL=http://api.myapp.com/
EXPOSE 3000

在dockerfile中启动项目

在dockerfile通过命令启动项目有两种方式,一种是 CMD,另外一种是ENTRYPOINT。

CMD

支持三种格式:

  • CMD ["executable","param1","param2"]使用 exec 执行,推荐方式
  • CMD command param1 param2/bin/sh 中执行,提供给需要交互的应用;
  • CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;

指定启动容器时执行的命令,每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。如果用户启动容器时候指定了运行的命令,则会覆盖掉 CMD 指定的命令

ENTRYPOINT

两种格式:

ENTRYPOINT ["executable", "param1", "param2"] ,推荐方式

ENTRYPOINT command param1 param2(shell中执行)。

配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效

因此,当命令一定会被执行的时候,推荐使用ENTRYPOINT,因为其不会被覆盖。

Speeding Up Builds

我们看到对于这个小项目,m1的mac mini每次编译运行都要大概三四十秒的时间,还是比较慢的。这和docker的镜像机制有关。当Docker在编译一个项目的时候,每次执行一条指令就会新建一个只读层,这个只读层包含了这条指令修改了的文件。

因此我们要优化它。首先我们用docker history react-app来看一下image的各层:

我们看到下面没有COMMENT的半部分是 Base Image,也就是 我们创建的 node:14.16.0-alpine3.13 ,这几层创建的时间都是十几天前,说明创建一次之后,若没有改变就不用再创建了

然后,执行RUN addgroup app && adduser -S -G app app 命令后, image又加了一层,文件系统多了几个文件。但是新建过后,就不需要再改变了,我们看到这个修改时间是四个小时之前了。

接下来执行的是 USER appWORKDIR /app ,它们没有添加也没有减少文件,因此这层layer的大小是0B

但是COPY . . 每次都会将本地文件夹中的内容放到镜像中,因此每执行一次build,这一层就会新建一次。并且,当里面的一层只读层重建的话,建立在其上面的只读层都必须重建。因此在COPY 之后的指令都会创建一个新的镜像。

现在技巧性的东西来了,我们希望 npm install 这一步不要经常改变,因为每次下载node_modules需要非常多的时间,而且依赖并不是经常会修改的。因此我们可以先将 pakage.json文件加入镜像并执行npm install,然后再把其他的文件加入镜像。这样以后执行起来就会跳过 RUN npm install 这条指令。

1
2
3
4
5
6
7
8
9
10
FROM node:14.16.0-alpine3.13
RUN addgroup app && adduser -S -G app app
USER app
WORKDIR /app
COPY package*.json . # 先把依赖文件加进去
RUN npm install # 安装依赖,没有修改就跳过
COPY . . # 再将其他的文件加入,从这里开始,每次都新建
ENV API_URL=http://api.myapp.com/
EXPOSE 3000
CMD ["npm","start"]

在第一次执行的时候,build需要执行38.2s,但是在修改了文件而不修改依赖的情况下再次build只需要4.9s,这是因为npm install的那一层并不需要重新再建一次,而是在缓存中了。

Removing Images

我们用docker images查看,会发现很多没有名字,也没有Tag的镜像文件:

这些镜像文可能是临时镜像,没有最终build为一个最终的镜像,删掉即可。

我们可以使用:docker container prune 来删除已经停止多余的容器.

然后运行docker image prune来删除多余的镜像

删除了之后,我们再打开镜像列表,发现只有两个我们在使用的镜像了。

当我们想删除特定容器或者对象的时候,可以这样:

首先用docker ps -a 列出当前容器,然后:

docker rm -f <?containerid> 来删除特定ID的容器或者镜像

或者用 docker image rm <name> 来制定删除特定名字的镜像或者容器

Tagging Images

为了项目维护方便,我们要养成给 Images打标签的习惯,否则每个项目的标签都是latest,比较难区分。比如说 testing, staging 之类的

举例: docker build -t react-app:3.1.5 也就是创建image的时候就给他打一个标签。其中,repository的名字是react-app, 该image的tag是冒号后面的 3.1.5

删除tag: docker image remove react-app:1

这和git中的tag有些类似,我们要时刻更新latest 这个tag

比如现在我要将 lastest从上一个版本切换到现在tag为2的版本

可以用: docker image tag 94e react-app:latest 其中,945是Image ID的前三位

Sharing Images

我们在dockerhub上

然后我们把本地的image push上去, 首先为了管理方便我们再给第二版的项目打一个标签。

docker image tag 94e jasonxqh/react-app :2

然后利用docker push jasonxqh/react-app:2将第二版本的项目推送到docker hub上 ,推送之前记得先登录docker hub

Saving and Loading Images

如果我们不通过dockerhub,而是通过压缩文件的方法来保存images: docker image save -o react-app.tar jasonxqh/react-app:3 其中,前面的react-app.tar是压缩文件包的名字,后面的 jasonxqh/react-app:3是这个image的reference。

打开 .tar文件,里面保存的是一层一层的只读层

当我们在另一台电脑上要加载这个镜像的时候,可以:

docker image load -i react-app.tar

这样就把这个镜像加入进去了。

Summary

Working with Containers

Starting Containers

docker run react-app 可以让docker容器在前台运行:

docker run -d react-app 可以让docker容器在后台运行。

docker run -d --name blue-sky react-app 启动名为 react-app的docker镜像,并命名为 blue-sky

Viewing the Logs

现在我们在后台启动了这个容器,但是这对于我们来说相当于一个黑匣子,我并不清楚其中的运行信息。因此我们需要查看日志。

利用命令 docker logs <DockerID> 就可以查看该进程在后台打印的信息了:

此外,通过查询 docker logs --help 可以查询更多操作:

比如说:

dash logs -f ID 可以实时监控后台的打印信息,可以通过 Ctrl+C 来退出该模式

dash logs -n 5 ID 可以看到最后n条日志信息,这里是5条

dash logs -t ID 可查看各条日志信息的时间戳

Publishing Ports

之前我们说过,启动了container 之后,在本地访问localhost:3000 是没有效果的。因为在 Dockerfile 中,Exposing 3000只是在container所在的虚拟机中暴露3000端口。

为了做端口映射,我们可以用这条指令:

docker run -d -p 80:3000 --name c1 jasonxqh/react-app:3

其中, -d 代表detach;-p 代表 port, 这里将container中的3000端口和本地的80端口做映射; —name是启动的container的名字; 最后一个参数是要启动的镜像

Executing Commands in Running Containers

之前我们学了在dockerfile中利用CMD执行命令。现在我们要来看怎么在一个正在运行的容器中执行命令

可以利用 docker exec -it c1 sh 来对一个正在运作的容器执行命令:

其中 -it 代表 interactive (交互), c1是容器的名字, sh则是我们要打开的shell。

操作完之后,我们可以输入exit 退出shell,但是并不会使运行中的容器退出。

Stopping and Starting Containers

使用docker stop c1docker start c1来结束或者开始一个容器。

docker rundocker start 的区别就是前者是创建一个新的容器出来,后者是启动现有的容器

Removing Containers

我们用docker rm c1 是没有办法删除一个正在运行中的容器的,如果我们强制移除:docker rm -f c1

Containers File System

每一个容器都有独立的文件系统,在一个容器中创建的文件在另一个容器是访问不到的。

Persisting Data using Volumes

Volume是 在container之外的一片存储空间。是绕过container的文件系统,直接将数据写到host机器上,只是volume是被docker管理的,docker下所有的volume都在host机器上的指定目录下/var/lib/docker/volumes。

docker volume create app-data 是创建一个名叫app-data volume.

docker volume inspect app-data 是查看这个volume的基本信息,我们看到这个volume是放在 /var/lib/docker/volumes/app-data/_data 文件夹下的

接下来我们要把这个volume挂载到container中:

docker run -d -p 4000:3000 -v app-data:/app/data jasonxqh/react-app:3

如果volume是空的而container中的目录有内容,那么docker会将container目录中的内容拷贝到volume中,但是如果volume中已经有内容,则会将container中的目录覆盖

也就是说现在我们往container中的/app/data目录写文件是会保存在本地电脑上的。

但是,现在我们直接 echo something > sth.data ,是没有权限的,因为现在的用户是app,但是data的所有者是root。为此,我们要在dockerfile中事先以app用户的名义创建好data文件夹。

现在,权限问题解决了。

Copying Files between the Host and Containers

现在我们要把container中的文件拷贝到本地,或者把本地的文件拷贝到容器中去。

docker cp 776:/app/log.txt . 其中776:/app/log.txt 是ID为776的container/app文件夹下的目标文件, . 是复制到的目的地。这里是把容器中的log.txt拷贝到本地当前文件夹。

同样的,docker cp secret.txt 776:/app 就是把本地的 secret.txt文件拷贝到container中去。

Sharing the Source Code with a Container

当我们正式开始写项目的时候,我们不希望修改一点点东西就要新创建一个image,新开一个container,才能在网页上显示改变。这样太浪费时间了。

因此我们要做一个映射,就是在本地项目文件夹和docker的工作文件夹之间建立一个映射

我们可以用这条命令来启动,首先我们做端口映射,然后 -v 代表挂载外接盘,这里我们把 ${PWD}/app 做一个映射,也就是当我修改 当前目录文件夹的文件时,容器中的/app文件也会同步。

注意 PWD 代表当前的目录,这里一定要使用大写 ,否则是非法的。

docker run -d -p 5005:3000 -v ${PWD}:/app react-app

Running Multi-container Applications

Installing Docker Compose

前面我们使用 Docker 的时候,定义 Dockerfile 文件,然后使用 docker build、docker run 等命令操作容器。然而微服务架构的应用系统一般包含若干个微服务,每个微服务一般都会部署多个实例,如果每个微服务都要手动启停,那么效率之低,维护量之大可想而知

使用 Docker Compose 可以轻松、高效的管理容器,它是一个用于定义和运行多容器 Docker 的应用程序工具mac和windows用户,当我们下载了Docker Desktop之后就自动安装了Docker Compose

Cleaning Up our Workspace

docker container rm -f $(docker container ls -aq) 可以删除所有状态下的容器

docker image rm -f $(docker image ls -aq) 可以删除所有状态下的 Image

其中-aq 中的a代表all, -q 则代表获取所有对象的ID。

The Sample Web Application

这是一个有前端和后端的Web项目:

按正常的方法,我们需要分别进入前后端文件夹并使用npm run start 但是如果我们使用了 docker compose,编写了yml文件,我们就可以直接输入docker-compose up让项目在docker容器中运行。

bug

这里我遇到了一个比较难发现的bug,那就是我一直无法启动 ./docker-entrypoint.sh 这个文件。

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

echo "Waiting for MongoDB to start..."
./wait-for db:27017

echo "Migrating the databse..."
npm run db:up

echo "Starting the server..."
npm start

当后端启动,我们再调用这个文件,就能在container中运行shell命令。

但是,我这个文件是本地创建的,创建时并没有像app用户开放执行权限,导致我docker-compose up 的时候始终报:Permission denied。 因此,我们要在运行docker-compose up 前手动修改docker-entrypoint.sh 的权限:

chmod a+x docker-entrypoint.sh 修改执行权限,再执行docker-compose up 之后,bug解决。

JSON and YAML Formats

现在我们就来介绍一下YAML格式以及它和JSON格式的区别。

首先我们看以下JSON格式的文件:JSON格式就是键值对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "vidly-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.21.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --colors",
"eject": "react-scripts eject"
},
}

YAML的语法和其他高级语言类似,并且可以简单表达清单、散列表,标量等数据形态。它使用空白符号缩进和大量依赖外观的特色,特别适合用来表达或编辑数据结构、各种配置文件、倾印调试内容、文件大纲(例如:许多电子邮件标题格式和YAML非常接近)。那么YAML格式长啥样呢?下面是 docker-compose.yml 文件中的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
version: "3.8"

services:
frontend:
depends_on:
- backend
build: ./frontend
ports:
- 3000:3000

backend:
depends_on:
- db
build: ./backend
ports:
- 3001:3001
environment:
DB_URL: mongodb://db/vidly
command: ./docker-entrypoint.sh

db:
image: mongo:4.0-xenial
ports:
- 27017:27017
volumes:
- vidly:/data/db

volumes:
vidly:

我们发现,YAML其实就是JSON格式去掉所有的大括号和引号,并用缩进来表示从属关系。

在一个冒号下的多个数值的列举,使用 - 使条理清晰

那么YAML可读性比JSON高的话,为什么我们步一直使用YAML呢?因为解析YAML格式的文件要比解析JSON格式的文件更慢一些,所以说各有利弊

接下来这章,我们就来复盘一下这样一份 docker-compose.yml 是怎么写出来的

Creating a Compose File

首先,我们新建一个 docker-compose.yml 文件。

version字段

在文件的开始,我们要确定compose版本:

在: https://docs.docker.com/compose/compose-file/ 中,我们可以查到compose file与Docker Engine对应的表格,按照上面写即可:

这里我们使用3.8版本:

Version: "3.8"

services字段

然后我们要确定services字段:这个字段告诉Docker,它要创建哪些container,比如frontend,backend,db,这些服务的名字我们可以乱取,Docker会依次为其创建容器。但是为了简洁起见,我们还是将其命名为frontend,backend,db三个服务,分别代表前端,后端,数据库

1
2
3
4
5
6
7
8
version: "3.8"

services:
frontend:

backend:

db:
frontend

在前端服务中,我们首先要运行frontend文件夹中的 dockerfile 文件。我们可以这么写:

1
2
frontend:
build: ./frontend

接下来我们做端口映射,也就是将容器中的端口映射到本地。这里虽然我们只做一个端口映射,但是考虑到可能有多重端口映射,因此这里使用 - 来列举

1
2
3
4
frontend:
build: ./frontend
ports:
- 3000:3000
backend

和frontend一样,这里我们也要定义build属性,它将执行 backend文件夹下的dockerfile文件

1
2
backend: 
build: ./backend

和frontend一样,后端服务也需要端口映射:

1
2
3
4
backend: 
build: ./backend
ports:
- 3001:3001

下面一个属性是environment,这是后端服务所需要

然后,我们要做一个volume映射,来告诉后端数据库在哪里.这个关键变量其实就是一个Mongodb的Connection URL —— mongodb://db/vidly ,db是一个host(因为按照YAML创建的container中的host名与service的名字一样),因此,这里是db; /vidly代表这个db数据库下的一张数据表.

1
2
3
4
5
6
7
8
backend: 
depends_on:
- db
build: ./backend
ports:
- 3001:3001
environment:
DB_URL: mongodb://db/vidly
db

这是一个数据库服务,因为我们本地没有相关的文件,所以就要用为其规定image属性,比如说这里的 mongo:4.0-xenial ,这是一个ubuntu 版本的mongodb,比windows下的mongodb要轻量化许多。然后同样做一个端口映射

1
2
3
4
db:
image: mongo:4.0-xenial
ports:
- 27017:27017

通常情况下,一个容器启动之后,所有容器中的数据都存在容器内部的临时文件中,如果容器停止,则数据也就清空了,为了能够在使用容器的过程中,还能把一些数据持久化下来,也即容器消失掉,这些数据依然还存在,因此dockercompose支持了数据卷(volume)功能,通过他可以指定Docker中一块持久化的区域,该区域在容器消失之后,还可以依然将区域中的数据保存下来。

相当于这部分区域不在属于某一个容器了,而是由dockercompose管理的一部分区域,只要通过compose启动容器,这部分区域就一直会存在

这里我们就要定义一个持久化的数据卷:左边的是主机目录,右边的是容器目录。也就是说,如果项目往容器数据库中写数据的话,那么这些数据会保存在主机上不会消失。

1
2
3
4
5
6
db: 
image: mongo:4.0-xenial
ports:
- 27017:27017
volumes:
- vidly:/data/db

volumes:

上面我们使用了一个数据卷 ,但我们还没有定义,因此接下来我们要定义它。

1
2
volumes:
vidly:

Building Images

之前我们说了 Docker-Compose 是建立在 Docker-engine之上的,因此docker engine能完成的工作(build,run,listening等),docker-compose都能够完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Commands:
build Build or rebuild services
config Validate and view the Compose file
create Create services
down Stop and remove resources
events Receive real time events from containers
exec Execute a command in a running container
help Get help on a command
images List images
kill Kill containers
logs View output from containers
pause Pause services
port Print the public port for a port binding
ps List containers
pull Pull service images
push Push service images
restart Restart services
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
top Display the running processes
unpause Unpause services
up Create and start containers
version Show version information and quit

现在我们来学习如何用 build 指令, build就是用来构建刚才我们写的服务的。build完成后service会以 image的形式存在。

当我们想不借助cache,重新编译的时候,可以使用 docker-compose build --no-cache 但是显然这样会比较慢

Starting and Stopping the Application

之前学的 docker-compose up 是将 build image,run container一步到位了。如果我们想重新build一遍,并运行起来,可以使用 docker-compose up --build ;如果我们想要其在后台运行,可以使用docker-compose up -d

使用docker-compose运行起来的容器可以使用 docker-compose ps 一览

当我们想要关掉后台运行中的的容器时,可以 docker-compose down

Docker Networking

当我们用 docker-compose 启用一个项目的时候,Docker Compose会自动创建一个关系网络并将containers加到这个网络上,一遍这几个网络可以互相通讯。

我们可以使用docker network ls 来查看这几个container 的关系网:

然后我用 root 用户登录backend(否则没有ping的权限),然后ping 本地的frontend地址,就可以发现它们是可以互相通讯的。

这也是为什么backend container能够通过 connection url与mongodb container 通讯了。

Viewing Logs

使用docker-compose logs 可以查看后端程序日志信息,和docker logs 一样,也可以加上 -f,-t 用来跟踪日志或者打印时间戳。

Publishing Changes

和单个container一样,我们不希望修改一点代码就要rebuild整个项目,那样太麻烦了。因此我们要降本的文件夹和container中的/app 文件夹做映射,而且要比之前的利用绝对路径$(PWD)做映射更简单。比如说,我要把 本地的backend文件夹和/app文件夹做映射。只要 ./backend:/app 即可

但是这有一个bug,就是后端会报一个错误,也就是 nodemon:not found

我们知道nodemon是一个很有用的包,它会监测文件的修改并实时渲染。但是,我们只在container中装载了node_modules文件夹而本地的backend目录下并没有node_modules文件夹。因此无法实现监测。

为了解决这个问题,我们可以在本地也安装node_modules 文件夹。这样再次启动前后端,就能实现同步更新了。

Migrating the Database

如果要在一个新的电脑上运行这个项目,我们势必要把数据插入到新的mongodb当中去。因此这就需要用到数据迁移。在nodejs中有个很好用的工具,可以用来迁移mongodb数据库:migrate-mongo

使用了这个工具,我们可以创建 database-migration scripts 如下:

这个文件存放在 migrations文件夹中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
async up(db, client) {
await db
.collection("movies")
.insertMany([
{ title: "Avatar" },
{ title: "Star Wars" },
{ title: "Terminator" },
{ title: "Titanic" },
]);
},

async down(db, client) {
await db.collection("movies").deleteMany({
title: {
$in: ["Avatar", "Star Wars", "Terminator", "Titanic"],
},
});
},
};

这里有两个模块:up函数是往database的movies数据表中中添加数据,down函数是在database删除数据。这两个函数式异步的

要启动这个脚本文件,可以 migrate-mongo up。因为在数据库中有变更日志,所以migrate-mongo 模块并不会重复执行一个脚本,插入重复的数据。

在package.json 中,我们看到可以用npm run db:up来简化migrate-mongo up

介绍完database migration之后,我们希望在启动项目的一开始先执行数据库迁移,在后端的Dockerfile中,我们只写了 CMD ["npm","start"] , 这是不够的,我们可在docker-compose 中定义 command字段来重写要执行的操作。

1
2
3
backend: 
#...
command: migrate-mongo up && npm start

但是这样写有个问题,因为当我执行migrate-mongo up 的时候, 我的db service可能还没有启动,这样就会报错了。为解决这个问题,需要在启动命令前增加判断依赖服务状态的工具,主要有三种:wait-for-it,dockerize,waitfor在这我使用./wait-for :

1
2
3
backend: 
#...
command: ./wait-for db:27017 && migrate-mongo up && npm start

其中./wait-for后面的第一个参数是service 的名字,第二个参数是其对应的端口.

虽然写是写好了,但是这样的command未免有点不美观,因此,我们可以新建一个docker-entrypoint.sh的可执行文件,然后用command来执行它。

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

echo "Waiting for MongoDB to start..."
./wait-for db:27017 # 首先等待db service 启动

echo "Migrating the databse..."
npm run db:up # 进行数据迁移操作

echo "Starting the server..."
npm start # 最后启动backend server

把command字段改成执行docker-entrypoint.sh 即可

1
2
3
backend: 
#...
command: ./docker-entrypoint.sh

最后我们来设置一下服务器的顺序启动问题,比如说先启动db服务,再启动backend服务,最后启动frontend服务,但是注意这只能保证容器进入了running状态,而不保证进入 ready状态,所以说我们要将depends_on和wait-for搭配使用。可以设置 depends_on字段来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3.8"

services:
frontend:
depends_on:
- backend
#...


backend:
depends_on:
- db
#...

db:
#...

Summary

这一节我们主要讲了这几个命令以及它们的延伸。

1
2
3
4
5
6
7
8
9
docker-compose build
docker-compose build --no-cached
ocker-compose up
docker-compose up -d
docker-compose up —build
docker-compose down
docker-compose ps
docker-compose logs
Docker Compose commands

Deploying Applications

这一章节我们来讲一下如何发布一个应用

Deployment Options

对于一个项目,有两种部署的方式,其一可以部署到一个服务器上(single-host deployment),也可以部署到一个服务器集群上(Cluster deployment)

只部署到一个服务器上,虽然操作比较简单,但是一旦服务器宕机,我们的项目也就失效了。而且当应用的瞬时流量较大时,单个服务器可能承受不了这样的压力。

如果选择cluster deployment 这种方式,这需要编制软件(orchestration tools)帮忙,主流orchestration tools有: docker swarmkubernetes(俗称k8s) ,但是部署的逻辑较为复杂,因此这章还是使用single-host deployment 这种方式。

Getting a Virtual Private Server

VPS提供商有很多,比如说:

  • Digital Ocean
  • Google Cloud Platform(GCP)
  • Microsoft Azure
  • Amazon Web Services(AWS)

一般来说租个VPS都是要花钱的,我这里使用Oracle Virtualbox 来演示

Installing Docker Machine

首先我们需要在项目文件夹中安装 Docker Machine。Docker Machine 是一种可以让您在虚拟主机上安装 Docker 的工具,并可以使用 docker-machine 命令来管理主机。Docker Machine 也可以集中管理所有的 docker 主机,比如快速的给 100 台服务器安装上 docker。

可以在这里下载:https://github.com/docker/machine/releases

我们使用的是mac os,那就复制这段连接:

1
2
curl -L https://github.com/docker/machine/releases/download/v0.16.2/docker-machine-`uname -s`-`uname -m` >/usr/local/bin/docker-machine && \
chmod +x /usr/local/bin/docker-machine

当然,我们下载了homebrew,可直接用 brew install docker docker-machine

Provisioning a Host

现在我们要用docker machine 创建一个虚拟机。docker-machine支持的虚拟机提供服务如下:

https://docs.docker.com/machine/drivers/

Connecting to the Host

Defining the Production Configuration

Reducing the Image Size

Deploying the Application

Troubleshooting Deployment Issues

Publishing Changes

Course Wrap Up

-------------本文结束,感谢您的阅读-------------