Docker 浅谈

44 阅读15分钟

Docker 是什么

你可以把 Docker 理解为软件世界的集装箱。就如 Docker logo 所展示的,是一头鲸鱼驮着若干集装箱。Docker 可以允许开发者将应用以及所有依赖项(库、环境等)打包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 或 Windows 机器上。

诞生的背景

应用在开发、测试、生产环境中,由于环境依赖、配置差异等等问题,常导致 “在我这能跑” 的难题。传统虚拟机虽能隔离环境,但资源占用高、启动慢、笨重,无法满足轻量、快速部署的需求。 Docker 便在这种背景下诞生。

Docker 与虚拟机的区别

虚拟机能实现环境隔离,解决部署环境一致性的问题。那么 Docker 又是怎么解决的,有什么优势呢?

一言以蔽之:虚拟机虚拟化硬件,依赖完整的操作系统;而容器虚拟化操作系统,共享主机内核,因此更轻量、更快速。

通俗化的理解就是:

  • 虚拟机就像一栋独栋别墅。拥有自己的地基、墙体、水电系统(虚拟硬件)和完整的家庭空间(操作系统)。私密性好(隔离强),但建造和启动慢,占用土地多(资源消耗大)。

  • Docker 容器就像一栋公寓楼里的一个单间。所有房间共享大楼的地基和主体结构(主机内核),但每个房间有自己独立的墙壁(隔离)、家具(应用与依赖)。启动快(拎包入住),非常节省资源。

核心概念

  Dockerfile

  文本文件,描述如何构建镜像(例如指定基础镜像、安装软件、复制文件等)。

  镜像 Image

  Dockerfile 构建后的产物即镜像(Image)。镜像是只读的,类似于安装系统用的 .iso 文件,它包含了运行应用所需的所有东西(代码、库、环境变量、配置文件)。

    层 Layer

  镜像由层构成,每个层表示文件系统的变更——增加、修改、删除。

  容器 Container

  容器就是实际运行中的实例,基于镜像创建而来,与其他容器隔离。类似于一个轻量级的、隔离的虚拟机(但本质是进程)。

  仓库 Registry

  存储和分发镜像的平台,如 Docker Hub(Docker 官方平台)。

  Docker Compose

  容器的一个最佳实践是每个容器应该只做一件事。但实际项目可能需要同时运行过个容器,并且同时管理容器间的网络连接等。使用 Docker Compose,可以实现在一个 YAML 文件中定义所有容器及其配置。然后使用一条命令便可启动运行。

  例如一个项目包含前端、后端、数据库,分别对应三个镜像,这一个项目可以用一个 docker-compose.yml 文件描述,包含三个镜像,用一条命令就能启动。

Docker 架构

  Docker 采用客户端-服务器架构。Docker 客户端和守护进程可以运行在同一系统上,也可以将 Docker 客户端连接到远程 Docker 守护进程。Docker 客户端和守护进程通过 REST API 进行通信,通信方式可以是 UNIX 套接字或网络接口。

  • Docker 守护进程(dockerd)监听 Docker API 请求,并管理 Docker 对象,如镜像、容器、网络和卷。守护进程还可以与其他守护进程通信,以管理 Docker 服务。

  • Docker 客户端(docker)是许多 Docker 用户与 Docker 交互的主要方式。当你使用 docker run 等命令时,客户端会将这些命令发送给 dockerd,由其执行。docker 命令使用 Docker API。Docker 客户端可以与多个守护进程通信。另一个 Docker 客户端是 Docker Compose,它允许你操作由一组容器组成的应用程序。

Docker 的基础使用场景

用 Dockerfile 构建镜像

以下是一个简单示例,描述了一个前端项目的 Dockfile

# 指定基础镜像,使用官方 nginx 镜像
FROM nginx:alpine

# 删除默认 nginx 静态资源
RUN rm -rf /usr/share/nginx/html/*

# 拷贝 web 项目的 build  文件到 nginx 静态目录下
COPY dist/ /usr/share/nginx/html/

# 拷贝自定义 nginx 配置
COPY default.conf /etc/nginx/conf.d/default.conf

# 暴露端口 80
EXPOSE 80

# 让 NGINX 在前台运行,防止 Docker 容器启动后立即退出
CMD [ "nginx" , "-g" , "daemon off;" ]

如上 FROM/RUN/CMD 等是 Dockerfile 的指令

指令说明
FROM定制的镜像都是基于某个基础镜像,FROM 就是用来指定基础镜像
RUN在构建过程中的执行命令
CMD创建容器时要执行的默认命令
COPY复制指令,从上下文目录中复制文件或者目录到容器内指定路径
EXPOSE声明容器运行时监听的网络端口
ENV在容器内部设置环境变量

构建指令

# 以当前目录为上下文构建名称为 frontend 的镜像,即当前目录需要有 Dockerfile
docker build -t frontend .

frontend 为镜像名称,也可以指定标签如 frontend:v1,frontend:latest,

镜像推送和拉取

如果是为了在其他机器上使用镜像,那我们需要用到镜像仓库。

需要登陆才可使用进行推送操作

# docker 登陆命令
docker login [hub URL]

将本地镜像推送到远程的镜像仓库

docker push my-image:latest

从镜像仓库拉取指定镜像

docker pull my-image:latest

关于拉取镜像:拉取的镜像会存在本地,然后当基于镜像创建容器时,如果指定的镜像本地没有,则会默认从仓库拉取镜像,有则使用本地镜像。

本地可以安装使用 Docker Desktop,查看镜像、容器等,非常方便。

基于镜像创建容器

创建容器使用 docker run 命令,如启动一个基于 ubuntu 的镜像,启动后在前台运行(即命令行不会退出)

docker run ubuntu

一般有几种场景:

后台运行容器

在后台运行 ubuntu 容器并返回容器 ID

docker run -d ubuntu 

交互式运行并分配终端

以交互模式运行 ubuntu 容器,并启动一个 Bash shell

docker run -it ubuntu /bin/bash

临时容器

启动一个 ubuntu 容器,执行 xxxx 命令,完成后自动删除容器

docker run --rm ubuntu sh -c "xxxxx"

另外一下是一些常用的参数:

参数示例含义
--name--name my_container指定容器名称,不指定的话 docker 会随机生成一个名称
-p-p 8080:80端口映射,主机的 8080 端口映射为容器内的 80 端口
-v-v /host/data:/container/data挂载卷,将主机的 /host/data 目录挂载到容器内的 /container/data 目录
-e, --env-e MY_ENV=my_value设置环境变量 MY_ENV=my_value
--network--network host指定网络模式

容器管理

容器启动、停止、重启

docker start my_container
docker stop my_container
docker restart my_container

删除一个或多个已经停止容器

docker rm my_container

在运行中的容器中执行命令

# 在 my_container 容器中执行 ls /app 命令
docker exec my_container ls /app 

查看当前运行的容器

# 查看所有运行的容器
docker ps

# 查看所有容器,包含停止的
docker ps -a

# 显示所有容器 ID
docker ps -aq

# 输出示例
CONTAINER ID   IMAGE               COMMAND                  CREATED       STATUS       PORTS                                     NAMES
09b4274e4356   project__frontend   "/docker-entrypoint.…"   3 hours ago   Up 3 hours   0.0.0.0:8081->80/tcp, [::]:8081->80/tcp   frontend
30486b3d96d0   project__backend    "./wait-for-mysql.sh"    3 hours ago   Up 3 hours   1024/tcp                                  backend
929f7bfcf316   project__mysql      "docker-entrypoint.s…"   3 hours ago   Up 3 hours   3306/tcp, 33060/tcp                       mysql
6d78b99b0e3b   redis:alpine        "docker-entrypoint.s…"   3 hours ago   Up 3 hours   6379/tcp                                  redis

docker ps 常用来查看所有容器的运行状态。容器状态有 7 种:

  • created,已创建,从未启动过的容器

  • running,运行中,通过docker startdocker run启动。

  • paused,已暂停,通过docker pause暂停。

  • restarting,重启中,由于容器指定的重启策略而正在启动。

  • exited,已停止,一个不再运行的容器。例如,容器内的进程已完成,或者容器已通过docker stop命令停止。

  • removing,删除中,执行docker rm,容器正在被移除

  • dead,已失效,例如,由于外部进程占用资源而仅被部分移除的容器。dead的容器无法(重新)启动,只能被移除

查看容器运行的日志

docker logs my_container

# 跟随日志,类似于 tail -f
docker logs -f my_container 

查看容器端口映射信息

docker port my_container

# 输出示例
80/tcp -> 0.0.0.0:8081  # ipv4
80/tcp -> [::]:8081     # ipv6

Docker Compose 的使用场景

实际一个项目,可能会有多个服务组成,即完整运行一个服务可能会基于多个镜像运行多个容器,包含若干配置,以及多个容器协作需要创建网络等。docker compose 就是针对这种情况的解决方案,通过 docker-compose.yml 文件可以配置多个容器组成的服务,以及它们依赖的网络配置等。然后可以用一个命令就能启动这个多容器的项目。

docker-compose.yml

如下所示,docker-compose.yml 定义了 5 个服务,包含数据库服务、后端服务、前端服务

services:  # 服务配置
  mysql:
    container_name: mysql # 容器名称
    build: ./mysql             # 构建目录
    platform: linux/amd64      # 架构类型
    image: project__mysql  # 镜像名称(本地构建可不需要)
    restart: always   # 重启策略
    volumes:          # 依赖卷
      - mysql-data:/var/lib/mysql

  redis:
    container_name: redis
    image: redis:alpine
    restart: always
    volumes:
      - redis-data:/data

  backend:
    container_name: backend
    build: ./backend
    platform: linux/amd64
    image: project__backend
    depends_on:     # 启动顺序依赖
      mysql:
        condition: service_healthy
      redis:
        condition: service_started

  frontend:
    container_name: frontend
    build: ./frontend
    platform: linux/amd64
    image: project__frontend
    ports:     # 端口映射
      - "8081:80"
    depends_on:
      - backend

  frontend-admin:
    container_name: frontend_admin
    build: ./frontend-admin
    platform: linux/amd64
    image: project__frontend-admin
    ports:
      - "8082:80"
    depends_on:
      - backend

volumes:   # 卷配置
  mysql-data:
  redis-data:

docker-compose.yml 所处目录的上下文示意:

.
|-mysql
|  |-Dockerfile
|  `-(other files)
|-backend
|  |-Dockerfile
|  `-(other files)
|-frontend
|  |-Dockerfile
|  `-(other files)
|-frontend-admin
|  |-Dockerfile
|  `-(other files)
`-docker-compose.yml

构建与推送镜像

构建

含有 build 配置的服务,会自动依赖其所指定的目录进行构建,指定目录下需包含 Dockerfile

docker compose build

推送

构建完成后,可以将构建完成的镜像自动推送到镜像仓库,即含有 build 配置的项会自动推送到 image 定义的镜像位置(当然镜像仓库是有权限控制的)

docker compose push

启动命令与过程

启动命令

# 启动服务在前台运行,一旦退出则服务停止
docker compose up 

# 启动服务在后台运行
docker compose up -d

启动的具体过程

  1. 前期准备与验证

    1. 读取 Compose 文件
    2. 验证配置,检查 YAML 语法、服务定义、网络、卷等配置是否正确。
    3. 项目环境,确定项目名称(默认是当前目录名,可通过 -p 指定)。
  2. 构建与拉取镜像

    1. 如果配置了 build,会构建镜像,如果本地构建过,则直接使用缓存,否则会进行构建,如果配置了 --build 则会使用进行构建。
    2. 如果配置了 image,会拉取镜像,如果本地不存在就会从镜像仓库拉取,如果配置了 --pull=always 会始终从镜像仓库拉取。
  3. 创建网络和卷

    1. 创建网络:根据 networks 部分创建自定义网络。默认会为整个项目创建一个名为 {project-name}_default 的网络,所有服务都会连接到这个网络,以便通过服务名进行通信。
    2. 创建卷:根据 volumes 部分创建命名的持久化卷。如果卷已存在,则直接使用。
  4. 启动服务(容器)

    1. 按依赖顺序启动,会根据 depends_on 配置来确定启动顺序。

      1. 例如,一个 web 服务依赖 db 服务,那么 Compose 会先启动 db,然后再启动 web。但注意 depends_on 只能决定启动顺序,并不能保证 web 启动时 db 已经正常运行
    2. 容器创建:为每个服务创建并启动容器,这相当于对每个服务执行 docker run

    3. 重启策略:会根据 restart 策略来管理容器的生命周期

  5. 服务健康检查(如果配置了 healthcheck

    1. 等待健康状态:如果服务配置了健康检查(healthcheck),Compose 会等待容器变为 healthy 状态,然后再启动依赖于它的下一个服务。

    2. depends_on 的区别:

      • depends_on 只控制启动顺序(容器进程运行)。
      • healthcheck + condition: service_healthy 控制就绪顺序(容器内应用真正准备好接收请求)。这是更可靠的方式
  6. 后续

    1. 前台模式:会用一个彩色的输出流输出所有服务的日志,按下 Ctrl+C 会停止所有正在运行的服务

    2. 后台模式(-d 参数):启动命令会立即返回,只打印出创建了哪些容器。之后可以用docker compose logs 来查看日志,使用 docker compose ps 查看状态。

使用启动命令时常用的一些参数

参数示例含义
-fdocker compose -f xxx.yml up用于指定某个 docker compose yml 配置启动,默认是当前目录下的 docker-compose.yml 文件
-pdocker compose -p app up指定启动项目名称,默认是以当前的目录名
--builddocker compose up --build启动时强制重新构建镜像,即使镜像已存在
--pulldocker compose up --pull=指定启动前拉镜像的策略,
- always,总是从远程拉镜像,即使本地已经有了,保证运行时是最新镜像
- missing,(默认值)如果本地不存在指定的镜像,则从仓库拉取。如果本地已存在,则直接使用本地镜像
- never,从不拉取镜像。如果本地不存在该镜像,则命令失败

运维:查看、停止

查看日志

docker compose logs 

停止并删除 docker compose 所创建的容器

docker compose down

重启 docker compose 所创建的容器

docker compose restart

网络 Network

网络模式

安装 Docker 时,它会自动创建三个网络,bridge(创建容器默认连接到此网络)、 none 、host。

运行容器时,可以指定容器运行的网络模式。

  • Host

    • 通过 --network=host 指定
    • 容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口
  • Bridge

    • 通过 --network=bridge 指定,如果不指定,会默认以 brdge 模式
    • 为每一个容器分配、设置IP等,并将容器连接到一个 docker0 虚拟网桥,通过 docker0 网桥以及 Iptables nat 表配置与宿主机通信
  • None

    • 通过 --network=none 指定
    • 关闭容器的网络功能
  • Container

    • 通过 --network=container:NAME_or_ID 指定
    • 创建的容器不会创建自己的网卡,配置自己的IP,而是和一个指定的容器共享IP、端口范围。
  • 指定自定义网络

相关命令

# 列出所有网络
docker network ls
# 创建一个新的网络
docker network create <network>
# 删除指定的网络
docker network rm <network>
# 连接容器到网络
docker network connect <network> <container>
# 断开容器与网络的连接
docker network disconnect <network> <container>

卷 Volume

卷是容器的持久性数据存储,由 Docker 创建和管理。可以使用docker volume create命令显式创建卷,Docker 也可以在创建容器或服务时创建卷。

创建卷时,它会存储在 Docker 主机上的一个目录中。将卷挂载到容器时,挂载到容器中的就是这个目录。这与绑定挂载的工作方式类似,不同之处在于卷由 Docker 管理,并且与主机的核心功能隔离开来。

卷相关的命令

# 列出所有卷
docker volume ls
# 创建一个新的卷
docker volume create <volume>
# 删除指定的卷
docker volume rm <volume>
# 显示卷的详细信息
docker volume inspect <volume>

Docker in Docker (DinD)

Docker in Docker(简称 DinD)指的是在一个 Docker 容器内部运行一个完整的 Docker 守护进程(Docker Daemon)和客户端(Docker CLI)。

简单来说,就是“容器里的容器”。你可以在一个容器中执行 docker builddocker run 等命令,就像在宿主机上一样。

可以使用官方的 docker:dind 镜像进行构建。这个镜像经过特殊配置,可以在容器内启动并运行 Docker 守护进程。运行此类容器通常需要 --privileged(特权)模式。

优点

  • 环境隔离:构建环境与宿主机完全隔离,非常干净,不会相互污染。

  • 安全性相对较高:与 DoD 相比,它对宿主机的影响更小,进程和文件系统都是隔离的。

缺点

  • 性能开销大:在容器内运行完整的 Docker 守护进程和存储驱动(通常使用效率较低的 vfs),资源占用高。

  • 无法共享缓存:每次启动一个 DinD 容器,都是一个全新的环境,之前构建的镜像缓存会丢失,导致构建速度慢。

业务场景

业务中需要通过 docker compose 启动多个容器,而部署环境的容器中使用了 host 的网络模式,内部再使用 docker compose 启动会默认创建网络,由于要部署的环境众多,这会导致 host 网络无法分配的问题。此时需要使用 Docker in Docker 的方式进行部署,隔绝项目网络与部署环境的网络。

结语

以上是业务中使用到的 Docker 知识的简单总结,本文对 Docker 相关知识的多个方面均有涉及,但受限于篇幅和定位,各部分内容的探讨尚属浅显。若希望深入理解 Docker 的底层原理、高级应用及最佳实践,可进一步阅读专业技术文档如 Docker 官方网站等,以获取更系统、全面的知识体系。