学习了神光大佬的《Nest 通关秘籍》后,对docker做了个笔记,并实操部署了一下个人项目,在此记录一下
是什么
Docker是一种开源的容器化平台,它可以将应用程序及其依赖项打包到一个可移植的容器中,使得应用程序能够在任何地方以相同的方式运行。Docker容器可以在多种操作系统上运行,并且可以在云计算、物联网等各种场景下使用。Docker的出现,使得应用程序的开发、测试、部署等过程更加便捷、快速和可靠。
它把系统的所有文件封装成一个镜像,镜像跑起来作为容器,它可以在一台机器上跑多个容器,每个容器都有独立的操作系统环境,比如文件系统、网络端口等,在容器内跑各种服务。
这样整个环境都保存在这个镜像里,部署多个实例只要通过这个镜像跑多个容器就行。
容器是一组用于隔离文件系统、CPU 资源、内存资源、系统权限等的各种轻量级方法。容器在很多意义上类似于虚拟机,但它们比虚拟机更高效,而安全性则往往低于虚拟机。
docker desktop
一款为Mac和Windows操作系统提供的桌面应用程序,它可以帮助用户快速地安装和配置Docker环境。Docker Desktop包含了Docker Engine、Docker CLI、Docker Compose、Docker Machine等核心工具,同时还提供了图形界面,帮助用户更加方便地管理和操作Docker容器和镜像。
官网下载地址
images 是镜像,containers 是镜像跑起来的容器
搜索并拉取一个远程的镜像(nginx)
启动服务
浏览器访问 http://localhost 就可以访问到
docker命令
- docker pull
docker pull nginx:latest
docker pull node:16.7.0
- docker run
docker run --name nginx-test2 -p 90:80 -v /Users/liuzepeng/Desktop/test:/usr/share/nginx/html -e KEY1=VALUE1 -d nginx:latest
# docker run --name=容器名称 镜像名称
docker run --name=restart-test-container restart-test:first
# --restart=always 指定重启策略
# -d 后台运行
docker run -d --restart=always --name=restart-test-container2 restart-test:first
-p 是端口映射
-v 是指定数据卷挂载目录
-e 是指定环境变量
-d 是后台运行
docker run 会返回一个容器的 hash
- docker ps
显示容器列表的,默认是运行中的
docker ps
想显示全部的,可以加个 -a
- docker images
获取镜像列表
docker images
- docker exec
我们在容器的 terminal 里执行命令,对应的是 docker exec 命令
docker exec -it c9014aa1e6ad43e5900f8ca8df764401554c1a83e5b914a20b786c5c2c90c601 /bin/bash
-i 是 terminal 交互的方式运行
-t 是 tty 终端类型
然后指定容器 id 和 shell 类型,就可以交互的方式在容器内执行命令了。
exit 退出
- docker logs
docker logs 容器id ---> 查看日志
- docker inspect
docker inspect 容器id ---> 容器详情
- docker volume
管理数据卷
- docker start:启动一个已经停止的容器
- docker rm:删除一个容器
- docker stop:停止一个容器
其他
来自chatgpt
当谈及 Docker 命令时,以下是一些常用且详细的 Docker 命令的总结:
镜像操作:
- docker images:列出已下载的镜像。
- docker pull <image>:从 Docker Hub 或其他 Registry 拉取指定的镜像。
- docker build -t <name:tag> .:使用 Dockerfile 构建镜像,为其命名和标记。
- docker rmi <image>:删除指定的镜像。
- docker tag <source_image> <target_image>:给镜像打标签。
容器生命周期管理:
- docker run <image>:创建并启动一个容器。
- docker start <container>:启动已停止的容器。
- docker stop <container>:停止正在运行的容器。
- docker restart <container>:重启容器。
- docker rm <container>:删除指定的容器。
- docker ps:列出当前正在运行的容器。
- docker logs <container>:查看容器日志。
容器操作和交互:
- docker exec <container> <command>:在正在运行的容器中执行指定的命令。
- docker attach <container>:附加到运行中的容器,即进入容器终端。
- docker cp <local_path> <container>:<container_path>:将文件从主机复制到容器内。
- docker cp <container>:<container_path> <local_path>:将文件从容器复制到主机内。
网络操作:
- docker network ls:列出现有的 Docker 网络。
- docker network create <name>:创建一个 Docker 网络。
- docker network connect <network> <container>:将容器连接到指定的网络。
- docker network disconnect <network> <container>:从指定的网络中断容器的连接。
Docker Compose:
- docker-compose up:根据 docker-compose.yml 配置文件启动容器。
- docker-compose down:停止并移除由 Docker Compose 启动的容器。
这些命令涵盖了 Docker 中常见的操作。然而,Docker 提供了更多功能丰富的命令和选项,可以根据具体需求进行进一步探索和学习。
如何制作镜像
Dockerfile
FROM node:latest
WORKDIR /app
COPY . .
RUN npm config set registry https://registry.npm.taobao.org
RUN npm install -g http-server
EXPOSE 8080
CMD ["http-server", "-p", "8080"]
- FROM:基于一个基础镜像来修改 先通过 FROM 继承了 node 基础镜像,里面就有 npm、node 这些命令
- WORKDIR:指定当前工作目录
- COPY:把容器外的内容复制到容器内 通过 COPY 把 Dockerfile 同级目录下的内容复制到容器内,这里的 . 也就是 /app 目录
COPY . . 是 Dockerfile 中的一条指令,用于将当前目录下的所有文件和文件夹复制到容器的工作目录(WORKDIR)中
- EXPOSE:声明当前容器要访问的网络端口,比如这里起服务会用到 8080
- RUN:在容器内执行命令 通过 RUN 执行 npm install,全局安装 http-server
- CMD:容器启动的时候执行的命令 这里就是执行 http-server 把服务跑起来
创建如下文件
执行build
docker build -t aaa:ccc
aaa 是镜像名,ccc 是镜像的标签
docker build -t name:tag -f filename .
- docker build:启动 Docker 构建过程。
- -t name:tag:指定构建生成的镜像的名称和标签。name 是镜像的名称,tag 是镜像的标签,通常用于版本控制。
- -f filename:指定 Dockerfile 文件的路径和名称,用于告诉 Docker 使用哪个 Dockerfile 进行构建。filename 是 Dockerfile 的路径和名称。
- .:表示当前目录,也就是 Dockerfile 所在的目录。Docker 在构建过程中会将当前目录下的文件复制到镜像中。
减小镜像体积
镜像自然是越小性能越好,所以 docker 支持你通过 .dockerignore 声明哪些不需要发送给 docker daemon。
.dockerignore 是这样写的:
*.md
!README.md
node_modules/
[a-c].txt
.git/
.DS_Store
.vscode/
.dockerignore
.eslintignore
.eslintrc
.prettierrc
.prettierignore
docker build 时,会先解析 .dockerignore,把该忽略的文件忽略掉,然后把剩余文件打包发送给 docker daemon 作为上下文来构建产生镜像。
多阶段构建
还有一种减小镜像体积的手段:多阶段构建
第一次构建出 dist 目录,第二次再构建出跑 dist/main.js 的镜像
# build stage
FROM node:18 as build-stage
WORKDIR /app
COPY package.json .
RUN npm config set registry https://registry.npmmirror.com/
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:18 as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm install --production
EXPOSE 3000
CMD ["node", "/app/main.js"]
之前是
打包后,这时候 app 下就是有 dist 的文件、生产阶段的 node_modules、package.json 这些文件:
对比下镜像体积,明显看出有减小,少的就是 src、test、构建阶段的 node_modules 这些文件:
但现在镜像依然很大呀,那是因为我们用的基础的 linux 镜像比较大,可以换成 alpine 的,这是一个 linux 发行版,主打的就是一个体积小。
alpine 基础镜像
FROM node:18.0-alpine3.14 as build-stage
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
# production stage
FROM node:18.0-alpine3.14 as production-stage
COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json
WORKDIR /app
RUN npm install --production
EXPOSE 3000
CMD ["node", "/app/main.js"]
一般情况下,我们都会用多阶段构建 + alpine 基础镜像。
docker daemon
Docker Daemon(dockerd)是 Docker 引擎的核心组件之一。它是一个运行在后台的持续运行程序,负责管理和控制 Docker 容器和镜像的生命周期。
守护进程(Daemon)是一种在操作系统中以服务方式运行的进程。与普通的前台进程不同,守护进程通常在系统启动时就会初始化并一直运行,不依赖于用户的登录会话。守护进程在后台默默地工作,接受来自其他进程或用户的请求,并提供相应的服务。
对于 Docker Daemon 来说,它作为 Docker 引擎的后台进程,持续运行,等待来自 Docker 客户端的指令和请求。当使用命令行或其他工具与 Docker 交互时,实际上是与 Docker Daemon 进行通信。Docker Daemon 负责解析和执行这些命令,并控制容器的创建、启动、停止、删除等操作,同时也管理镜像的构建、拉取、推送等过程。
因此,Docker Daemon 可以看作是 Docker 引擎的后台服务进程,它提供了对 Docker 容器和镜像的管理和控制能力,并在后台持续运行以提供服务。
docker
容器和宿主机还是有关联的,比如可以把宿主机的端口映射到容器内的端口、宿主机某个目录挂载到容器内的目录
比如映射了 3000 端口,那容器内 3000 端口的服务,就可以在宿主机的 3000 端口访问了。
比如挂载了 /aaa 到容器的 /bbb/ccc,那容器内读写 /bbb/ccc 目录的时候,改的就是宿主机的 /aaa 目录,反过来,改宿主机 /aaa 目录,容器内的 /bbb/ccc 也会改,这俩同一个。
这分别叫做端口映射、数据卷(volume)挂载。
运行镜像
docker run image-name
docker run -p 3000:3000 -v /aaa:/bbb/ccc --name xxx-container xxx-image
通过 xxx-image 镜像跑起来一个叫做 xxx-container 的容器。
-p 指定端口映射,映射宿主机的 3000 到容器的 3000 端口。
-v 指定数据卷挂载,挂载宿主机的 /aaa 到容器的 /bbb/ccc 目录。
这个镜像是通过 Dockerfile 经过 build 产生的。
过程
docker容器间通信
Docker 通过 Namespace 的机制实现了容器的隔离,其中就包括 Network Namespace。
因为每个容器都有独立的 Network Namespace,所以不能直接通过端口访问其他容器的服务。
那如果这个 Network Namespace 不只包括一个 Docker 容器呢??
可以创建一个 Network Namespace,然后设置到多个 Docker 容器,这样这些容器就在一个 Namespace 下了,就可以直接访问对应端口
Docker 确实支持这种方式,叫做桥接网络。
桥接网络
通过 docker network create 创建一个桥接网络,
docker network create common-network
然后把之前的 3 个容器停掉、删除,我们重新跑:
docker stop mysql-container redis-container nest-container
docker rm mysql-container redis-container nest-container
然后 docker run 的时候指定 --network 为我们刚创建的 common-network,这样 3 个容器就可以通过容器名互相访问了。
docker run -d --network common-network -v /Users/guang/mysql-data:/var/lib/mysql --name mysql-container mysql
在 docker-compose.yml 配置下 networks 创建桥接网络,然后添加到不同的 service 上即可。
version: '3.8'
services:
nest-app:
build:
context: ./
dockerfile: ./Dockerfile
depends_on:
- mysql-container
- redis-container
ports:
- '3000:3000'
networks:
- common-network
mysql-container:
image: mysql
volumes:
- /Users/guang/mysql-data:/var/lib/mysql
networks:
- common-network
redis-container:
image: redis
volumes:
- /Users/guang/aaa:/data
networks:
- common-network
networks:
common-network:
driver: bridge
实现原理就是对 Network Namespace 的处理,本来是 3个独立的 Namespace,当指定了 network 桥接网络,就可以在 Namespace 下访问别的 Namespace 了。
多个容器之间的通信方式,用桥接网络是最简便的。
docker和pm2重启策略
其实 PM2 诞生的时候是没有 Docker 这种容器技术的,那时候都是直接部署在机器上,这时候自然需要一个进程管理工具来做进程的重启、负载均衡等功能。这是 PM2 当年很流行的原因。
但后来有了 Docker,里面跑的进程崩溃之后,Docker 容器支持自动重启。
Docker 是支持自动重启的,可以在 docker run 的时候通过 --restart 指定重启策略,或者 Docker Compose 配置文件里配置 restart。
有 4 种重启策略:
- no: 容器退出不自动重启(默认值)
- always:容器退出总是自动重启,除非 docker stop。
- on-failure:容器非正常退出才自动重启,还可以指定重启次数,如 on-failure:5
- unless-stopped:容器退出总是自动重启,除非 docker stop
重启策略为 always 的容器在 Docker Deamon 重启的时候容器也会重启,而 unless-stopped 的不会。
其实我们用 PM2 也是主要用它进程崩溃的时候重启的功能,而在有了 Docker 之后,用它的必要性就不大了。
当然,进程重启的速度肯定是比容器重启的速度快一些的,如果只是 Docker 部署,可以结合 pm2-runtime 来做进程的重启。
绝大多数情况下,直接用 Docker 跑 node 脚本就行,不需要 PM2。
优化Dockerfile技巧
Dockerfile 有挺多技巧:
- 使用 alpine 的镜像,而不是默认的 linux 镜像,可以极大减小镜像体积,比如 node:18-alpine3.14 这种
- 使用多阶段构建,比如一个阶段来执行 build,一个阶段把文件复制过去,跑起服务来,最后只保留最后一个阶段的镜像。这样使镜像内只保留运行需要的文件以及 dependencies。
- 使用 ARG 增加构建灵活性,ARG 可以在 docker build 时通过 --build-arg xxx=yyy 传入,在 dockerfile 中生效,可以使构建过程更灵活。如果是想定义运行时可以访问的变量,可以通过 ENV 定义环境变量,值使用 ARG 传入。
- CMD 和 ENTRYPOINT 都可以指定容器跑起来之后运行的命令,CMD 可以被覆盖,而 ENTRYPOINT 不可以,两者结合使用可以实现参数默认值的功能。
- ADD 和 COPY 都可以复制文件到容器内,但是 ADD 处理 tar.gz 的时候,还会做一下解压。
灵活使用这些技巧,可以让你的 Dockerfile 更加灵活、性能更好。
其他:dockerfile 内换行使用 \
使用 alpine 镜像
上面👆
https://www.yuque.com/leyioliu-76kvf/salixq/szw7vv4htqewch72#P7EvB
使用多阶段构建
上面👆
https://www.yuque.com/leyioliu-76kvf/salixq/szw7vv4htqewch72#P7EvB
使用 ARG 增加构建灵活性
灵活使用 ARG,可以增加 dockerfile 的灵活性
使用 Docker 的 ARG 指令可以增加构建过程的灵活性,允许在构建镜像时传递参数。这样可以在构建过程中根据不同的需求动态地设置一些值。
下面是使用 ARG 指令增加构建灵活性的步骤:
- 在 Dockerfile 中使用 ARG 指令定义一个变量,并可选地指定默认值,例如:
ARG my_var=default_value
- 在 Dockerfile 的构建过程中,你可以通过引用这个变量来使用它,例如:
ENV MY_VAR=${my_var}
这里使用了 ENV 指令将 ARG 变量的值赋给环境变量 MY_VAR。
- 构建镜像时,可以通过 --build-arg 标志传递一个新的值给这个变量。例如,通过以下命令构建镜像并传递一个新的值给 ARG 变量:
docker build --build-arg my_var=new_value -t image_name .
在构建过程中,Docker 将使用传递的值来覆盖 ARG 变量的默认值。
通过上述步骤,你可以在构建镜像时灵活地传递参数,从而定制化构建过程。这在需要根据不同环境或需求进行自定义构建的场景下非常有用。
CMD 结合 ENTRYPOINT
当使用 Docker 构建镜像时,可以使用 CMD 和 ENTRYPOINT 这两个指令来定义容器启动时要执行的命令或脚本。
- ENTRYPOINT 指令用于设置容器启动时要执行的主要命令或脚本。
- CMD 指令用于定义容器的默认执行命令,可以在运行容器时被覆盖。
它们可以结合使用,提供更灵活的容器启动方式。当二者同时存在时,CMD 的值会作为 ENTRYPOINT 的默认参数传递,并且 CMD 可以通过 docker run 的参数进行覆盖。
这样,我们可以在 Dockerfile 中设置容器启动时的默认命令,但也允许在运行容器时传入新的命令来替换默认值。这样的设计允许用户根据具体需求来调整容器的行为,增加了灵活性和自定义性。
COPY vs ADD
这俩都可以把宿主机的文件复制到容器内,
但有一点区别,就是对于 tar.gz 这种压缩文件的处理上。
ADD 把 tar.gz 给解压然后复制到容器内
而 COPY 没有解压,它把文件整个复制过去了
但是 ADD 还可以解压 tar.gz 文件。
一般情况下,还是用 COPY 居多
在 Docker 中,COPY 和 ADD 是两个用于将文件从主机复制到容器内部的指令。
COPY 指令的格式如下:
COPY <源路径> <目标路径>
它将主机上的文件或目录复制到容器中的目标路径。
ADD 指令与 COPY 类似,但功能更丰富:
ADD <源路径> <目标路径>
除了复制文件和目录外,ADD 还支持自动解压缩 tar 文件、从远程 URL 下载文件,并将它们复制到容器中的目标路径。
一般而言,在构建镜像时使用 COPY 指令更常见,因为它更简单且能满足基本的文件复制需求。只有在特定情况下需要自动解压缩或直接从远程下载文件时,才会考虑使用 ADD 指令。
docker Compose
Docker Compose 是一个用于定义和运行多个 Docker 容器的工具。它通过使用简单的 YAML 文件来配置和管理应用程序的服务、网络和卷等方面。Docker Compose 可以让你轻松地在单个主机上定义和管理多个容器,实现容器化应用程序的快速部署。
使用 Docker Compose,你可以将整个应用程序的多个服务组合在一起,并定义它们之间的关系和依赖。这样,你就可以使用一个命令来启动、停止和管理整个应用程序,而不是手动逐个管理每个容器。
以下是 Docker Compose 的主要特点和用法:
- 声明式语法:使用 YAML 文件编写 Docker Compose 配置,非常直观和易于理解。
- 多容器编排:定义和管理多个容器,包括镜像、端口映射、环境变量、数据卷等。
- 服务依赖:可以指定服务之间的依赖关系,确保它们按正确的顺序启动和关闭。
- 网络管理:可以定义自定义网络,使容器能够相互通信,并与外部网络隔离。
- 即时构建和重建:可以通过指定 Dockerfile 或镜像名称来构建和重建容器,保持代码和环境的同步。
- 扩展和伸缩性:可以根据需要通过指定副本数来扩展服务,实现负载均衡。
- 简化部署流程:使用一个命令(docker-compose up)即可启动整个应用程序,而不是手动操作每个容器。
总的来说,Docker Compose 是一个方便的工具,用于定义和管理 Docker 容器化应用程序的多个服务,简化了开发者在本地环境中的部署和管理过程。
常用命令
docker --version #查看版本
docker-compose -h # 查看帮助
docker-compose up # 启动所有docker-compose服务
docker-compose up -d # 启动所有docker-compose服务并后台运行
docker-compose down # 停止并删除容器、网络、卷、镜像。
docker-compose exec yml里面的服务id # 进入容器实例内部 docker-compose exec docker-compose.yml文件中写的服务id /bin/bash
docker-compose ps # 展示当前docker-compose编排过的运行的所有容器
docker-compose top # 展示当前docker-compose编排过的容器进程
docker-compose logs yml里面的服务id # 查看容器输出日志
docker-compose config # 检查配置
docker-compose config -q # 检查配置,有问题才有输出
docker-compose restart # 重启服务
docker-compose start # 启动服务
docker-compose stop # 停止服务
services:
nest-app:
build:
context: ./
dockerfile: ./Dockerfile
depends_on: #会先启动另外两个,再启动这个
- mysql-container
- redis-container
ports:
- '3000:3000'
mysql-container:
image: mysql
ports:
- '3306:3306'
volumes:
- /Users/guang/mysql-data:/var/lib/mysql
redis-container:
image: redis
ports:
- '6379:6379'
volumes:
- /Users/guang/aaa:/data
docker-compose up
它会把所有容器的日志合并输出:
docker原理
docker的实现基于下面三方面:
- Namespace:实现各种资源的隔离
类似这样的 namespace 一共有 6 种:
-
- PID namespace: 进程 id 的命名空间
- IPC namespace: 进程通信的命名空间
- Mount namespace:文件系统挂载的命名空间
- Network namespace:网络的命名空间
- User namespace:用户和用户组的命名空间
- UTS namespace:主机名和域名的命名空间
- Control Group:实现容器进程的资源访问限制
但是只有命名空间的隔离还不够,还得对资源做限制。比如一个容器占用了太多的资源,那就会导致别的容器受影响。怎么能限制容器的资源访问呢?
这就需要 linux 操作系统的另一种机制:Control Group。
创建一个 Control Group 可以给它指定参数,比如 cpu 用多少、内存用多少、磁盘用多少,然后加到这个组里的进程就会受到这个限制。
这样,创建容器的时候先创建一个 Control Group,指定资源的限制,然后把容器进程加到这个 Control Group 里,就不会有容器占用过多资源的问题了。
- UnionFS:实现容器文件系统的分层存储,镜像合并
每个容器都是独立的文件系统,相互独立,而这些文件系统之间可能很大部分都是一样的,同样的内容占据了很大的磁盘空间,会导致浪费。
所以 Docker 设计了一种分层机制:每一层都是不可修改的,也叫做镜像。
我本地两个 nest 镜像,它们都继承了 node 镜像,这两个合起来有 2g 的存储空间么?
没有,因为下面的镜像层是公用的:
使用案例
我的项目:
- 个人网站 网站+后台管理系统+大屏+后端服务(node)
- 掘金自动签到
- 微信推送
version: '3'
services:
mysql-blog:
image: mysql:8.0
container_name: mysql-blog
restart: unless-stopped
environment:
- MYSQL_ROOT_PASSWORD=xxxxx
volumes:
- /Users/liuzepeng/Desktop/all/docker/mysql-docker:/var/lib/mysql
networks:
- common-network
ports:
- '3307:3306'
auto-check-in:
build:
context: ./auto-check-in
dockerfile: Dockerfile
container_name: auto-check-in-container
restart: unless-stopped
wechat_message:
build:
context: ./wechat_message
dockerfile: Dockerfile
container_name: wechat-message-container
restart: unless-stopped
todo-nodejs-api:
build:
context: ./todo-nodejs-api
dockerfile: Dockerfile
container_name: todo-nodejs-api-container
restart: unless-stopped
depends_on:
- mysql-blog
ports:
- '8088:8088'
networks:
- common-network
networks:
common-network:
driver: bridge