官网getting-started的教程:Getting Started

由于我在自己的Mac上已经安装了Docker Desktop for Mac,并且已经按照安装后的指导,从GitHub下载了getting-started源码、编译了镜像并上传到了自己的Docker Hub,并启动了容器,所以可以在浏览器上直接输入http://localhost/tutorial/开启教程

对于一些单个字符的参数设置,可以进行合并以简化整个命令,比如 docker run -d -p 80:80 docker/getting-started可以简化成docker run -dp 80:80 docker/getting-started

开始

编译镜像

下载项目后解压,添加Dockerfile,编译

docker build -t getting-started .

Mac版的Desktop安装了Docker Engine, Docker CLI client, Docker Compose, Notary, Kubernetes, 和 Credential Helper.

Mac版的Docker,Docker daemon是运行在一个轻量级的Linux VM之上的,Mac版Docker通过对外提供daemon和API的方式与Mac环境实现无缝集成,这意味着可以直接使用Mac自带的终端来使用docker命令

编译完成后可以通过docker image ls命令查看当前的镜像

REPOSITORY                TAG                 IMAGE ID            CREATED             SIZE
getting-started           latest              3bb6f512366f        9 minutes ago       179MB
docker101tutorial         latest              de96b07c686e        24 hours ago        27.3MB
cyc97/docker101tutorial   latest              de96b07c686e        24 hours ago        27.3MB
<none>                    <none>              5ed322bc3f46        24 hours ago        85.6MB
<none>                    <none>              3016bdb0d25c        24 hours ago        72MB
<none>                    <none>              fc1d422e512b        24 hours ago        224MB
nginx                     alpine              6f715d38cfe0        9 days ago          22.1MB
python                    alpine              44fceb565b2a        11 days ago         42.7MB
node                      12-alpine           18f4bc975732        3 weeks ago         89.3MB

启动容器

使用命令docker run -dp 3000:3000 getting-started将Docker主机的3000端口与容器的3000端口进行映射,则外部主机可以通过3000端口来访问容器。

运行docker container ls查看当前容器情况:

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
dfceb09f33fd        getting-started     "docker-entrypoint.s…"   12 seconds ago      Up 11 seconds       0.0.0.0:3000->3000/tcp   sleepy_brattain
7ec741ea53c2        docker101tutorial   "/docker-entrypoint.…"   24 hours ago        Up About an hour    0.0.0.0:80->80/tcp       docker-tutorial

测试

打开http://localhost:3000,然后可以查看当前应用的界面,在这个todo应用中,可以增删item

请输入图片描述

更新应用

按照教程修改源码的空提示文案后,重新编译镜像,然后要先删除原容器,才能启动新容易,避免因为占有相同的3000端口而报错。

通过命令行删除原容器

删除之前要先停止容器,这样可以给容器中运行的应用/进程一个停止运行并清理残留数据的机会,内部使用的是Linux的信号量SIGTERM和SIGKILL。

使用docker ps获取容器ID,然后使用docker stop the-container-id停止容器,然后docker rm the-container-id删除原容器。

可以通过添加force标记使得在一行命令中完成暂停和删除容易的操作,即: docker rm -f the-container-id

推送应用到Docker Hub

Docker镜像存储在镜像仓库服务(Image Registry)当中,Docker客户端的镜像仓库服务是可配置的,默认使用Docker Hub。

登录Docker Hub

在终端中使用docker login -u YOUR-USER-NAME来登录

重命名镜像

在推送前,需要对现有的镜像进行重命名,因为最终推送的命令是:docker push YOUR-USER-NAME/getting-started,但是docker在本地只有getting-started镜像,所以会找不到,因此需要使用tag命令来重命名:

docker tag getting-started YOUR-USER-NAME/getting-started

推送

我的仓库namespace是cyc97,因此最后使用的推送命令如下:

docker push cyc97/getting-started

默认会打上latest的TAG,也可以指定其他的TAG:docker push cyc97/getting-started:latest

推送成功后会有如下输出,同时可以在浏览器登入Docker Hub查看相应的repo下是否有对应镜像

latest: digest: sha256:7b9ea56ea1ab6bf46aff7ad0b8ada8100bc6510904b1d7ddd533bb3cdccacf1c size: 1788

在另外一个Docker主机上运行

这里可以使用PWD(Play with Docker),不过我点Login一直没有反应。。。

数据的持久化

每个Docker容器都有自己的非持久化存储,非持久化存储自动创建,从属于容器,生命周期和容器相同,这意味着删除容器就会删除全部的非持久化数据。

即使是使用相同镜像的容器,在一个容器中对于非持久化的数据修改,也不会反应到其他的容器中。

Volume卷

Docker中卷属于一等公民。因此可以先创建卷,然后创建容器,然后将卷挂在到容器的一个目录上,所有这个目录内文件的改动,都会反应到主机的卷中,即使删除容器,卷依然存在。

这个示例todo应用使用SQLite Database 来存储数据,文件位于 /etc/todos/todo.db

  • 使用docker volume create todo-db创建卷
  • 暂停并删除原容器
  • 创建新的容器并挂载卷docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started

尽管命名卷和绑定挂载Bind Mounts(我们将在稍后讨论)是默认Docker引擎安装支持的两种主要卷类型,但是有许多可用的卷驱动器插件来支持NFS,SFTP,NetApp等! 一旦开始在具有Swarm,Kubernetes等集群环境中的多个主机上运行容器,这将尤其重要。

使用docker volume inspect volume_name | grep Mount命令可以查看卷位于Docker主机的什么位置。

使用绑定挂载 Bind Mounts

项目的源码经常需要改动,在前面更新空文案的步骤中,需要重新编译镜像,这会很耗时。

使用绑定挂载 Bind Mounts,我们可以控制主机上的确切挂载点。 我们可以使用它来持久化数据,但是通常用于向容器中提供其他数据。 在处理应用程序时,我们可以使用Bind Mounts将源代码挂载到容器中,以使其查看代码更改,做出响应并立即查看更改。

对于基于Node的应用程序,nodemon是监视文件更改然后重新启动应用程序的好工具。 大多数其他语言和框架都有等效的工具。

除了Docker引擎中默认的命名卷Volume和绑定挂载,还有很多其他的卷驱动 (SFTP, Ceph, NetApp, S3, and more).

| | Named Volumes | Bind Mounts |
| - | - | - |
| Host Location | Docker chooses | You control |
| Mount Example (using-v) | my-volume:/usr/local/data | /path/to/data:/usr/local/data |
| Populates new volume with container contents | Yes | No |
| Supports Volume Drivers | Yes | No |

开启一个开发模式的容器

启动一个支持开发工作流程的容器,有如下步骤:

  • 将我们的源代码挂载到容器内
  • 安装所有的依赖,包括 dev模式下的依赖
  • 开启nodemon工具来监视文件系统的变化

在删除原getting-started容器后,使用下面的命令创建一个新的容器,并使用 Bind Mount,

docker run -dp 3000:3000 \
    -w /app -v "$(pwd):/app" \
    node:12-alpine \
    sh -c "yarn install && yarn run dev"
  • -w /app设置当前命令运行所于的工作目录或者当前目录
  • -v "$(pwd):/app" 将容器中的当前目录绑定挂载到/ app目录中
  • node:12-alpine使用的镜像。注意这里是来自于Dockfile中的,整个app的基础镜像。
  • sh -c "yarn install && yarn run dev"要运行的命令。使用sh开启一个shell,运行yarn install来安装所有的依赖,然后运行yarn run dev,如果查看package.json文件,就会发现dev脚本就是启动nodemon

通过使用docker logs -f container-id查看相应log,来确认操作正确。然后可以使用Ctrl+C退出。

这时候修改src/static/js/app.js中的内容,保存后,直接刷新浏览器,就可以立即看到相应的修改。

多容器的应用

通过使用Docker Compose,能够在Docker节点上,以单引擎模式(Single-Engine Mode)进行多容器应用的部署和管理。Docker Stack则可以Swarm模式对Docker节点上的多容器应用进行部署和管理,后续会专门写一篇使用Docker Stack部署的文章。

使用Docker Compose

由于使用的是Docker Desktop,所以已经安装好Docker Compose工具了,我们需要定义一个YAML文件,Docker Compose会解析该文件,并通过Docker API进行应用的部署和管理。

新建docker-compose.yml文件,由于Docker Engine版本是19.03.0+,所以可以使用Compose文件的版本为3.8,内容如下:

version: "3.8"

services:
  app:
    image: node:12-alpine
    command: sh -c "yarn install && yarn run dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
    environment:
      MYSQL_HOST: mysql
      MYSQL_USER: root
      MYSQL_PASSWORD: secret
      MYSQL_DB: todos

  mysql:
    image: mysql:5.7
    volumes:
      - todo-mysql-data:/var/lib/mysql
    environment: 
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: todos

volumes:
  todo-mysql-data:

其中定义了app和mysql两个服务,并指定了镜像、卷、环境变量等配置,compose全配置参数可以参考:Compose file reference

这里没有指定网络networks,默认Docker Compose会创建bridge网络,这是一种单主机网络,只能够实现同一主机上容器的连接。后续文章在使用Swarm模式时,将指定使用overlay驱动,来跨主机连接容器。

Volume卷可以先创建好,也可以在一级配置参数中指明,Docker Compose会自动创建,网络也是一样。

当应用程序启动时,它实际上会坐下来等待MySQL启动并准备就绪,然后再尝试连接它。 Docker没有任何内置支持来等待另一个容器完全启动,运行并准备就绪,然后再启动另一个容器。 对于基于Node的项目,可以使用 wait-port依赖项。 对于其他语言/框架也存在类似的项目。

其他docker-compose 命令

  • docker-compose down 停止和关闭应用,会删除网络,卷Volume和拉取的镜像都会保留在系统中,
  • docker-compose restart 可以重启应用
  • docker-compose ps 查看应用状态
  • docker-compose rm 对于已经停止的Compose应用,可以用该命令删除应用,这会删除相关的容器和网络

这里YAML文件中定义的一个Service服务,就对应着一个容器。

编译镜像的最佳实践

镜像是由多个镜像层构成,Docker的镜像编译过程用到了缓存机制。Dockerfile中的FROM、COPY、RUN指令都会生成一个镜像层,由于一层发生变化后,下面的所有层都需要重新编译,所以在编写Dockerfile时,应该尽量将易于发生变化的指令置于Dockerfile文件的后方执行。这意味着缓存未命中的情况将直到编译的后期才会出现。

在使用之前的Dockerfile编译镜像后,当我们更改镜像时,必须重新安装yarn依赖项。这是毫无意义且费时的操作。

充分利用镜像缓存

要解决此问题,我们需要重组Dockerfile来帮助支持依赖项的缓存。对于基于Node的应用程序,那些依赖项在package.json文件中定义。

因此,如果我们仅先复制该文件,安装依赖项,然后再复制其他所有内容,然后,仅当package.json发生更改时,我们才重新创建yarn依赖项的镜像层。

原来的Dockerfile内容:

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

修改后的:

FROM node:12-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]

这时当我们修改了项目源码,比如src/static/index.html中添加一个<title> 标签,然后重新编译镜像时,由于将COPY其他文件的步骤放到了最后,所以前序步骤都可以利用编译的镜像层缓存,如下:

Sending build context to Docker daemon  4.689MB
Step 1/6 : FROM node:12-alpine
 ---> 18f4bc975732
Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> d3540a5567d2
Step 3/6 : COPY package.json yarn.lock ./
 ---> Using cache
 ---> c92e80b3a8c9
Step 4/6 : RUN yarn install --production
 ---> Using cache
 ---> 1389c2804feb
Step 5/6 : COPY . .
 ---> 85cde901aa32
Step 6/6 : CMD ["node", "src/index.js"]
 ---> Running in fa61534d1afd
Removing intermediate container fa61534d1afd
 ---> c295411dde1a
Successfully built c295411dde1a
Successfully tagged getting-started:latest

.dockerignore文件

创建一个.dockerignore文件,.dockerignore文件是有选择地仅复制与镜像相关的文件的简便方法。更多介绍可以参考.dockerignore file

文件内容是要忽略复制到镜像中的文件,比如这里添加了node_modules文件夹,那么在这种情况下,应该在第二个COPY步骤中省略node_modules文件夹,因为否则,它可能会覆盖由RUN步骤中的命令创建的文件。有关为什么这是对Node.js应用程序的推荐做法和其他最佳实践的更多详细信息,请参阅Dockerizing a Node.js web app.

生产环境中的多阶段编译(Multi-Stage Builds)

从Docker 17.05开始,支持多阶段编译。通过在一个Dockerfile文件中使用多个FROM指令来构成多个阶段的编译,最终生产环境的镜像只需要从前序编译阶段拷贝一些文件即可,这可以极大的减少最终镜像的大小。

比如对于基于Java的应用,需要使用JDK将源码编译成字节码,但是整个JDK是不需要包含到最终的镜像中的。同样,使用Maven或者Gradle工具来编译的应用,这些拉取的Maven和Gradle镜像本身是不需要包含到最终的生产镜像中的。

多阶段编译有几个优点:

  • 将编译时依赖项与运行时依赖项分开
  • 通过仅运送应用程序需要运行的内容来减小整体镜像大小

下面是一个使用React来多阶段编译镜像的例子:

FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

在第一个编译阶段,起了一个别名 build,在第二个编译阶段从build中拷贝需要的文件,这样就不会把体积很大的node镜像添加到最终镜像中。

关于这个getting-started项目的Dockerfile如何修改成多阶段构建的方式,我试了一下没有成功,主要是对node 项目和yarn都不熟悉,如果有了解的同学可以评论告诉我,

如果觉得我的文章对你有用,请随意赞赏