官网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
删除原容器。
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
使用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会自动创建,网络也是一样。
其他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都不熟悉,如果有了解的同学可以评论告诉我,