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;使用 hostcontainer: 等模式时则例外。
  • 独立文件系统:容器拥有独立的 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.04ubuntu:noble

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

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

其实基本命令是 docker image,而 docker imagesdocker 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

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

格式化输出

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

只输出 IDdocker images -q

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

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

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

显示摘要docker images --digests

删除镜像

基本用法

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

docker rmidocker 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.04latest 同时指向同一个 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 是一个包含了一条条的指令的文本文件:

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

创建 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 里面会写 COPYRUN 指令,假设有这样的:

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 rmdocker 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 依赖主机目录结构。

网络配置