容器化应用 Containerize an application

  • 拉取远程仓库代码
    git clone https://github.com/docker/getting-started-app.git

  • 构建 app 镜像

    • A Dockerfile is simply a text-based file with no file extension that contains a script of instructions.

    • touch Dockerfile in app directory

    • In Dockerfile

      # syntax=docker/dockerfile:1
      FROM node:18-alpine
      WORKDIR /app
      COPY . .
      RUN yarn install --production
      CMD ["node", "src/index.js"]
      EXPOSE 3000
      
    • docker build -t getting-started .

  • 运行 app 容器

    • docker run -dp 127.0.0.1:3000:3000 getting-started

      • -d 表示后运行,-p 表示端口映射(HOST:CONTAINER)
      • docker run 执行完后可以用 3000 端口访问到本地/远程服务器上的应用
  • 查看容器镜像列表 - docker ps

更新已部署的容器 Update the application

  • 移除原先的旧容器

    • 暂停旧的容器 docker stop <the-container-id>

    • 清楚旧的容器 docker rm <the-container-id>

    • 合并上述两条命令一起执行为:docker rm -f <the-container-id>

  • 重启已更新的新容器

    • docker run -dp 127.0.0.1:3000:3000 getting-started

分享容器镜像 Share the application

  • Before, you have to use a Docker registry in Docker Hub.(类似于 GitHub、Gitee 中的个人仓库)

  • 向远程仓库推送镜像 PUSH the image

    • docker push YOUR-USER-NAME/getting-started 这意味着 docker 会在 image ls 中查找名叫 YOUR-USER-NAME/getting-started:tagname 的镜像;

    • tagname默认为 latest,即最新的;

    • 若找到,则可以推送且覆盖原仓库的 YOUR-USER-NAME/getting-started:tagname 的镜像;

    • 若找不到,则需要 tag to give it another name.,实际上就是起一个 alias,这样就可以通过别名去引用它,即被标签(tagged)的镜像实际上是同一份数据的引用

    • docker tag getting-started YOUR-USER-NAME/getting-started:tagname

    • docker push YOUR-USER-NAME/getting-started:tagname

    • 至此,镜像推送成功,可查看自己的 Docker Hub 仓库验证。

  • 线上感受镜像拉取部署 Run the image on a new instance

    • 浏览器打开 Play with Docker

    • 登录并且从下拉菜单中选择 docker;

    • 或用你的 Docker Hub 注册并且点击开始;

    • click ADD NEW INSTANCE button on the left side bar and you should see the following image

    • 在终端输入一下命令,启动你新推送的镜像:
      docker run -dp 0.0.0.0:3000:3000 YOUR-USER-NAME/getting-started
      你应该看到镜像成功被拉取,并成功跑起来了

    • click OPEN PORT button and specify 3000 as the port.

    • happy~

持久化数据 Persist the DB

1、容器的文件系统隔离于其他容器 - Any changes won't be seen in another container, even if they're using the same image.

2、docker 中的 volumes 有两种常用的文件系统挂载方式 - Volume mounts 和 Bind mounts,此章节介绍 Volume mounts 且仅仅介绍了在一个容器中使用 SQLite 存储数据的案例,后续章节会讲到 MySQL 容器存储其他容器数据的情况

3、A volume mount is a great choice when you need somewhere persistent to store your application data.

  • Volumes 提供了将容器文件系统连接回主机文件系统的能力,这使得容器数据可以被主机文件系统持久化,并且可以在容器重启时保持数据。

  • Docker 全权管理 volume mount,包括在磁盘的存储位置,你仅仅需要记住 the volume 的命名;

  • 创建 volume、使用 volume mounts、启用一个容器

    • 创建一个 volume docker volume create todo-db

    • 启动一个容器,添加 --mount 指定一个 volume mount(Give the volume a name, and mount it to /etc/todos in the container, which captures all files created at the path.)

    • docker run -dp 127.0.0.1:3000:3000 --mount type=volume,src=todo-db,target=/etc/todos getting-started

    • 容器跑起来后,Docker 会自动管理名为 todo-db 的 volume,并将其挂载到容器的/etc/todos 路径下,这样,在 host machine 上会捕获容器/etc/todos 创建的所有文件,即在容器中创建的数据文件会被反映到 host machine 中 Docker 管理文件的路径中。

  • Where is Docker storing my data when I use a volume?

    • docker volume inspect command can help you.

自定义目录映射 Use bind mounts

1、you can develop your app more efficiently using bind mounts.

2、此章节介绍 Bind mounts,你可以挂载代码目录到容器内部,当你修改并保存文件时,容器会监听到变化且回应你(容器中运行了一个进程来监听文件系统的变化)

  • volume type comparisons

  • development container:你可以将自己的项目目录挂载到一个容器中,在容器中可以安装项目依赖和构建工具.....而不需要在本地安装。

  • 开启一个开发容器

    • -w 设置容器中的工作目录;

    • sh -c 使用 sh (shell 脚本) 跑 yarn 命令(centos 中使用 bash);

    • docker logs -f <container-id> 观察容器日志;

    • 项目开发完毕, Ctrl+C 退出;

    • 关闭开发容器并构建新镜像 docker build -t YOUR-IMAGE-NAME .

为什么 bind mounts 的容器可以协助开发:host source direction 改变,引起 container work direction 改变,yarn run dev 实际上是根据 package.json 的 "script" 启动了 nodemon 监听 node 项目文件变化,当 /app(work direction) 变化时,nodemon restart src/index.js,并输出 logs。

个人觉得这个开发容器的用法于 vite 构建工具类似,如果要用 Docker 开发容器的流程解释,也很容易让人理解。大胆猜测,在开发服务器中,服务器会把源文件 copy 一份到自己的工作目录下,使用 node --watch 监听整个目录下的文件变化(这与 bind mounts 目录映射一样),源文件变化时,开发服务器会接受到通知并更新(开发容器也会这么做),开发服务器会将现有的代码文件打包,相同文件用缓存,不同则更新原有文件,然后将 build 的文件部署在开发服务器上,提供一个 5173 端口给开发者在网页中打开。以上解释为个人浅薄理解。

容器通信 Multi container apps

1、MySQL 容器存储其他容器产生的数据

2、Run the container separately

  • 容器间通信,关键在网络 networking

  • 以下两种都可以将一个容器放到一个网络:

    • 当启动新容器时分配一个网络
    • 将一个正在运行的容器连接到一个网络
  • 建立一个在 todo-app 网络下运行的 MySQL 容器

    • 创建网络 docker network create todo-app(networking name)

    • docker run MySQL container

      docker run -d \
      --network todo-app --network-alias mysql \
      -v todo-mysql-data:/var/lib/mysql \
      -e MYSQL_ROOT_PASSWORD=secret \
      -e MYSQL_DATABASE=todos \
      mysql:8.0
      
    • --network-alias 网络别名,用于在 todo-app 网络下连接和访问这个 MySQL 容器

    • docker exec -it <mysql-container-id> mysql -u root -p 进入数据库容器且可以用 MySQL 命令操作(例如 mysql -u root -pmysql>>> SHOW DATABASES;mysql>>> exit

  • deployed Nicolaka/netshoot container debug the MySQL container

  • 启动一个连接 MySQL 存储数据 的容器

    • docker run node 项目

      docker run -dp 127.0.0.1:3000:3000 \
      -w /app -v "$(pwd):/app" \
      --network todo-app \
      -e MYSQL_HOST=mysql \
      -e MYSQL_USER=root \
      -e MYSQL_PASSWORD=secret \
      -e MYSQL_DB=todos \
      node:18-alpine \
      sh -c "yarn install && yarn run dev"
      
    • MYSQL_HOST=mysql 此处的 mysql 为之前的 --network-alias意味着在同一网络下,--network-alias 实际上就是这个容器的 hostname,每个容器都有自己的 IP 地址

    • 为什么需要这些环境变量?是因为 node 会将这些环境变量从命令行中提取到 process.env,然后在 node 项目中使用他们,以指定连接到 MySQL 服务。

  • 查看数据变化和存储

    • 进入 MySQL 数据库容器中使用 MySQL 命令查看数据变化 docker exec -it <mysql-container-id> mysql -p todos

    • 通过 MySQL 所映射的 volume 使用 docker volume inspect todo-mysql-data 查看

以更简单的方式==打包和分享==镜像 Use Docker Compose

Docker Compose is a tool that helps you define and share multi-container applications.

  • 在项目中创建一个 compose.yaml,字段如下:

    • 顶层字段为 servicesvolumes(named volumes)
  • 运行 docker compose up 启动项目

    • docker compose up -d

    • -d 表示在后台运行

    • Docker Compose 会自动创建网络运行 yaml 文件中的服务,使得这些容器可以进行通信。

  • docker compose down 停掉 docker compose 启动的服务并且移除网络。默认情况下,不会清除创建的 named volumes,可以通过 --volumes 参数来清除。

镜像构建的最佳实践 Image-building best practices

  • 层缓存 Layer caching

    # old
    # syntax=docker/dockerfile:1
    FROM node:18-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    CMD ["node", "src/index.js"]
    
    # new
    # syntax=docker/dockerfile:1
    FROM node:18-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production
    COPY . .
    CMD ["node", "src/index.js"]
    
    • 上游 ---> 下游(从上到下)

    • 镜像是由一层一层的 layer 所构建起来的,先构建上层,再构建下层,当某一层发生变化时,其下游所有 layer 都需要重新构建;

    上述两个 Dockerfile 对比,当修改源文件重新构建时,第一个 Dockerfile 会从 COPY 步骤开始依次执行下游的构建步骤,如复制粘贴源文件(包括 node_modules)、重新 yarn install......而第二个 Dockerfile 会从 RUN 步骤开始向下游构建,且第二种方式会添加一个额外的 .dockerignore 文件,COPY 步骤就会忽略掉 node_modules,因此,每次更新完代码后构建镜像会更快。


  • 多阶段构建 Multi-stage builds --- React example

    • 在 Dockerfile 中,使用多个 FROM 可进行多阶段构建,前一阶段的输出作为后一阶段的输入。

    • AS build 将当前构建阶段命名为 build 阶段;

    • --from= 告诉 COPY 步骤需要复制的文件在 build 阶段的 /app/build 目录中;

    • 多阶段构建结束后,最终只会打包成一个镜像,此处为 nginx 镜像(build 镜像结果以最后一个 FROM 为准,其他为临时环境.....个人理解),打包好的静态文件被托管在 nginx /usr/share/nginx/html/ 。

文章参考:Docker 官方文档 - Get Started