Docker 快速入门实操教程(完结)
Docker,启动!
如果安装好Docker不知道怎么使用,不理解各个名词的概念,不太了解各个功能的用途,这篇文章应该会对你有帮助。
前置条件:已经安装Docker并且Docker成功启动。
实操内容:使用Docker容器替换本地安装的程序并迁移数据(MySQL、redis)。
最终目的:熟练使用Docker各项功能。
理解概念
Docker官方提供了一个分发平台DockerHub,可以从上面拉取已经提供好的镜像直接构建容器运行。
这个过程会涉及到Docker的一些概念,在刚接触的时候比较抽象,这里以烘焙出一个蛋糕为例子说明一下:
- Dockerfile: 蛋糕的配方。配方上详细列出了需要的材料(如面粉、糖、鸡蛋)以及烘焙的步骤(如先将面粉和糖混合,然后加入鸡蛋搅拌)。
- 镜像(Image): 按照配方做出了一个半成品蛋糕,这就是蛋糕的"镜像" 。这个蛋糕可以被任何人复制,每一个复制品都会和原蛋糕一模一样。
- 容器(Container): 将半成品蛋糕烘焙后,得到一个可食用的蛋糕。可以根据同一个镜像制作出很多个完全一样的蛋糕,也可以在烘焙时自己加一些材料。每个蛋糕都是独立的,和其他蛋糕没有关联。
所以从DockerHub拉取镜像并且跑起来的过程就可以理解为:
- 镜像提供者编写好了配方(
Dockerfile
),将其制成(构建
)了半成品蛋糕(镜像
)。 - 用户购买(
拉取
)这个半成品蛋糕。 - 烘焙(
创建
)后得到了一个可食用的蛋糕(容器
),食用蛋糕(运行容器
)。 - 通常创建容器和运行容器都会归拢在同一步:创建并运行。
还有另外两个比较重要的概念: 层(Layers)
和 缓存(Cache)
,目前不会接触到,可以在看 构建/推送镜像
这一节时再去深入理解。
创建/运行容器
每一步都提供了Docker desktop(简称桌面版)的操作截图和终端命令(桌面版界面友好但局限较大,仅适合初步上手)。
拉取镜像
从DockerHub拉取MySQL镜像到本地,这一步可能会因为网络原因失败,可以配置其他镜像源或者使用代理,网上教程很多。
终端命令
docker pull 仓库地址/命名空间/镜像名称:标签
- 仓库地址: 没有显式指定仓库地址时,默认会从DockerHub查找镜像;拉取私有仓库的镜像,需要指定仓库地址。
- 命名空间: 截图最后有一个名为
ubuntu/mysql
的镜像,其中ubuntu
是命名空间,用以区分不同的个人或组织发布的镜像。没有显式指定命名空间时,默认会查找官方团队发布的镜像。 - 镜像名称: 需要拉取的镜像的名称。
- 标签: 没有显式指定标签时,默认会拉取
latest
标签,latest
表示这是最新的版本。
通过 docker pull
拉取镜像并不是必须的,在 docker run
时,如果本地不存在指定镜像,Docker会自动拉取。
创建并运行容器
拉取完成后,通过 docker run
创建容器并运行前进行一些配置:
终端命令
# 截图对应命令
docker run -d --name mysql_8.3.0 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:latest
# 完整命令
docker run [选项参数] 仓库地址/命名空间/镜像名称:标签 [命令行] [命令行参数]
-
选项参数:
--name
:设置容器名称,不能重复,这里使用的是镜像名_版本号
的方式。-p
:设置端口映射,将宿主机的3306
端口映射到容器的3306
端口,宿主机上的其他进程通过该端口才能访问到容器内的服务。如果不配置端口映射,则只能在容器内部访问服务或通过虚拟网络让容器可以相互通信。-e
:设置环境变量,配置MYSQL_ROOT_PASSWORD=root
用以指定root用户密码,这是由镜像创建者约定的,不同的镜像配置项会有所不同。-v
:设置目录挂载,用法参考目录挂载
章节。-d
:让容器在后台运行 -
命令行: 在容器启动时执行命令(如
ls
),可以省略。 -
命令行参数: 传给 命令行 的额外参数(如
/etc
,这样在容器启动时就会执行ls /etc
),可以省略。
常用命令
容器已经创建好后就不再适用于 docker run
命令了, docker run
命令主要是用于创建新的容器并运行,如果需要启动已经存在的容器,则使用 docker start
命令。
# 列出所有容器
docker ps -a
# 列出所有镜像
docker image ls
docker images
# 启动容器
docker start 容器名称/容器ID
# 停止容器
docker stop 容器名称/容器ID
# 强制停止容器
docker kill 容器名称/容器ID
# 重启容器
docker restart 容器名称/容器ID
# 删除容器
docker rm 容器名称/容器ID
# 删除镜像
docker rmi 容器名称/容器ID
目录挂载
现存问题:
- 数据没有保存到宿主机中,当容器删除后,数据就丢失了。
- 宿主机和容器之间的文件传递比较麻烦。
- 多个容器需要共享数据。
目录挂载可以解决以上问题,Docker为目录挂载提供了三种方式:
-
bind mount: 把宿主机目录映射到容器内,双向文件传递。适合变动比较频繁的场景,比如代码目录、配置文件等。
-
volume: 由容器创建和管理,存储在宿主机中,官方推荐,Linux 文件系统。适合存储不需要关心的数据,如数据库数据。
-
tmpfs mount: 适合存储临时文件,存储在宿主机内存中。不可多容器共享。
以MySQL镜像为例,其 Dockerfile
中写了创建 volume
用于持久化保存数据的命令(其他镜像也可以通过这种方式查看需要持久化的目录)。
虽然 Dockerfile
中有创建 volume
的命令,但是如果创建容器时没有主动为 volume
命名,其就是匿名 volume
,Docker会为匿名 volume
随机生成一个名称,当挂载该 volume
的容器被删除后,该 volume
也会被删除。
当创建容器时主动指定的 volume
路径和 Dockerfile
约定的路径一致,则该镜像创建的 volume
就不会被挂载为匿名 volume
了,容器删除后该 volume
也会保留,这也是最方便的一种命名方式。
终端命令
docker run -d --name mysql_8.3.0 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -v=mysql_volume:/var/lib/mysql -v D:\mount:/pc_mount mysql:latest
挂载目录时,如果只赋予了名称则是 volume
方式,如果指定了具体目录就是 bind mount
方式。
所以在这个容器中:
- 将容器内的
/var/lib/mysql
目录挂载为volume
并且命名为mysql_volume
。 - 将宿主机的
D:\mount
目录映射至容器中的/pc_mount
。
在挂载目录时,如果你指定的目录不存在于容器中,则会自动创建,这里的 /pc_mount
目录就会自动被创建。
迁移实操
现在需要将宿主机中MySQL数据迁移到容器中,打算采用navicat的数据迁移工具,那么就需要同时运行两个数据库,端口同为 3306
会冲突,宿主机上MySQL端口改起来并不方便,容器创建后端口也不能修改,那么就可以使用数据挂载的方式。
迁移方案:
- 停止运行端口为
3306
的MySQL容器。 - 新创建一个MySQL容器,端口指定为
3305
(或其他任意未被占用的端口),指定volume
的名称和端口3306
的容器一致。 - 迁移数据,删除端口为
3305
的容器,运行端口为3306
的容器,数据迁移成功。
当数据库文件较大时,使用navicat迁移则会显得有些性能不足了,这时候就需要通过命令行导入:
-
将需要导入的SQL文件放在宿主机挂载的目录下(宿主机:
D:\mount
;容器:/pc_mount
)。 -
打开容器的终端
docker exec -it mysql_8.3.0 bash
-
登入MySQL并选择需要导入的数据库
mysql -u root -proot use test-base; source /pc_mount/001.sql;
从容器中导出SQL文件到宿主机同理,将SQL文件导出至挂载的 /pc_mount
目录下,在宿主机的 D:\mount
就可以看到。
虚拟网络
每个Docker容器都运行在自己的环境中,互不干扰,所以上述内容中都依赖宿主机的端口映射进行容器通信。但是有些时候我们只要让这个项目能在宿主机上访问到,并不在意其所依赖的服务是否能够被宿主机操作和管理。就可以通过Docker提供的虚拟网络实现容器之间的通信,再映射项目入口到宿主机即可。
桌面版并没有为虚拟网络提供较好的GUI支持,需要终端执行。
# 查看已存在的虚拟网络
docker network ls
默认已经存在了三个虚拟网络,这是由Docker创建的,对应着不同的网络驱动类型,驱动类型的区别如下:
- Bridge网络: 默认值。容器在独立的网络空间运行,可以相互通信并访问外部网络。容器内服务能通过端口映射被外部访问。
- Host网络: 容器共享宿主机的网络空间,不再需要端口映射,直接使用宿主机的端口。这种模式提供了最高的网络性能,但是失去了隔离性。
- None网络: 容器拥有自己的网络空间,但不配置任何网络接口。它只有本地回环接口,没有任何外部网络访问能力,提供了最高的网络隔离性。
# 创建名为 test_net 的网络
docker network create test_net
# 在该网络下创建两个容器
docker run --name redis_temp --network=test_net -d redis:latest
docker run --name redisinsight -p 8001:8001 --network test_net -d redislabs/redisinsight:latest
创建了redis容器,但是并没有为其映射端口。所以现在在宿主机中并不能访问到这个redis容器。
创建了redisInsight容器并且映射了8001端口,这是一个redis的GUI工具,用于测试是否可以通过虚拟网络访问到redis容器。
访问 http://localhost:8001/ 进入redisInsight的主页,添加一个redis数据库。
Docker内部的DNS服务会自动将容器名称解析为容器对应的IP地址(即容器名称就是域名),所以主机地址填写容器名称即可。
连接成功,这样既可以操作容器内的redis数据,又不会占用宿主机自身的redis应用抢占端口。同理,部署其他项目时,如果项目容器需要连接数据库容器,也可以通过虚拟网络实现。
如果容器已经被创建,可以更改已存在的容器的连接的网络
docker network connect 网络名称 容器名称
使用技巧
查看软件版本
部分镜像的 Tag
是 latest
,并没有明确指出具体的版本号,想要查看版本号就只能手动查看。
桌面版点击容器右侧 ···
打开更多选项,选择 Open in terminal
进入容器的终端,执行该软件查看版本的命令。
终端命令
docker exec mysql_8.3.0 mysql -V
但是问题就来了,如果需要版本号是为了给容器命名,这种方案需要先运行容器,将容器删除,再重新创建容器,很麻烦。
通常镜像的环境变量中会指明版本号,可以直接点开镜像查看
终端命令
docker inspect mysql:latest
这个方式虽然比较方便,但是需要进行推测,并非一定正确。
保持容器运行
当在桌面版运行ubuntu等容器时,会发现容器启动后就停止了,进入 Exited
状态,如果想要容器持续运行,就需要需要在容器内部执行一个持续运行的进程。
桌面版已经不能满足需求了,需要终端执行
docker run -it --name ubuntu_22.04 ubuntu:latest
-t
指令分配一个虚拟的终端或控制台,可以让容器持续运行不会关闭。
-i
指令可以让打开的控制台能够接受用户输入。
构建/推送镜像
想要通过Docker将项目部署到服务器上或是分发项目供他人使用,就需要将项目构建为镜像,官方主要推荐通过 Dockerfile
构建镜像。 Dockerfile
是一个文本文件(无文件后缀),由一系列的命令和参数构成,这些命令对应了在构建镜像时的操作步骤。
编写Dockerfile文件
Dockerfile常用指令:
- FROM: 指定基础镜像。所有后续的操作都是基于这个基础镜像进行的。
- WORKDIR: 设定后续命令的执行目录。
- COPY: 复制文件、指定目录中的所有内容(不含目录本身)到镜像中。
- ADD: 复制文件、指定目录中的所有内容(不含目录本身)到镜像中。对tar格式的压缩文件会自动解压。
- RUN: 构建过程中执行命令。比如安装一些软件,创建一些文件等。
- CMD: 为容器提供默认的执行命令,会被
docker run
的命令行参数覆盖。 - ENTRYPOINT: 为容器提供默认的执行命令,不会被
docker run
的命令行参数覆盖。 - EXPOSE: 公开容器的一个端口供外部访问。
通过maven执行 package
手动将项目打包,命名为 output-dem.jar
,在项目根目录下新建一个 Dockerfile
文件:
# 使用JDK17基础镜像
FROM openjdk:17-jdk-slim
# 设置工作目录,容器也会使用该目录作为工作目录
WORKDIR /app
# 将jar包复制到/app路径下
COPY target/output-demo.jar app.jar
# 设置在运行此镜像后默认执行的命令,让它运行刚才的jar包
ENTRYPOINT ["java", "-jar", "app.jar"]
# 暴露端口,取决于项目实际使用的端口号
EXPOSE 8080
如果不是Java开发,设备上并没有安装 JDK
和 maven
等构建需要的环境(其他语言同理),但是又有打包项目的需求,则可以通过多阶段构建的方式,在镜像中完成编译等操作:
# 使用包含JDK17和Maven3.8.5的基础镜像
# 将本构建阶段命名为 build ,以便在后面的阶段中引用
FROM maven:3.8.5-openjdk-17-slim AS build
# 设置工作目录,容器也会使用该目录作为工作目录
WORKDIR /app
# 将当前目录下的所有文件添加到工作目录下(.和./都可以表示当前目录)
ADD . .
# 使用Maven构建项目为jar包
RUN mvn clean package
# 使用Maven构建项目为jar包(跳过测试阶段)
# RUN mvn clean package -DskipTests=true
# 新的构建阶段
# 引入JDK17的基础镜像
FROM openjdk:17-jdk-slim
# 设置工作目录,容器也会使用该目录作为工作目录
WORKDIR /app
# 将 build 阶段构建jar包复制到新阶段的/app路径下
COPY --from=build /app/target/output-demo.jar app.jar
# 设置在运行此镜像后默认执行的命令,让它运行刚才的jar包
ENTRYPOINT ["java", "-jar", "app.jar"]
# 暴露端口,取决于项目实际使用的端口号
EXPOSE 8080
在Docker的多阶段构建中,每次使用新的 FROM
指令,都会开始一个新的构建阶段,上一阶段的指令会创建为一个临时的镜像。在新阶段中,前一阶段的层和设置都被丢弃,只有 --from
指定的之前阶段的内容被保留,就像是开始了一个全新的 Dockerfile
一样。
构建镜像
写好 Dockerfile
后,就可以通过该文件构建镜像了
docker build -t 仓库地址/命名空间/镜像名:标签 .
docker build -t 仓库地址/命名空间/镜像名:标签 -f /path/myDockerfile .
-t
指定镜像名称(如果不推送到私有仓库,仅本地使用, 仓库地址/命名空间/
可以省略)。
-f
指定 Dockerfile
所在的目录,也可以指定 自定义名称的Dockerfile
( -f
参数可省略)。
.
使用当前目录下作为上下文环境, COPY
等命令会从该目录查找文件。未指定 -f
参数时,则使用上下文环境中名为 Dockerfile
的文件。
推送镜像
每次发布更改内容都需要打包镜像后上传到生产环境再部署很麻烦,将镜像推送至私有仓库中,在生产环境直接从仓库中拉取镜像则更加高效。
# 登录仓库
docker login 仓库地址
# 推送镜像
docker push 仓库地址/命名空间/镜像名:标签
没有显式指定仓库地址时,默认会将DockerHub作为仓库地址。
层与缓存
层(Layers): 根据 Dockerfile
构建镜像时,每一个会改变文件系统状态的指令( RUN
、COPY
、 ADD
等)都会新建一个层 ,每个层都是前一层的改动的结果,并且每个层都只保留改动的部分,共享未改动的部分。依次将所有的层合并起来就是完成的镜像。
根据这个例子可以方便理解层的概念:
# Dockerfile A
RUN apt-get install -y software-package # 第一层
RUN rm -rf /var/lib/apt/lists/* # 第二层
在这个片段中,第一层中安装了一个软件包,第二层中删除了软件包。
因为 每个层都是前一层的改动的结果
,所以第二层的删除文件并不能影响到第一层,只是会在第二层中对该文件打上一个 删除
的标记,这个文件会作为一个无用的文件存在于最终构建的镜像中,增加了镜像的体积。
# Dockerfile B
RUN apt-get install -y software-package && rm -rf /var/lib/apt/lists/*
在这个片段中,安装和删除软件包是在同一个指令下执行的,所以他们是处于同一层的操作,当这一层的构建结束时,这些文件就会被清理掉,它们也就不会存在于最终的镜像中了。
缓存: 缓存多个镜像之间可以共享层,如果多镜像都是基于同一个基础镜像进行构建的。那么,这个基础镜像的所有层都只需要存储一次,就能在所有的镜像中共享。如果一个镜像的大部分层已经在本地存在,那么在拉取这个镜像时,只有不存在的层需要被下载,这可以极大地节省时间和网络带宽。
Docker Compose
当项目依赖的服务较多时,每个容器都要单独配置运行,指定网络。使用Docker Compose,可以通过一个YAML文件定义服务,并同时运行它们。
Docker Compose将所管理的容器分为三层:工程(Project)、服务(Service)、容器(Container)。
通过一个例子来理解三层结构:
- 工程: 一个工程可以被视为一家公司,它为所有服务提供了整体的工作环境和资源配置。
- 服务: 公司内设有各种部门,如财务和行政等,每个部门有自己特定的职责和任务。每个部门都可以被看作一个服务。
- 容器: 每个部门由一个或多个员工组成。尽管每个员工都是独立的,但他们共享同样的环境。一个员工相当于一个容器。
所有部门都在同一家公司工作并使用该公司的资源,所以所有服务在工程中共享同样的网络、卷等资源。
各个部门之间还是会进行交流和协作,所以各个服务之间可以互相通信。
同一部门的所有员工都具有相同的工作环境,所以属于同一服务的所有容器都有统一的配置。员工各自做自己的项目,所以容器间有一定的隔离性。
在要部署项目的目录创建一个 docker-compose.yml
文件:
# 指定Docker Compose配置文件的版本
version: '3.8'
services:
# 定义应用服务,名为 app
app:
image: '仓库地址/命名空间/镜像名称:标签'
# 将容器的8080端口映射到宿主机的8080端口
ports:
- 8080:8080
volumes:
# 将 docker-compose.yml 所在目录映射到容器中的 /app 目录(在 Dockerfile 中给定的工作目录)
- ./:/app
# 定义启动依赖,会先启动 mysqldb 和 redisdb,再启动 app
depends_on:
- mysqldb
- redisdb
# 指定容器启动后执行的命令
command: ["java", "-jar", "app.jar"]
# 如果服务非手动停止,Docker会自动尝试重启服务
restart: always
# 定义一个MySQL服务,名为 mysqldb
# 其他服务连接MySQL数据库时,主机地址就是 mysqldb:3306
mysqldb:
image: mysql:8.0.30
environment:
- MYSQL_ROOT_PASSWORD=root
volumes:
- db_data:/var/lib/mysql
# 定义一个Redis服务,名为 redisdb
# 其他服务连接Redis数据库时,主机地址就是 redisdb:6379
redisdb:
image: redis:7.2.4
volumes:
- redis_data:/data
volumes:
db_data:
redis_data:
在 docker-compose.yml
文件所在的目录执行
docker compose up -d
docker compose up
根据 docker-compose.yml
文件内容启动、创建、连接服务。
-d
参数表示以后台方式运行。
-f
如果文件名称不是 docker-compose.yml
,可以通过 -f
命令指定,使用方法与 构建镜像
章节一致。
每次更改了 docker-compose.yml
文件,都需要重新运行 docker-compose up -d
命令以应用更改。
结语
任何技术都有其深度与复杂性,难以通过一篇文章详尽阐述。本文的初衷是为你在遭遇问题时,提供一个寻找解答的方向指引。