Loading... 官网getting-started的教程:[Getting Started](https://github.com/docker/getting-started) 由于我在自己的Mac上已经安装了[Docker Desktop for Mac](https://docs.docker.com/docker-for-mac/#where-to-go-next),并且已经按照安装后的指导,从GitHub下载了getting-started源码、编译了镜像并上传到了自己的Docker Hub,并启动了容器,所以可以在浏览器上直接输入http://localhost/tutorial/开启教程。 <div class="tip inlineBlock info"> 对于一些单个字符的参数设置,可以进行合并以简化整个命令,比如 `docker run -d -p 80:80 docker/getting-started`可以简化成`docker run -dp 80:80 docker/getting-started` </div> # 开始 ## 编译镜像 下载项目后解压,添加Dockerfile,编译 `docker build -t getting-started .` Mac版的Desktop安装了[Docker Engine](https://docs.docker.com/engine/), Docker CLI client, [Docker Compose](https://docs.docker.com/compose/), [Notary](https://docs.docker.com/notary/getting_started/), [Kubernetes](https://github.com/kubernetes/kubernetes/), 和 [Credential Helper](https://github.com/docker/docker-credential-helpers/). 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](http://localhost:3000/),然后可以查看当前应用的界面,在这个todo应用中,可以增删item  # 更新应用 按照教程修改源码的空提示文案后,重新编译镜像,然后要先删除原容器,才能启动新容易,避免因为占有相同的3000端口而报错。 ## 通过命令行删除原容器 删除之前要先停止容器,这样可以给容器中运行的应用/进程一个停止运行并清理残留数据的机会,内部使用的是Linux的信号量SIGTERM和SIGKILL。 使用`docker ps`获取容器ID,然后使用`docker stop the-container-id`停止容器,然后`docker rm the-container-id`删除原容器。 <div class="tip inlineBlock info"> 可以通过添加force标记使得在一行命令中完成暂停和删除容易的操作,即: `docker rm -f the-container-id` </div> # 推送应用到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](http://play-with-docker.com/)),不过我点Login一直没有反应。。。 # 数据的持久化 每个Docker容器都有自己的非持久化存储,非持久化存储自动创建,从属于容器,生命周期和容器相同,这意味着删除容器就会删除全部的非持久化数据。 即使是使用相同镜像的容器,在一个容器中对于非持久化的数据修改,也不会反应到其他的容器中。 ## Volume卷 Docker中卷属于一等公民。因此可以先创建卷,然后创建容器,然后将卷挂在到容器的一个目录上,所有这个目录内文件的改动,都会反应到主机的卷中,即使删除容器,卷依然存在。 这个示例todo应用使用[SQLite Database](https://www.sqlite.org/index.html) 来存储数据,文件位于 `/etc/todos/todo.db` * 使用`docker volume create todo-db`创建卷 * 暂停并删除原容器 * 创建新的容器并挂载卷`docker run -dp 3000:3000 -v todo-db:/etc/todos getting-started` <div class="tip inlineBlock info"> 尽管命名卷和绑定挂载Bind Mounts(我们将在稍后讨论)是默认Docker引擎安装支持的两种主要卷类型,但是有许多可用的卷驱动器插件来支持NFS,SFTP,NetApp等! 一旦开始在具有Swarm,Kubernetes等集群环境中的多个主机上运行容器,这将尤其重要。 </div> 使用`docker volume inspect volume_name | grep Mount`命令可以查看卷位于Docker主机的什么位置。 # 使用绑定挂载 Bind Mounts 项目的源码经常需要改动,在前面更新空文案的步骤中,需要重新编译镜像,这会很耗时。 使用绑定挂载 Bind Mounts,我们可以控制主机上的确切挂载点。 我们可以使用它来持久化数据,但是通常用于向容器中提供其他数据。 在处理应用程序时,我们可以使用Bind Mounts将源代码挂载到容器中,以使其查看代码更改,做出响应并立即查看更改。 对于基于Node的应用程序,nodemon是监视文件更改然后重新启动应用程序的好工具。 大多数其他语言和框架都有等效的工具。 除了Docker引擎中默认的命名卷Volume和绑定挂载,还有很多其他的卷驱动 ([SFTP](https://github.com/vieux/docker-volume-sshfs), [Ceph](https://ceph.com/geen-categorie/getting-started-with-the-docker-rbd-volume-plugin/), [NetApp](https://netappdvp.readthedocs.io/en/stable/), [S3](https://github.com/elementar/docker-s3-volume), 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](https://docs.docker.com/compose/compose-file/#volume-configuration-reference), 这里没有指定网络networks,默认Docker Compose会创建bridge网络,这是一种单主机网络,只能够实现同一主机上容器的连接。后续文章在使用Swarm模式时,将指定使用overlay驱动,来跨主机连接容器。 Volume卷可以先创建好,也可以在一级配置参数中指明,Docker Compose会自动创建,网络也是一样。 <div class="tip inlineBlock info"> 当应用程序启动时,它实际上会坐下来等待MySQL启动并准备就绪,然后再尝试连接它。 Docker没有任何内置支持来等待另一个容器完全启动,运行并准备就绪,然后再启动另一个容器。 对于基于Node的项目,可以使用 [wait-port](https://github.com/dwmkerr/wait-port)依赖项。 对于其他语言/框架也存在类似的项目。 </div> ## 其他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](https://docs.docker.com/engine/reference/builder/#dockerignore-file)。 文件内容是要忽略复制到镜像中的文件,比如这里添加了`node_modules`文件夹,那么在这种情况下,应该在第二个COPY步骤中省略node_modules文件夹,因为否则,它可能会覆盖由RUN步骤中的命令创建的文件。有关为什么这是对Node.js应用程序的推荐做法和其他最佳实践的更多详细信息,请参阅[Dockerizing a Node.js web app](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/). ## 生产环境中的多阶段编译(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都不熟悉,如果有了解的同学可以评论告诉我, © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 ×Close 赞赏作者 扫一扫支付 0