Docker Note

本文最后更新于 2026年7月19日 凌晨

序

Docker——学过,看过,用过,都很零散,这次决定系统梳理一遍。

梳理一下我不太熟悉的东西:

  • Docker Compose
  • Docker 的网络相关

这次着重补一下这两块。

学习主线使用《Docker 从入门到实践》这本书,辅可能会有读到的文档和看的教学视频的内容加以补充。

Docker 简介

  • 轻量级的虚拟化技术
  • 将应用程序及其依赖环境可以被打包成一个标准化的单元,在满足架构、内核能力和外部依赖前提的环境中高度一致地运行。

和虚拟机的差别

传统虚拟机:

  • 虚拟出一套完整的硬件,在其之上运行一个完整的 OS

Docker 容器:

  • 容器运行与宿主的内核,容器没有自己的内核,没有对于硬件的虚拟
特性 Docker 容器 传统虚拟机
启动速度 秒级 分钟级
资源占用 MB 级别 GB 级别
性能 接近原生 有明显损耗
隔离级别 进程级隔离 完全隔离
单机数量 可运行上千个 通常几十个

Docker 的优势

  • 一次构建,到处运行
  • 环境一致性
  • 启动速度快
    • 对 CI/CD,弹性扩容很好
  • 资源效率相比虚拟机高
  • 持续交付和部署
    • 契合 DevOps 的工作流程:代码提交、
  • 轻松迁移
  • 微服务架构的基石

Docker 概念

镜像

  • 只读的模板,包含运行应用所需要的一切。

    镜像只读,不包含动态数据,构建之后内容不改变。

  • 一个镜像可以创建多个容器,而镜像本身保持不变。

镜像与 OS 的关系

OS 分为内核和用户空间:

flowchart TD
    subgraph UserSpace ["用户空间"]
        direction TB
        App["应用程序、工具、库、配置文件...<br/>(这部分被打包成 Docker 镜像)"]
    end

    subgraph KernelSpace ["Linux 内核"]
        direction TB
        Kernel["容器共享宿主机的内核"]
    end

    UserSpace --- KernelSpace

Docker 容器:本质是一个自己的 root 文件系统,挂载在 OS 的内核之下运行,不包含自己的内核。

分层存储

分成很多层,每一层都是基于上一层运行的。

体现在 Dockerfile 的不同的行中。

举一个陷阱式的例子来理解:

## 错误示范 ❌

FROM ubuntu:24.04
RUN apt-get update
RUN apt-get install -y build-essential  # 安装编译工具(约 200MB)
RUN make && make install                  # 编译应用
RUN apt-get remove build-essential        # 试图删除编译工具

## 结果:镜像仍然包含 200MB 的编译工具!
## 正确做法 ✅

FROM ubuntu:24.04
RUN apt-get update && \
    apt-get install -y build-essential && \
    make && make install && \
    apt-get remove -y build-essential && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

## 在同一层完成安装、使用、清理

镜像的标识

多种标识方式

  • 镜像名称和标签:

    • 格式:[仓库地址/]仓库名[:标签],但也有缩写形式,举例:
    • 完整格式 registry.example.com/myproject/myapp:v1.2.3
    • 简写,默认仓库地址为官方的 Docker Hub:nginx:1.28
    • 省略标签,nginx,等价于 nginx:latest
  • 镜像 ID

    $ docker images
    REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
    nginx        latest    a6bd71f48f68   2 weeks ago    187MB
    ubuntu       24.04     ca2b0f26964c   3 weeks ago    78.1MB

    此处的 IMAGE ID 即为。

  • 摘要

    • digest
    • 镜像内容的唯一标识
    • 推荐使用这个代替标签

容器

  • 容器是镜像的运行实例。
  • 如果把镜像比作程序,那么容器就是进程。
  • 用 OOP 术语来说:镜像是类 (Class),容器是对象 (Instance)。

容器的特征:

  • 一个镜像可以创建多个容器
  • 每个容器相互独立,互不影响
  • 容器可以被创建、启动、停止、删除、暂停

容器的本质

容器的本质是一个特殊的进程。但是与一般的进程相比,有如下特质:

  • 独立的进程空间:容器看不到宿主机上的其他进程。
  • 独立网络环境:在默认网络模式下,容器通常拥有独立的网络命名空间,并可分配独立 IP;使用 host 或 container: 等模式时则例外。
  • 独立文件系统:容器拥有独立的 root 目录。
  • 独立的用户空间:鉴于我不是很理解 Linux 的用户空间,这里暂且 TODO。

容器的存储层

镜像层 + 容器层:假设容器基于的镜像有 $N$ 层,那么容器就是构建一个第 $N+1$ 层,可写,作为容器存储层。

Copy-on-Write 写时复制:

当容器需要修改镜像层中的文件时:

  1. Docker 将该文件 复制 到容器存储层
  2. 在容器层中进行修改
  3. 原始镜像层保持不变

容器存储层的生命周期:和容器绑定,删除容器同时删除容器存储层。

数据持久化范式:Docker 的最佳实践认为容器存储层应该保持无状态性,如果需要保留,应当使用数据卷 / 绑定挂载。

容器的生命周期

存在的状态:

  • Created
  • Running
  • Paused
  • Stopped
  • Deleted

容器和进程的关系

容器的生命周期 = 主进程 (PID 1) 的生命周期

运行:

docker run ubuntu

容器会立刻退出:默认启动的程序没有持续工作,PID 1 很快结束了。使用:

docker run -it ubuntu bash

则 bash 成为 PID 1;只要你不退出这个 shell,容器就会继续运行。

容器的隔离是如何实现的

使用 Linux 的 Namespace 机制。

Docker Registry

核心概念

Docker Registry 是存储和分发 Docker 镜像的服务,类似于代码的 GitHub 或包管理的 npm。

Docker Registry 中可以包含多个 Repository,每个 Repository 可以包含多个 Tag。

这也就是为什么,镜像的完整名称是 [registry 地址/][用户名/]仓库名[:标签]。

如果不指定 Registry 地址,默认使用 Docker Hub。如果不指定标签,默认使用 latest。

公共 Registry 服务

Docker Hub 是最大的公共 Registry,也是 Docker 的默认 Registry。

  • 免费账户可以创建公开仓库
  • 免费个人账户可创建 1 个私有仓库;更高套餐支持更多私有仓库

还有一些镜像源比如 Github Container Registry,Google 的,阿里巴巴的,腾讯的等。

可以配置镜像加速器来加速:

// /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://your-accelerator-url"
  ]
}

私有 Registry

云厂商有提供服务,可以自己看,比如阿里云 ACR、腾讯云 TCR、AWS ECR 等。

有时需要使用 docker login 来登录到 Docker Registry。

镜像的推送和拉取

开发者机器                    Registry                    生产服务器
     │                           │                             │
     │  docker build             │                             │
     │  构建镜像                  │                             │
     │                           │                             │
     │  docker push ─────────────▶                             │
     │  推送镜像                  │  存储镜像                   │
     │                           │                             │
     │                           │  ◀───────────── docker pull │
     │                           │                  拉取镜像    │
     │                           │                             │
     │                           │                  docker run │
     │                           │                  运行容器    │

使用镜像

获取镜像

代码BASH · 23 行
## 完整格式

$ docker pull docker.io/library/ubuntu:24.04

## 省略 Registry(默认 Docker Hub)

$ docker pull library/ubuntu:24.04

## 省略 library(官方镜像)

$ docker pull ubuntu:24.04

## 省略标签(默认 latest)

$ docker pull ubuntu

## 拉取第三方镜像

$ docker pull bitnami/redis:latest

## 从其他 Registry 拉取

$ docker pull ghcr.io/username/myapp:v1.0

下载内容解析

$ docker pull ubuntu:24.04
24.04: Pulling from library/ubuntu
92dc2a97ff99: Pull complete
be13a9d27eb8: Pull complete
c8299583700a: Pull complete
Digest: sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04
输出内容 说明
Pulling from library/ubuntu 正在从官方 ubuntu 仓库拉取
92dc2a97ff99: Pull complete 各层的下载状态 (显示层 ID 前 12 位)
Digest: sha256:... 镜像内容的唯一摘要
docker.io/library/ubuntu:24.04 镜像的完整名称

可以看到镜像是分层下载的。

对于 pull:

  • --quiet -q 可以静默安装
  • --platform 可以指定平台架构

关于摘要

查看镜像摘要:

$ docker images --digests ubuntu
REPOSITORY   TAG     DIGEST                                                                    IMAGE ID
ubuntu       24.04   sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26   ca2b0f26964c

然后使用摘要拉取:

$ docker pull ubuntu@sha256:4bc3ae6596938cb0d9e5ac51a1152ec9dcac2a1c50829c74abd9c4361e321b26

生产环境使用摘要而非标签,因为标签可能被覆盖,摘要则是不可变的。

磁盘空间不足的解决方案

## 清理未使用的镜像

$ docker image prune

## 清理所有未使用资源

$ docker system prune

查看镜像

基本用法

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
redis        latest    5f515359c7f8   5 days ago     183MB
nginx        latest    05a60462f8ba   5 days ago     181MB
ubuntu       24.04     329ed837d508   3 days ago     78MB
ubuntu       noble     329ed837d508   3 days ago     78MB

关于 ubuntu:24.04 和 ubuntu:noble:

ubuntu:24.04 是具体版本号,ubuntu:noble 是发布代号。

拥有相同的 IMAGE ID,是同一个镜像的不同标签,只占用一份存储空间。

其实基本命令是 docker image,而 docker images 是 docker image ls 的 alias。

理解镜像的大小

本地大小和 Docker Hub 显示的大小:

  • 前者是本地解压后的实际大小
  • 后者是压缩后的网络传输大小

实际磁盘占用:

  • 由于镜像分层存储,不同的镜像共享不同的层
  • 故 $\sum{\text{size}} \gt \text{实际磁盘占用}$。

如何查看实际空间的占用:

$ docker system df
TYPE            TOTAL   ACTIVE   SIZE      RECLAIMABLE
Images          15      3        2.5GB     1.8GB (72%)
Containers      5       2        100MB     80MB (80%)
Local Volumes   8       2        500MB     400MB (80%)
Build Cache     0       0        0B        0B

查找特定的镜像(过滤镜像)

按镜像名过滤:

$ docker images ubuntu

$ docker images ubuntu:24.04

使用过滤器:

--filter -f

有一系列的过滤条件,这个感觉记了也记不住,知道就好。

虚悬镜像 dangling images

仓库名和标签都显示为 <none> 的镜像。

产生原因:

  • 镜像重新构建:新镜像使用了旧镜像的标签,旧镜像标签被移除
  • docker pull 更新:拉取更新版本时,旧版本失去标签(latest)

处理方式:

  • 列出:docker images -f dangling=true
  • 删除:docker image prune

中间层镜像

docker images 只列出顶层镜像,为了查看中间镜像,使用 docker images -a。

永远不需要手动删除中间层镜像。

格式化输出

主要是命令之间的搭配使用。

只输出 ID:docker images -q

举例:删除所有 redis 镜像:docker rmi $(docker images -q redis)

不过其实我觉得个人开发而言,用 Docker Desktop GUI 管理更方便。

对于 Agent 而言,这些东西自然语言阐述即可。

显示摘要:docker images --digests

删除镜像

基本用法

$ docker image rm [选项] <镜像1> [<镜像2> ...]

docker rmi 是 docker image rm 的简写,两者等效。

镜像标识方式

  • 短 ID(可以标识的前几位)
  • 完整 ID
  • 镜像名:标签
  • 镜像摘要
    • 最精确,适合 CI/CD 场景

删除镜像的输出信息

$ docker rmi redis:alpine
Untagged: redis:alpine
Untagged: redis@sha256:f1ed3708f538b537eb9c2a7dd50dc90a706f7debd7e1196c9264edeea521a86d
Deleted: sha256:501ad78535f015d88872e13fa87a828425117e3d28075d0c117932b05bf189b7
Deleted: sha256:96167737e29ca8e9d74982ef2a0dda76ed7b430da55e321c071f0dbff8c2899b
Deleted: sha256:32770d1dcf835f192cafd6b9263b7b597a1778a403a109e2cc2ee866f74adf23
  • Untagged:移除镜像的标签
  • Deleted:删除镜像的存储层

删除流程:

先检查指向该镜像的标签并逐个 untag,然后再逐层删除镜像,被使用则保留,不被使用则删除。

批量删除

这里大概自然语言描述一下能做到哪些吧:

  • 删除所有虚悬镜像,docker image prune
  • 删除一段时间内未使用的镜像:docker image prune -a --filter "until=24h"
  • 按照条件删除,比如:
    • 删除所有 redis 镜像
    • 删除某个时间点之前的镜像
    • 删除某个版本号之前的镜像

删除失败的原因

  • 存在容器依赖这个镜像
  • 这个镜像作为中间层被其他镜像依赖
  • 多个标签指向同一个镜像,比如 ubuntu 的 24.04 和 latest 同时指向同一个 IMAGE ID

清理策略

开发环境:

  • 定期清理 dangling images
  • 清理未使用的资源 docker system prune -a

CI/CD:

  • 只保留最近使用过的镜像(即一键删除一段时间内未使用过的镜像)

查看空间占用:

  • docker system df

使用 docker commit

把某个容器当前在文件系统上的改动,固化成一个新的 Docker 镜像。

命令格式:

$ docker commit [OPTIONS] 容器名或容器ID 新镜像名[:标签]

举个例子,进入容器之后,修改了里面文件系统的一些内容,

执行

docker commit dev my-ubuntu:v1

实际上做的:

原镜像的只读层 + dev 容器的可写层 -> 转化成新的只读镜像层 -> 新的镜像

原理是让新镜像继续复用原来的镜像层,再增加一层,用来保存这个容器相对于原镜像产生的文件系统变化。

缺点

使用 docker commit 定制的镜像存在如下问题:

  • 执行命令会连着根式地修改一大堆无关的文件,可以通过 docker diff webserver 看出;
  • 对镜像的操作都是黑箱操作;
  • 层数会膨胀

使用 Dockerfile 定制镜像

解决上面所说的问题。

Dockerfile 是一个包含了一条条的指令的文本文件:

  • 会修改文件系统的指令通常会创建新层;
  • 而 LABEL、CMD 这类只修改镜像元数据的指令,则不会新增文件系统层。

创建 Dockerfile

比较详细的命令后面单开一节介绍。

不过我觉得也没什么必要,知道基本概念最重要。

FROM 指定基础镜像:

所谓定制镜像,是以一个镜像为基础,在其上进行定制。所以需要 FROM 指定的基础镜像。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch
...

如果以 scratch 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言开发的应用很多会使用这种方式来制作镜像,这也是有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令行命令:

  • shell 格式:RUN <命令>
  • exec 格式:RUN ["可执行文件", "参数1", "参数2"]

每一个 RUN 指令都会产生一个新的镜像层。为了减少镜像体积和层数,我们通常会将多个命令合并到一个 RUN 指令中执行。

构建镜像

docker build [选项] <上下文路径/URL/->

执行之后的输出,含义还挺明显的,没什么必要单独扯出来记。

镜像构建上下文

关于什么是上下文路径:构建器可以访问到的文件集合。

比如 Dockerfile 里面会写 COPY 和 RUN 指令,假设有这样的:

COPY ./package.json /app/

这里复制的就是上下文目录下的 package.json。

而假设写成:

COPY ../package.json /app

这里的 .. 属于越界,构建器都无法读取上下文之外的宿主机文件。

由此可见,上下文,和 Dockerfile 所在的目录,没有关系。假设把上下文直接指定成 / ,理论是可行的,不过 BuildKit 的可见上下文会过大导致构建缓慢甚至失败。

而所谓的 .dockerignore 文件(通常置于项目根目录下,和 Dockerfile 平级,采用和 .gitignore 一样的 Glob 语法),就是为了忽略掉在构建时不希望传给 Docker 引擎的文件。

其余 docker build 的用法

  • 从 Git Repo 中构建
  • 用给定的压缩包构建
  • 从标准输入中读取 Dockerfile 进行构建 cat Dockerfile | docker build -

操作容器

启动

启动容器有两种方式:

  • 新建并启动:基于镜像创建新容器 docker run
  • 重新启动:将已终止的容器重新运行 docker start

由于 Docker 容器非常轻量,实际使用中常常是随时删除和新建容器,而不是反复重启同一个容器。

新建并启动

docker run [选项] 镜像 [命令] [参数...]

交互式容器:

$ docker run -it ubuntu:24.04 /bin/bash
root@af8bae53bdd3:/#
参数 作用
-i 保持标准输入 (stdin) 打开,允许输入
-t 分配伪终端 (pseudo-TTY),提供终端界面
-it 两者组合使用,获得交互式终端

启动选项

选项 说明 示例
-d 后台运行 (detach) docker run -d nginx:latest
-it 交互式终端 docker run -it ubuntu:24.04 bash
--name 指定容器名称 docker run --name myapp nginx:latest
--rm 退出后自动删除容器 docker run --rm ubuntu:24.04 echo hi

端口映射:

## 将容器的 80 端口映射到宿主机的 8080 端口

$ docker run -d -p 8080:80 nginx:latest

## 只绑定到 localhost

$ docker run -d -p 127.0.0.1:8080:80 nginx:latest

映射端口这一点很重要,比如像 nginx 等服务,不暴露出端口,外部根本无法访问。

网络相关的东西,等到网络专题再细究吧。

数据卷挂载

略,懒得记了

环境变量

## 设置单个环境变量

$ docker run -e MYSQL_ROOT_PASSWORD=secret mysql

## 从文件加载环境变量

$ docker run --env-file .env myapp

这个倒是,容器有时候是需要诸如 .env 文件里面的环境变量的,假设 .dockerignore 了之后。

但是感觉把 .env 文件 docker ignore 掉的意义也不大,为什么要这样做呢?

Answer from ChatGPT,我归纳一下:

首先,肯定不能把 .env 放进镜像中啊(回忆一下本节的标题「新建并启动」),因为镜像是要分发的,分发出去,直接访问里面的 .env 文件不完蛋了,所以肯定要忽略掉。

其次,这样做的话,镜像和运行环境就解耦合了。

资源限制:

## 限制内存

$ docker run -m 512m nginx:latest

## 限制 CPU

$ docker run --cpus=1.5 nginx:latest

重新启动容器

使用 docker start,后面跟上容器名。

获取容器名,使用 docker ps -a。

守护态运行

当在终端运行一个程序时,有两种模式:

  • 前台运行:程序占用当前终端,输出直接显示,关闭终端程序就停止
  • 后台运行:程序在后台执行,不占用终端,终端关闭也不影响程序

Docker 容器默认是 前台运行 的。使用 -d (detach) 参数可以让容器在后台运行。

理解为什么容器会立即退出

核心原理:容器的生命周期与主进程绑定。

举个例子,即使使用了 -d 参数运行:

$ docker run -d ubuntu:24.04

再使用 docker ps 也看不见容器在运行,原因:

  1. 容器启动
  2. 没有指定命令,默认执行 /bin/bash
  3. 但没有交互式终端 (没有 -it 参数),bash 发现没有输入源
  4. bash 立即退出
  5. 主进程退出,容器停止

-d 参数是让容器“在后台运行”,能运行多久取决于主进程

查看容器

$ docker container ls
$ docker ps
# 前者是后者的简写

# 查看日志
$ docker container logs 77b2dc01fe0f

# 实时查看日志
$ docker container logs -f 77b2dc01fe0f

对于一次性任务,使用 --rm 参数让容器退出后自动删除。

终止

docker stop 优雅终止

docker kill 直接终止

进入容器

-d 启动之后,有时候需要进入容器进行操作。

## 进入容器并启动交互式 shell

$ docker exec -it 容器名 /bin/bash

## 或使用 sh(适用于 Alpine 等精简镜像)

$ docker exec -it 容器名 /bin/sh

导入和导出

docker export docker import

删除

docker rm

docker rm 是 docker container rm 的简写,两者等效。

Dockerfile 指令详解

数据管理

数据卷

首先需要 $ docker volume create my-vol 创建一个数据卷,然后新建容器的时候,使用 --mount 或者 -v 挂载数据卷。

挂载主机目录

数据卷(volume):由 Docker 管理的存储。你只需要指定卷名,Docker 负责在主机上创建、保存和定位实际数据目录。

docker run -v mydata:/app/data nginx
# 名为 mydata 的数据卷,映射到容器的 /app/data 目录

挂载主机目录(bind mount):把你指定的主机目录直接映射到容器中。

docker run -v /home/user/data:/app/data nginx

核心区别:

  • volume:你管卷名,Docker 管实际存放位置,适合数据库等持久化数据。
  • bind mount:你自己管理主机路径,适合开发时挂载代码、配置文件。
  • 可移植性:volume 不依赖具体主机路径;bind mount 依赖主机目录结构。

网络配置