Docker 快速入门实操教程(完结)

Docker 快速入门实操教程(完结)

Docker,启动!

如果安装好Docker不知道怎么使用,不理解各个名词的概念,不太了解各个功能的用途,这篇文章应该会对你有帮助。

前置条件:已经安装Docker并且Docker成功启动。

实操内容:使用Docker容器替换本地安装的程序并迁移数据(MySQL、redis)。

最终目的:熟练使用Docker各项功能。

理解概念

Docker官方提供了一个分发平台DockerHub,可以从上面拉取已经提供好的镜像直接构建容器运行。

这个过程会涉及到Docker的一些概念,在刚接触的时候比较抽象,这里以烘焙出一个蛋糕为例子说明一下:

  • Dockerfile: 蛋糕的配方。配方上详细列出了需要的材料(如面粉、糖、鸡蛋)以及烘焙的步骤(如先将面粉和糖混合,然后加入鸡蛋搅拌)。
  • 镜像(Image): 按照配方做出了一个半成品蛋糕,这就是蛋糕的"镜像" 。这个蛋糕可以被任何人复制,每一个复制品都会和原蛋糕一模一样。
  • 容器(Container): 将半成品蛋糕烘焙后,得到一个可食用的蛋糕。可以根据同一个镜像制作出很多个完全一样的蛋糕,也可以在烘焙时自己加一些材料。每个蛋糕都是独立的,和其他蛋糕没有关联。

所以从DockerHub拉取镜像并且跑起来的过程就可以理解为:

  1. 镜像提供者编写好了配方( Dockerfile ),将其制成( 构建 )了半成品蛋糕( 镜像 )。
  2. 用户购买( 拉取 )这个半成品蛋糕。
  3. 烘焙( 创建 )后得到了一个可食用的蛋糕( 容器 ),食用蛋糕( 运行容器 )。
  4. 通常创建容器和运行容器都会归拢在同一步:创建并运行。

还有另外两个比较重要的概念: 层(Layers)缓存(Cache) ,目前不会接触到,可以在看 构建/推送镜像 这一节时再去深入理解。

创建/运行容器

每一步都提供了Docker desktop(简称桌面版)的操作截图和终端命令(桌面版界面友好但局限较大,仅适合初步上手)。

拉取镜像

从DockerHub拉取MySQL镜像到本地,这一步可能会因为网络原因失败,可以配置其他镜像源或者使用代理,网上教程很多。

image-20240128210738711

终端命令

docker pull 仓库地址/命名空间/镜像名称:标签
  • 仓库地址: 没有显式指定仓库地址时,默认会从DockerHub查找镜像;拉取私有仓库的镜像,需要指定仓库地址。
  • 命名空间: 截图最后有一个名为 ubuntu/mysql 的镜像,其中 ubuntu 是命名空间,用以区分不同的个人或组织发布的镜像。没有显式指定命名空间时,默认会查找官方团队发布的镜像。
  • 镜像名称: 需要拉取的镜像的名称。
  • 标签: 没有显式指定标签时,默认会拉取 latest 标签, latest 表示这是最新的版本。

通过 docker pull 拉取镜像并不是必须的,在 docker run 时,如果本地不存在指定镜像,Docker会自动拉取。

创建并运行容器

拉取完成后,通过 docker run 创建容器并运行前进行一些配置:

image-20240128213738553

终端命令

# 截图对应命令
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 用于持久化保存数据的命令(其他镜像也可以通过这种方式查看需要持久化的目录)。

image-20240128224015528

虽然 Dockerfile 中有创建 volume 的命令,但是如果创建容器时没有主动为 volume 命名,其就是匿名 volume ,Docker会为匿名 volume 随机生成一个名称,当挂载该 volume 的容器被删除后,该 volume 也会被删除。

当创建容器时主动指定的 volume 路径和 Dockerfile 约定的路径一致,则该镜像创建的 volume 就不会被挂载为匿名 volume 了,容器删除后该 volume 也会保留,这也是最方便的一种命名方式。

image-20240128220629652

终端命令

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 方式。

所以在这个容器中:

  1. 将容器内的 /var/lib/mysql 目录挂载为 volume 并且命名为 mysql_volume
  2. 将宿主机的 D:\mount目录映射至容器中的 /pc_mount

在挂载目录时,如果你指定的目录不存在于容器中,则会自动创建,这里的 /pc_mount 目录就会自动被创建。

迁移实操

现在需要将宿主机中MySQL数据迁移到容器中,打算采用navicat的数据迁移工具,那么就需要同时运行两个数据库,端口同为 3306 会冲突,宿主机上MySQL端口改起来并不方便,容器创建后端口也不能修改,那么就可以使用数据挂载的方式。

迁移方案:

  1. 停止运行端口为 3306 的MySQL容器。
  2. 新创建一个MySQL容器,端口指定为 3305 (或其他任意未被占用的端口),指定 volume 的名称和端口 3306 的容器一致。
  3. 迁移数据,删除端口为 3305 的容器,运行端口为 3306 的容器,数据迁移成功。

当数据库文件较大时,使用navicat迁移则会显得有些性能不足了,这时候就需要通过命令行导入:

  1. 将需要导入的SQL文件放在宿主机挂载的目录下(宿主机: D:\mount ;容器: /pc_mount )。

  2. 打开容器的终端

    docker exec -it mysql_8.3.0 bash
    
  3. 登入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地址(即容器名称就是域名),所以主机地址填写容器名称即可。

image-20240129172212539

连接成功,这样既可以操作容器内的redis数据,又不会占用宿主机自身的redis应用抢占端口。同理,部署其他项目时,如果项目容器需要连接数据库容器,也可以通过虚拟网络实现。

如果容器已经被创建,可以更改已存在的容器的连接的网络

docker network connect 网络名称 容器名称

使用技巧

查看软件版本

部分镜像的 Taglatest ,并没有明确指出具体的版本号,想要查看版本号就只能手动查看。

桌面版点击容器右侧 ··· 打开更多选项,选择 Open in terminal 进入容器的终端,执行该软件查看版本的命令。

终端命令

docker exec mysql_8.3.0 mysql -V

但是问题就来了,如果需要版本号是为了给容器命名,这种方案需要先运行容器,将容器删除,再重新创建容器,很麻烦。

通常镜像的环境变量中会指明版本号,可以直接点开镜像查看

image-20240128220336815

终端命令

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开发,设备上并没有安装 JDKmaven 等构建需要的环境(其他语言同理),但是又有打包项目的需求,则可以通过多阶段构建的方式,在镜像中完成编译等操作:

# 使用包含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 构建镜像时,每一个会改变文件系统状态的指令( RUNCOPY 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)。

通过一个例子来理解三层结构:

  1. 工程: 一个工程可以被视为一家公司,它为所有服务提供了整体的工作环境和资源配置。
  2. 服务: 公司内设有各种部门,如财务和行政等,每个部门有自己特定的职责和任务。每个部门都可以被看作一个服务。
  3. 容器: 每个部门由一个或多个员工组成。尽管每个员工都是独立的,但他们共享同样的环境。一个员工相当于一个容器。

所有部门都在同一家公司工作并使用该公司的资源,所以所有服务在工程中共享同样的网络、卷等资源

各个部门之间还是会进行交流和协作,所以各个服务之间可以互相通信

同一部门的所有员工都具有相同的工作环境,所以属于同一服务的所有容器都有统一的配置。员工各自做自己的项目,所以容器间有一定的隔离性

在要部署项目的目录创建一个 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 命令以应用更改。

结语

任何技术都有其深度与复杂性,难以通过一篇文章详尽阐述。本文的初衷是为你在遭遇问题时,提供一个寻找解答的方向指引。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值