docker

文章目录

  • docker
    • [第一章 容器生态系统](#第一章 容器生态系统)
      • [001 了解容器技术](#001 了解容器技术)
      • [002 容器生态系统](#002 容器生态系统)
      • [003 容器生态系统(续)](#003 容器生态系统(续))
      • [004 安装docker](#004 安装docker)
      • [005 docker C/S分离部署](#005 docker C/S分离部署)
    • [第2章 容器架构](#第2章 容器架构)
      • [006 容器What,Why,How](#006 容器What,Why,How)
        • [What - 什么是容器?](#What - 什么是容器?)
        • [Why - 为什么需要容器?](#Why - 为什么需要容器?)
        • [How - 容器是如何工作的?](#How - 容器是如何工作的?)
      • Docker介绍
      • [007 Docker架构详解](#007 Docker架构详解)
      • [008 Docker组件如何协作?](#008 Docker组件如何协作?)
    • [第3章 镜像](#第3章 镜像)
    • [第4章 容器](#第4章 容器)
      • [022 如何运行容器?](#022 如何运行容器?)
      • [023 两种进入容器的方法](#023 两种进入容器的方法)
      • [024 运行容器的最佳实践](#024 运行容器的最佳实践)
      • [025 容器常用操作](#025 容器常用操作)
      • [026 一张图搞懂容器所有操作](#026 一张图搞懂容器所有操作)
      • [027 限制容器对内存的使用](#027 限制容器对内存的使用)
      • [028 限制容器对CPU的使用](#028 限制容器对CPU的使用)
      • [029 export和import容器](#029 export和import容器)
      • [030 实现容器的底层技术](#030 实现容器的底层技术)
          • [**UTS namespace**](#UTS namespace)
          • [**IPC namespace**](#IPC namespace)
          • [**PID namespace**](#PID namespace)
          • [**Network namespace**](#Network namespace)
          • [**User namespace**](#User namespace)
    • [第5章 网络](#第5章 网络)
      • [031 none和host网络的适用场景](#031 none和host网络的适用场景)
        • [**none 网络**](#none 网络)
        • [**host 网络**](#host 网络)
      • [032 学容器必须懂brige网络](#032 学容器必须懂brige网络)
      • [033 如何自定义容器网络?](#033 如何自定义容器网络?)
      • [034 理解容器之间的连通性](#034 理解容器之间的连通性)
      • [035 容器通信的三种方式](#035 容器通信的三种方式)
        • [**IP 通信**](#IP 通信)
        • [**Docker DNS Server**](#Docker DNS Server)
        • [**joined 容器**](#joined 容器)
      • [036 容器如何访问外部世界](#036 容器如何访问外部世界)
      • [036 容器如何访问外部世界](#036 容器如何访问外部世界)

docker

第一章 容器生态系统

001 了解容器技术

  1. 系统讲解当前最流行的容器技术。
    从容器的整个生态环境到各个具体的技术,从整体到细节逐一讨论。
  2. 重实践并兼顾理论。
    从实际操作的角度带领大家学习容器技术。

为什么要学这个

简单回答是:容器技术非常热门,但门槛高

容器技术是继大数据和云计算之后又一炙手可热的技术,而且未来相当一段时间内都会非常流行。

对 IT 行业来说,这是一项非常有价值的技术。而对 IT 从业者来说,掌握容器技术是市场的需要,也是提升自我价值的重要途径。

现在以 Docker 为代表的容器技术来了,而且关注度越来越高,这一点可以从 google 趋势 中 Docker 的搜索上升趋势(蓝色曲线)中清楚看到。

002 容器生态系统

对于像容器这类平台级别的技术,通常涉及的知识范围会很广,相关的软件,解决方案也会很多,初学者往往容易迷失。

那怎么办呢?

我们可以从生活经验中寻找答案。

当我们去陌生城市旅游想了解一下这个城市一般我们会怎么做?

我想大部分人应该会打开手机看一下这个城市的地图:

  1. 城市大概的位置和地理形状是什么?
  2. 都由哪几个区或县组成?
  3. 主要的交通干道是哪几条?

同样的道理,学习容器技术我们可以先从天上鸟瞰一下:

  1. 容器生态系统包含哪些不同层次的技术?
  2. 不同技术之间是什么关系?
  3. 哪些是核心技术哪些是辅助技术?

首先得对容器技术有个整体认识,之后我们的学习才能够有的放矢,才能够分清轻重缓急,做到心中有数,这样就不容易迷失了。

接下来我会根据自己的经验帮大家规划一条学习路线,一起探索容器生态系统。

学习新技术得到及时反馈是非常重要的,所以我们马上会搭建实验环境,并运行第一个容器,感受什么是容器。

千里之行始于足下,让我们从了解生态系统开始吧。

鸟瞰容器生态系统

一谈到容器,大家都会想到 Docker。

Docker 现在几乎是容器的代名词。确实,是 Docker 将容器技术发扬光大。同时,大家也需要知道围绕 Docker 还有一个生态系统。Docker 是这个生态系统的基石,但完善的生态系统才是保障 Docker 以及容器技术能够真正健康发展的决定因素。

大致来看,容器生态系统包含核心技术、平台技术和支持技术。

下面分别介绍。

容器核心技术

容器核心技术是指能够让 container 在 host os上运行起来的那些技术。

这些技术包括容器规范、容器 runtime、容器管理工具、容器定义工具、Registry 以及 容器 OS,下面分别介绍。

容器规范

容器不光是 Docker,还有其他容器,比如 CoreOS 的 rkt、红帽公司podman、containerd。为了保证容器生态的健康发展,保证不同容器之间能够兼容,包含 Docker、CoreOS、Google在内的若干公司共同成立了一个叫 Open Container Initiative(OCI) 的组织,其目是制定开放的容器规范。

目前 OCI 发布了两个规范:runtime spec 和 image format spec。

有了这两个规范,不同组织和厂商开发的容器能够在不同的 runtime 上运行。这样就保证了容器的可移植性和互操作性。

容器 runtime

runtime 是容器真正运行的地方。runtime 需要跟操作系统 kernel 紧密协作,为容器提供运行环境。

如果大家用过 Java,可以这样来理解 runtime 与容器的关系:

Java 程序就好比是容器,JVM 则好比是 runtime。JVM 为 Java 程序提供运行环境。同样的道理,容器只有在 runtime 中才能运行。

lxc、runc 和 rkt 是目前主流的三种容器 runtime。

lxc 是 Linux 上老牌的容器 runtime。Docker 最初也是用 lxc 作为 runtime。

runc 是 Docker 自己开发的容器 runtime,符合 oci 规范,也是现在 Docker 的默认 runtime。

rkt 是 CoreOS 开发的容器 runtime,符合 oci 规范,因而能够运行 Docker 的容器。

容器管理工具

光有 runtime 还不够,用户得有工具来管理容器啊。容器管理工具对内与 runtime 交互,对外为用户提供 interface,比如 CLI。这就好比除了 JVM,还得提供 java 命令让用户能够启停应用不是。

lxd 是 lxc 对应的管理工具。

runc 的管理工具是 docker engine。docker engine 包含后台 deamon 和 cli 两个部分。我们通常提到 Docker,一般就是指的 docker engine。

rkt 的管理工具是 rkt cli。

容器定义工具

容器定义工具允许用户定义容器的内容和属性,这样容器就能够被保存,共享和重建。

docker image 是 docker 容器的模板,runtime 依据 docker image 创建容器。

dockerfile 是包含若干命令的文本文件,可以通过这些命令创建出 docker image。

ACI (App Container Image) 与 docker image 类似,只不过它是由 CoreOS 开发的 rkt 容器的 image 格式。

Registry

容器是通过 image 创建的,需要有一个仓库来统一存放 image,这个仓库就叫做 Registry。

企业可以用 Docker Registry 构建私有的 Registry。

Docker Hub(https://hub.docker.com) 是 Docker 为公众提供的托管 Registry,上面有很多现成的 image,为 Docker 用户提供了极大的便利。

Quay.iohttps://quay.io/)是另一个公共托管 Registry,提供与 Docker Hub 类似的服务。

容器 OS

由于有容器 runtime,几乎所有的 Linux、MAC OS 和 Windows 都可以运行容器。但这不并没有妨碍容器 OS 的问世。

容器 OS 是专门运行容器的操作系统。与常规 OS 相比,容器 OS 通常体积更小,启动更快。因为是为容器定制的 OS,通常它们运行容器的效率会更高。

目前已经存在不少容器 OS,CoreOS、atomic 和 ubuntu core 是其中的杰出代表。

下一节继续介绍容器平台技术和容器支持技术。

003 容器生态系统(续)

容器生态系统包含核心技术、平台技术和支持技术三个方面。上一节我们讨论了核心技术,今天讨论另外两个部分。

容器平台技术

容器核心技术使得容器能够在单个 host 上运行。而容器平台技术能够让容器作为集群在分布式环境中运行。

容器平台技术包括容器编排引擎、容器管理平台和基于容器的 PaaS。

容器编排引擎

基于容器的应用一般会采用微服务架构。在这种架构下,应用被划分为不同的组件,并以服务的形式运行在各自的容器中,通过 API 对外提供服务。为了保证应用的高可用,每个组件都可能会运行多个相同的容器。这些容器会组成集群,集群中的容器会根据业务需要被动态地创建、迁移和销毁。

大家可以看到,这样一个基于微服务架构的应用系统实际上是一个动态的可伸缩的系统。这对我们的部署环境提出了新的要求,我们需要有一种高效的方法来管理容器集群。而这,就是容器编排引擎要干的工作。

所谓编排(orchestration),通常包括容器管理、调度、集群定义和服务发现等。通过容器编排引擎,容器被有机的组合成微服务应用,实现业务需求。

docker swarm 是 Docker 开发的容器编排引擎。

kubernetes 是 Google 领导开发的开源容器编排引擎,同时支持 Docker 和 CoreOS 容器。

mesos 是一个通用的集群资源调度平台,mesos 与 marathon 一起提供容器编排引擎功能。

以上三者是当前主流的容器编排引擎。

容器管理平台

容器管理平台是架构在容器编排引擎之上的一个更为通用的平台。通常容器管理平台能够支持多种编排引擎,抽象了编排引擎的底层实现细节,为用户提供更方便的功能,比如 application catalog 和一键应用部署等。

Rancher 和 ContainerShip 是容器管理平台的典型代表。

基于容器的 PaaS

基于容器的 PaaS 为微服务应用开发人员和公司提供了开发、部署和管理应用的平台,使用户不必关心底层基础设施而专注于应用的开发。

Deis、Flynn 和 Dokku 都是开源容器 PaaS 的代表。

容器支持技术

下面这些技术被用于支持基于容器的基础设施。

容器网络

容器的出现使网络拓扑变得更加动态和复杂。用户需要专门的解决方案来管理容器与容器,容器与其他实体之间的连通性和隔离性。

docker network 是 Docker 原生的网络解决方案。除此之外,我们还可以采用第三方开源解决方案,例如 flannel、weave 和 calico。不同的方案设计和实现方式不同,各有优势和特定,我们可以根据实际需要来选型。

服务发现

动态变化是微服务应用的一大特点。当负载增加时,集群会自动创建新的容器;负载减小,多余的容器会被销毁。容器也会根据 host 的资源使用情况在不同 host 中迁移,容器的 IP 和端口也会随之发生变化。

在这种动态的环境下,必须要有一种机制让 client 能够知道如何访问容器提供的服务。这就是服务发现技术要完成的工作。

服务发现会保存容器集群中所有微服务最新的信息,比如 IP 和端口,并对外提供 API,提供服务查询功能。

etcd、consul 和 zookeeper 是服务发现的典型解决方案。

监控

监控对于基础架构非常重要,而容器的动态特征对监控提出更多挑战。

针对容器环境,已经涌现出很多监控工具和方案。

docker ps/top/stats 是 Docker 原生的命令行监控工具。除了命令行,Docker 也提供了 stats API,用户可以通过 HTTP 请求获取容器的状态信息。

sysdig、cAdvisor/Heapster 和 Weave Scope 是其他开源的容器监控方案。

数据管理

容器经常会在不同的 host 之间迁移,如何保证持久化数据也能够动态迁移,是 Flocker 这类数据管理工具提供的能力。

日志管理

日志为问题排查和事件管理提供了重要依据。

docker logs 是 Docker 原生的日志工具。而 logspout 对日志提供了路由功能,它可以收集不同容器的日志并转发给其他工具进行后处理。

安全性

对于年轻的容器,安全性一直是业界争论的焦点。

OpenSCAP 能够对容器镜像进行扫描,发现潜在的漏洞。

004 安装docker

直接操作的环境:https://labs.play-with-docker.com/

环境选择

容器需要管理工具、runtime 和操作系统,我们的选择如下:

  1. 管理工具 - Docker Engine,Docker 最流行使用最广泛。
  2. runtime - runc,Docker 的默认 runtime
  3. 操作系统 - CentOS Stream8

安装操作系统

基于CentOS-Stream-8模板制作.pdf这个实验手册做出来的模板克隆一个虚拟机命名为docker,并配置静态IP 192.168.108.30,主机名改为docker

bash 复制代码
[root@localhost ~]# hostnamectl set-hostname docker
[root@localhost ~]# nmcli connection modify ens160 ipv4.method manual ipv4.addresses 192.168.108.30/24 ipv4.gateway 192.168.108.2 ipv4.dns 192.168.108.2 autoconnect yes
[root@localhost ~]# nmcli con up ens160

安装 Docker

Docker 支持几乎所有的 Linux 发行版,也支持 Mac 和 Windows。各操作系统的安装方法可以访问:https://docs.docker.com/engine/installation/

卸载旧版本

bash 复制代码
[root@docker ~ 10:02:52]# yum remove docker-ce
No match for argument: docker-ce
No packages marked for removal.
Dependencies resolved.
Nothing to do.
Complete!

安装必要工具

bash 复制代码
[root@docker ~ 10:02:59]# yum install -y yum-utils device-mapper-persistent-data                 lvm2 vim
CentOS Stream 8 - BaseOS                        5.7 kB/s | 3.9 kB     00:00
CentOS Stream 8 - AppStream                     6.7 kB/s | 4.4 kB     00:00
Package device-mapper-persistent-data-0.9.0-7.el8.x86_64 is already installed.
Package lvm2-8:2.03.14-14.el8.x86_64 is already installed.
Dependencies resolved.
================================================================================
 Package             Arch        Version                   Repository      Size
================================================================================
Installing:
 vim-enhanced        x86_64      2:8.0.1763-19.el8.4       appstream      1.4 M
 yum-utils           noarch      4.0.21-25.el8             baseos          80 k
Installing dependencies:
 gpm-libs            x86_64      1.20.7-17.el8             appstream       39 k
 vim-common          x86_64      2:8.0.1763-19.el8.4       appstream      6.3 M
 vim-filesystem      noarch      2:8.0.1763-19.el8.4       appstream       50 k

Transaction Summary
================================================================================
Install  5 Packages

Total download size: 7.9 M
Installed size: 30 M
Downloading Packages:
(1/5): gpm-libs-1.20.7-17.el8.x86_64.rpm         58 kB/s |  39 kB     00:00
(2/5): yum-utils-4.0.21-25.el8.noarch.rpm       101 kB/s |  80 kB     00:00
(3/5): vim-filesystem-8.0.1763-19.el8.4.noarch. 279 kB/s |  50 kB     00:00
(4/5): vim-enhanced-8.0.1763-19.el8.4.x86_64.rp 812 kB/s | 1.4 MB     00:01
(5/5): vim-common-8.0.1763-19.el8.4.x86_64.rpm  827 kB/s | 6.3 MB     00:07
--------------------------------------------------------------------------------
Total                                           1.0 MB/s | 7.9 MB     00:07
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                        1/1
  Installing       : vim-filesystem-2:8.0.1763-19.el8.4.noarch              1/5
  Installing       : vim-common-2:8.0.1763-19.el8.4.x86_64                  2/5
  Installing       : gpm-libs-1.20.7-17.el8.x86_64                          3/5
  Running scriptlet: gpm-libs-1.20.7-17.el8.x86_64                          3/5
  Installing       : vim-enhanced-2:8.0.1763-19.el8.4.x86_64                4/5
  Installing       : yum-utils-4.0.21-25.el8.noarch                         5/5
  Running scriptlet: yum-utils-4.0.21-25.el8.noarch                         5/5
  Running scriptlet: vim-common-2:8.0.1763-19.el8.4.x86_64                  5/5
  Verifying        : yum-utils-4.0.21-25.el8.noarch                         1/5
  Verifying        : gpm-libs-1.20.7-17.el8.x86_64                          2/5
  Verifying        : vim-common-2:8.0.1763-19.el8.4.x86_64                  3/5
  Verifying        : vim-enhanced-2:8.0.1763-19.el8.4.x86_64                4/5
  Verifying        : vim-filesystem-2:8.0.1763-19.el8.4.noarch              5/5

Installed:
  gpm-libs-1.20.7-17.el8.x86_64
  vim-common-2:8.0.1763-19.el8.4.x86_64
  vim-enhanced-2:8.0.1763-19.el8.4.x86_64
  vim-filesystem-2:8.0.1763-19.el8.4.noarch
  yum-utils-4.0.21-25.el8.noarch

Complete!
[root@docker ~ 10:03:31]# yum-config-manager --add-repo https://mirrors.aliyun.c                om/docker-ce/linux/centos/docker-ce.repo
Adding repo from: https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.re                po
[root@docker ~ 10:03:53]# yum makecache
Docker CE Stable - x86_64                        49 kB/s |  66 kB     00:01
CentOS Stream 8 - BaseOS                        5.7 kB/s | 3.9 kB     00:00
CentOS Stream 8 - AppStream                     7.2 kB/s | 4.4 kB     00:00
Metadata cache created.

allinone部署

安装软件

bash 复制代码
[root@docker ~ 10:04:11]# yum install -y docker-ce

配置服务

bash 复制代码
[root@docker yum.repos.d 10:48:44]# systemctl enable docker --now
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /us                r/lib/systemd/system/docker.service.

验证安装

查看docker版本

bash 复制代码
[root@docker yum.repos.d 11:13:17]# docker --version
Docker version 26.1.3, build b72abbb

验证docker状态

bash 复制代码
[root@docker yum.repos.d 11:13:22]# systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor pres>
   Active: active (running) since Thu 2025-11-13 10:55:47 CST; 17min ago
     Docs: https://docs.docker.com
 Main PID: 12225 (dockerd)
    Tasks: 10
   Memory: 32.0M
   CGroup: /system.slice/docker.service
           └─12225 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/conta>

Nov 13 10:55:46 docker systemd[1]: Starting Docker Application Container Engine>
Nov 13 10:55:46 docker dockerd[12225]: time="2025-11-13T10:55:46.782564098+08:0>
Nov 13 10:55:46 docker dockerd[12225]: time="2025-11-13T10:55:46.817740514+08:0>
Nov 13 10:55:47 docker dockerd[12225]: time="2025-11-13T10:55:47.720683635+08:0>
Nov 13 10:55:47 docker dockerd[12225]: time="2025-11-13T10:55:47.831176195+08:0>
Nov 13 10:55:47 docker dockerd[12225]: time="2025-11-13T10:55:47.847207750+08:0>
Nov 13 10:55:47 docker dockerd[12225]: time="2025-11-13T10:55:47.847327186+08:0>
Nov 13 10:55:47 docker dockerd[12225]: time="2025-11-13T10:55:47.872900188+08:0>
Nov 13 10:55:47 docker systemd[1]: Started Docker Application Container Engine.
配置镜像加速器(华为云)

登陆https://console.huaweicloud.com/swr/?region=cn-north-4#/swr/dashboard

在镜像资源中找到镜像中心,点击镜像加速器,提示如何配置

bash 复制代码
[root@docker yum.repos.d 11:13:55]# vi /etc/docker/daemon.json
{
    "registry-mirrors": [ "https://19adffc09b4f4fcbad0603a171dd0419.mirror.swr.myhuaweicloud.com" ]
}

[root@docker yum.repos.d 11:16:45]# systemctl restart docker

[root@docker docker 11:53:14]# docker info
Client: Docker Engine - Community
 Version:    26.1.3
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.14.0
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.27.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 0
 Server Version: 26.1.3
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8b3b7ca2e5ce38e8f31a34f35b2b68ceb8470d89
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
 Kernel Version: 4.18.0-553.6.1.el8.x86_64
 Operating System: CentOS Stream 8
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 7.486GiB
 Name: docker
 ID: 425d4420-fa3e-4fcb-8bb1-4d92f2970921
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:
  https://19adffc09b4f4fcbad0603a171dd0419.mirror.swr.myhuaweicloud.com/
 Live Restore Enabled: false

运行第一个容器

环境就绪,马上运行第一个容器,执行命令:

bash 复制代码
[root@docker ~ 17:14:35]# docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

其过程可以简单的描述为:

  1. 从本地查找hello-wrold镜像,没找到
  2. 从 Docker Hub 下载hello-world镜像。
  3. 启动hello-world容器。

清空刚才的实验环境:

bash 复制代码
[root@docker ~]# docker rm -f $(docker ps -aq)        #删除所有容器

[root@docker ~]# docker rmi -f hello-world           #删除镜像hello-world

此刻docker环境没有问题了,关机拍摄快照

运行第二个容器

bash 复制代码
[root@docker ~ 17:15:10]# docker run -d -p 80:80 httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
d7ecded7702a: Pull complete
fab98c44430d: Pull complete
4f4fb700ef54: Pull complete
13c22d886563: Pull complete
4870f70a8556: Pull complete
233dec01418c: Pull complete
Digest: sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
Status: Downloaded newer image for httpd:latest
060eb554d6f368f918eeaa1c536988ef7aa5e0383845b2a4a628c7af8f88e967

其过程可以简单的描述为:

  1. 从 Docker Hub 下载 httpd 镜像。镜像中已经安装好了 Apache HTTP Server。
  2. 启动 httpd 容器,并将容器的 80 端口映射到 host 的 80 端口。

下面我们可以通过浏览器验证容器是否正常工作。在浏览器中输入 http://[your host os IP]

bash 复制代码
[root@docker ~ 17:25:14]# curl 192.168.108.30
<html><body><h1>It works!</h1></body></html>

可以访问容器的 http 服务了,第一个容器运行成功!我们轻轻松松就拥有了一个 WEB 服务器。

005 docker C/S分离部署

基于CentOS-Stream-8模板制作.pdf这个实验手册做出来的模板克隆两个虚拟机命名为docker_client和docker_server

docker server端配置

配置ip 192.168.108.30

配置hostname docker_server

bash 复制代码
[root@localhost ~]# hostnamectl set-hostname docker_server
[root@localhost ~]# nmcli connection modify ens160 ipv4.method manual ipv4.addresses 192.168.108.30/24 ipv4.gateway 192.168.108.2 ipv4.dns 192.168.108.2 autoconnect yes
[root@localhost ~]# nmcli con up ens160

安装软件

bash 复制代码
[root@docker_server ~ 14:39:46]# yum install -y yum-utils device-mapper-persistent-data lvm2 vim
CentOS Stream 8 - BaseOS                                         27 kB/s | 3.9 kB     00:00
CentOS Stream 8 - AppStream                                      28 kB/s | 4.4 kB     00:00
Package device-mapper-persistent-data-0.9.0-7.el8.x86_64 is already installed.
Package lvm2-8:2.03.14-14.el8.x86_64 is already installed.
Dependencies resolved.
================================================================================================
 Package                 Architecture    Version                       Repository          Size
================================================================================================
Installing:
 vim-enhanced            x86_64          2:8.0.1763-19.el8.4           appstream          1.4 M
 yum-utils               noarch          4.0.21-25.el8                 baseos              80 k
Installing dependencies:
 gpm-libs                x86_64          1.20.7-17.el8                 appstream           39 k
 vim-common              x86_64          2:8.0.1763-19.el8.4           appstream          6.3 M
 vim-filesystem          noarch          2:8.0.1763-19.el8.4           appstream           50 k

Transaction Summary
================================================================================================
Install  5 Packages

Total download size: 7.9 M
Installed size: 30 M
Downloading Packages:
(1/5): gpm-libs-1.20.7-17.el8.x86_64.rpm                        215 kB/s |  39 kB     00:00
(2/5): yum-utils-4.0.21-25.el8.noarch.rpm                       330 kB/s |  80 kB     00:00
(3/5): vim-filesystem-8.0.1763-19.el8.4.noarch.rpm              815 kB/s |  50 kB     00:00
(4/5): vim-enhanced-8.0.1763-19.el8.4.x86_64.rpm                2.6 MB/s | 1.4 MB     00:00
(5/5): vim-common-8.0.1763-19.el8.4.x86_64.rpm                  2.7 MB/s | 6.3 MB     00:02
------------------------------------------------------------------------------------------------
Total                                                           3.4 MB/s | 7.9 MB     00:02
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                        1/1
  Installing       : vim-filesystem-2:8.0.1763-19.el8.4.noarch                              1/5
  Installing       : vim-common-2:8.0.1763-19.el8.4.x86_64                                  2/5
  Installing       : gpm-libs-1.20.7-17.el8.x86_64                                          3/5
  Running scriptlet: gpm-libs-1.20.7-17.el8.x86_64                                          3/5
  Installing       : vim-enhanced-2:8.0.1763-19.el8.4.x86_64                                4/5
  Installing       : yum-utils-4.0.21-25.el8.noarch                                         5/5
  Running scriptlet: yum-utils-4.0.21-25.el8.noarch                                         5/5
  Running scriptlet: vim-common-2:8.0.1763-19.el8.4.x86_64                                  5/5
  Verifying        : yum-utils-4.0.21-25.el8.noarch                                         1/5
  Verifying        : gpm-libs-1.20.7-17.el8.x86_64                                          2/5
  Verifying        : vim-common-2:8.0.1763-19.el8.4.x86_64                                  3/5
  Verifying        : vim-enhanced-2:8.0.1763-19.el8.4.x86_64                                4/5
  Verifying        : vim-filesystem-2:8.0.1763-19.el8.4.noarch                              5/5

Installed:
  gpm-libs-1.20.7-17.el8.x86_64                 vim-common-2:8.0.1763-19.el8.4.x86_64
  vim-enhanced-2:8.0.1763-19.el8.4.x86_64       vim-filesystem-2:8.0.1763-19.el8.4.noarch
  yum-utils-4.0.21-25.el8.noarch

Complete!

[root@docker_server ~ 14:40:04]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
Adding repo from: https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

[root@docker_server ~ 14:40:56]#  yum makecache
Docker CE Stable - x86_64                                       269 kB/s |  66 kB     00:00
CentOS Stream 8 - BaseOS                                         32 kB/s | 3.9 kB     00:00
CentOS Stream 8 - AppStream                                      36 kB/s | 4.4 kB     00:00
Metadata cache created.

[root@docker_server ~ 14:42:05]# yum install -y docker-ce
Last metadata expiration check: 0:00:06 ago on Thu 13 Nov 2025 02:42:04 PM CST.
Dependencies resolved.
================================================================================================
 Package                   Arch   Version                                Repository        Size
================================================================================================
Installing:
 docker-ce                 x86_64 3:26.1.3-1.el8                         docker-ce-stable  27 M
Installing dependencies:
 container-selinux         noarch 2:2.229.0-2.module_el8+847+7863d4e6    appstream         71 k
 containerd.io             x86_64 1.6.32-3.1.el8                         docker-ce-stable  35 M
 docker-ce-cli             x86_64 1:26.1.3-1.el8                         docker-ce-stable 7.8 M
 fuse-overlayfs            x86_64 1.13-1.module_el8+804+f131391c         appstream         70 k
 fuse3                     x86_64 3.3.0-19.el8                           baseos            55 k
 fuse3-libs                x86_64 3.3.0-19.el8                           baseos            96 k
 libcgroup                 x86_64 0.41-19.el8                            baseos            70 k
 libslirp                  x86_64 4.4.0-1.module_el8+804+f131391c        appstream         70 k
 slirp4netns               x86_64 1.2.3-1.module_el8+951+32019cde        appstream         56 k
Installing weak dependencies:
 docker-buildx-plugin      x86_64 0.14.0-1.el8                           docker-ce-stable  14 M
 docker-ce-rootless-extras x86_64 26.1.3-1.el8                           docker-ce-stable 5.0 M
 docker-compose-plugin     x86_64 2.27.0-1.el8                           docker-ce-stable  13 M
Enabling module streams:
 container-tools                  rhel8

Transaction Summary
================================================================================================
Install  13 Packages

Total download size: 103 M
Installed size: 390 M
Downloading Packages:
(1/13): docker-buildx-plugin-0.14.0-1.el8.x86_64.rpm            1.7 MB/s |  14 MB     00:07
(2/13): docker-ce-cli-26.1.3-1.el8.x86_64.rpm                   2.1 MB/s | 7.8 MB     00:03
(3/13): docker-ce-rootless-extras-26.1.3-1.el8.x86_64.rpm       1.6 MB/s | 5.0 MB     00:03
(4/13): docker-compose-plugin-2.27.0-1.el8.x86_64.rpm           1.5 MB/s |  13 MB     00:08
(5/13): fuse3-3.3.0-19.el8.x86_64.rpm                           371 kB/s |  55 kB     00:00
(6/13): fuse3-libs-3.3.0-19.el8.x86_64.rpm                      492 kB/s |  96 kB     00:00
(7/13): libcgroup-0.41-19.el8.x86_64.rpm                        441 kB/s |  70 kB     00:00
(8/13): container-selinux-2.229.0-2.module_el8+847+7863d4e6.noa 632 kB/s |  71 kB     00:00
(9/13): fuse-overlayfs-1.13-1.module_el8+804+f131391c.x86_64.rp 644 kB/s |  70 kB     00:00
(10/13): libslirp-4.4.0-1.module_el8+804+f131391c.x86_64.rpm    576 kB/s |  70 kB     00:00
(11/13): slirp4netns-1.2.3-1.module_el8+951+32019cde.x86_64.rpm 375 kB/s |  56 kB     00:00
(12/13): docker-ce-26.1.3-1.el8.x86_64.rpm                      945 kB/s |  27 MB     00:29
(13/13): containerd.io-1.6.32-3.1.el8.x86_64.rpm                1.2 MB/s |  35 MB     00:30
------------------------------------------------------------------------------------------------
Total                                                           3.4 MB/s | 103 MB     00:30
Docker CE Stable - x86_64                                       2.5 kB/s | 1.6 kB     00:00
Importing GPG key 0x621E9F35:
 Userid     : "Docker Release (CE rpm) <docker@docker.com>"
 Fingerprint: 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35
 From       : https://mirrors.aliyun.com/docker-ce/linux/centos/gpg
Key imported successfully
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                        1/1
  Running scriptlet: container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch          1/13
  Installing       : container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch          1/13
  Running scriptlet: container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch          1/13
  Installing       : fuse3-libs-3.3.0-19.el8.x86_64                                        2/13
  Running scriptlet: fuse3-libs-3.3.0-19.el8.x86_64                                        2/13
  Installing       : docker-compose-plugin-2.27.0-1.el8.x86_64                             3/13
  Running scriptlet: docker-compose-plugin-2.27.0-1.el8.x86_64                             3/13
  Installing       : fuse3-3.3.0-19.el8.x86_64                                             4/13
  Installing       : fuse-overlayfs-1.13-1.module_el8+804+f131391c.x86_64                  5/13
  Running scriptlet: fuse-overlayfs-1.13-1.module_el8+804+f131391c.x86_64                  5/13
  Installing       : containerd.io-1.6.32-3.1.el8.x86_64                                   6/13
  Running scriptlet: containerd.io-1.6.32-3.1.el8.x86_64                                   6/13
  Installing       : libslirp-4.4.0-1.module_el8+804+f131391c.x86_64                       7/13
  Installing       : slirp4netns-1.2.3-1.module_el8+951+32019cde.x86_64                    8/13
  Running scriptlet: libcgroup-0.41-19.el8.x86_64                                          9/13
  Installing       : libcgroup-0.41-19.el8.x86_64                                          9/13
  Running scriptlet: libcgroup-0.41-19.el8.x86_64                                          9/13
  Installing       : docker-buildx-plugin-0.14.0-1.el8.x86_64                             10/13
  Running scriptlet: docker-buildx-plugin-0.14.0-1.el8.x86_64                             10/13
  Installing       : docker-ce-cli-1:26.1.3-1.el8.x86_64                                  11/13
  Running scriptlet: docker-ce-cli-1:26.1.3-1.el8.x86_64                                  11/13
  Installing       : docker-ce-rootless-extras-26.1.3-1.el8.x86_64                        12/13
  Running scriptlet: docker-ce-rootless-extras-26.1.3-1.el8.x86_64                        12/13
  Installing       : docker-ce-3:26.1.3-1.el8.x86_64                                      13/13
  Running scriptlet: docker-ce-3:26.1.3-1.el8.x86_64                                      13/13
  Running scriptlet: container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch         13/13
  Running scriptlet: docker-ce-3:26.1.3-1.el8.x86_64                                      13/13
  Verifying        : containerd.io-1.6.32-3.1.el8.x86_64                                   1/13
  Verifying        : docker-buildx-plugin-0.14.0-1.el8.x86_64                              2/13
  Verifying        : docker-ce-3:26.1.3-1.el8.x86_64                                       3/13
  Verifying        : docker-ce-cli-1:26.1.3-1.el8.x86_64                                   4/13
  Verifying        : docker-ce-rootless-extras-26.1.3-1.el8.x86_64                         5/13
  Verifying        : docker-compose-plugin-2.27.0-1.el8.x86_64                             6/13
  Verifying        : fuse3-3.3.0-19.el8.x86_64                                             7/13
  Verifying        : fuse3-libs-3.3.0-19.el8.x86_64                                        8/13
  Verifying        : libcgroup-0.41-19.el8.x86_64                                          9/13
  Verifying        : container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch         10/13
  Verifying        : fuse-overlayfs-1.13-1.module_el8+804+f131391c.x86_64                 11/13
  Verifying        : libslirp-4.4.0-1.module_el8+804+f131391c.x86_64                      12/13
  Verifying        : slirp4netns-1.2.3-1.module_el8+951+32019cde.x86_64                   13/13

Installed:
  container-selinux-2:2.229.0-2.module_el8+847+7863d4e6.noarch
  containerd.io-1.6.32-3.1.el8.x86_64
  docker-buildx-plugin-0.14.0-1.el8.x86_64
  docker-ce-3:26.1.3-1.el8.x86_64
  docker-ce-cli-1:26.1.3-1.el8.x86_64
  docker-ce-rootless-extras-26.1.3-1.el8.x86_64
  docker-compose-plugin-2.27.0-1.el8.x86_64
  fuse-overlayfs-1.13-1.module_el8+804+f131391c.x86_64
  fuse3-3.3.0-19.el8.x86_64
  fuse3-libs-3.3.0-19.el8.x86_64
  libcgroup-0.41-19.el8.x86_64
  libslirp-4.4.0-1.module_el8+804+f131391c.x86_64
  slirp4netns-1.2.3-1.module_el8+951+32019cde.x86_64

Complete!

[root@docker_server ~ 14:44:00]# systemctl enable docker.service --now
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /usr/lib/systemd/system/docker.service.
[root@docker_server ~ 14:44:11]# vi /etc/docker/daemon.json
{
    "registry-mirrors": [ "https://19adffc09b4f4fcbad0603a171dd0419.mirror.swr.myhuaweicloud.com" ]
}


[root@docker_server ~ 14:45:24]# systemctl restart docker

配置服务

bash 复制代码
[root@docker_server ~ 14:45:33]# vim /usr/lib/systemd/system/docker.service
# 在ExecStart参数中最后添加 -H tcp://0.0.0.0:2375,docker默认监听2375
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375

[root@docker_server ~ 14:46:22]# systemctl daemon-reload
[root@docker_server ~ 14:46:28]# systemctl restart docker.service
[root@docker_server ~ 14:46:49]# systemctl stop firewalld

验证

bash 复制代码
[root@docker_server ~ 14:46:53]# yum install lsof
Last metadata expiration check: 0:05:00 ago on Thu 13 Nov 2025 02:42:04 PM CST.
Dependencies resolved.
================================================================================================
 Package            Architecture         Version                     Repository            Size
================================================================================================
Installing:
 lsof               x86_64               4.93.2-1.el8                baseos               253 k

Transaction Summary
================================================================================================
Install  1 Package

Total download size: 253 k
Installed size: 623 k
Is this ok [y/N]: y
Downloading Packages:
lsof-4.93.2-1.el8.x86_64.rpm                                    347 kB/s | 253 kB     00:00
------------------------------------------------------------------------------------------------
Total                                                           346 kB/s | 253 kB     00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                                        1/1
  Installing       : lsof-4.93.2-1.el8.x86_64                                               1/1
  Running scriptlet: lsof-4.93.2-1.el8.x86_64                                               1/1
  Verifying        : lsof-4.93.2-1.el8.x86_64                                               1/1

Installed:
  lsof-4.93.2-1.el8.x86_64

Complete!


[root@docker_server ~ 14:47:09]# lsof -i :2375
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
dockerd 12103 root    3u  IPv6  50049      0t0  TCP *:docker (LISTEN)
[root@docker_server ~ 14:47:13]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   latest    1b44b5a3e06a   3 months ago   10.1kB
[root@docker_server ~ 14:48:09]#

docker_client端

配置ip 192.168.108.31

bash 复制代码
[root@localhost ~]# hostnamectl set-hostname docker_client
[root@localhost ~]# nmcli connection modify ens160 ipv4.method manual ipv4.addresses 192.168.108.31/24 ipv4.gateway 192.168.108.2 ipv4.dns 192.168.108.2 autoconnect yes
[root@localhost ~]# nmcli con up ens160

只安装docker客户端

bash 复制代码
[root@docker-client ~]# yum install -y yum-utils device-mapper-persistent-data lvm2 vim      
[root@docker-client ~]# yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
Adding repo from: https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
[root@docker-client ~]# yum makecache
[root@docker-client ~]# yum install -y docker-ce-cli

验证

bash 复制代码
[root@docker_client ~]# docker run hello-world          #client直接执行报错,没有装服务端
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
See 'docker run --help'.

# client端连接server端执行命令
[root@docker-client ~]# docker -H 192.168.108.30 run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
 
[root@docker-client ~]# docker -H 192.168.108.30 images 
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
hello-world   latest    d2c94e258dcb   17 months ago   13.3kB
#这里也可以切换到Server端查看现象

说明:client只做管理,image和container存储在server端。

第2章 容器架构

006 容器What,Why,How

学习任何东西都可以按照3W的框架进行,容器技术也是一样,先回答 What、Why 和 How 这三个问题。

容器发展史

  • 1979年,在Unix V7 的开发过程中,引入Chroot Jail以及Chroot系统调用Chroot jail被用于"Change Root",它被认为是最早的容器化技术之一。它允许您将进程及其子进程与操作系统的其余部分隔离开来。这种隔离的唯一问题是根进程(root process)可以轻松地退出chroot。它从未考虑实现安全机制。
  • 1982年,Chroot加入BSD。
  • 2000年,在FreeBSD OS中引入FreeBSD Jail,旨在为简单的Chroot文件隔离带来更多安全性。FreeBSD Jail允许管理员将FreeBSD OS划分为几个独立的小型系统 - 称为"jails",还可以为每个小型系统分配IP地址。
  • 2001年,Linux内核具有操作系统级的虚拟化的功能以后推出了Linux VServer。它使用了类似chroot的机制与"安全上下文"("security context")以及操作系统虚拟化(容器化)相结合来提供虚拟化解决方案,允许您在单个Linux发行版上运行多个Linux发行版。
  • 2004年,Solaris Containers问世,它使用系统资源控制和"区域"(zone)实现边界分离。
  • 2006年,Google推出Process Containers,旨在限制,计算和隔离一组进程的资源使用(CPU,内存,磁盘I/O,网络)。2007年,命名为控制组-cgroups,最终合并到Linux内核2.6.24。
  • 2008年,LXC(LinuX Containers)是第一个Linux容器管理器,使用cgroups和命名空间(namespace)在单个Linux内核上运行多个容器。
  • 2013年,Docker推出了的第一个版本。在LXC的基础上,Docker进一步优化了容器的使用体验。后来用自己的库libcontainer替换了该容器管理器。
  • 2014年,Google推出容器LMCTFY(Let me contain that for you),谷歌容器栈的开源版本,提供Linux应用程序容器。谷歌工程师一直在与Docker合作libcontainer,并将核心概念和抽象移植到libcontainer。因此没有积极开发LMCTFY项目,未来LMCTFY项目的核心可能会被libcontainer取代。谷歌是容器化行业的领导者。谷歌的一切都在容器上运行。每周有超过20亿个容器在Google基础架构上运行。
  • 2014年12月,CoreOS发布并开始支持rkt。
What - 什么是容器?

容器是一种轻量级、可移植、自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运行。开发人员在自己笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机、物理服务器或公有云主机上运行。

容器与虚拟机

谈到容器,就不得不将它与虚拟机进行对比,因为两者都是为应用提供封装和隔离。

容器由两部分组成:

  1. 应用程序本身
  2. 依赖:比如应用程序需要的库或其他软件

容器在 Host 操作系统的用户空间中运行,与操作系统的其他进程隔离。这一点显著区别于的虚拟机。

传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。

下图展示了二者的区别。

容器 虚拟机
启动速度 秒甚至毫秒启动 数秒至数十秒
系统内核 共享内核 不共享内核
实现技术 利用Linux内核技术Namespace/Cgroup等实现。 依赖虚拟化技术实现,由Hypervisor层实现对资源的隔离
隔离效果 进程级别的隔离 系统资源级别的隔离
资源消耗(性能) 容器中的应用只是宿主机上的一个普通进程 使用虚拟化技术,就会有额外的资源消耗和占用
资源调用 (敏捷性) 应用进程直接由宿主机OS管理 应用进程需经过Hypervisor的拦截和处理,才能调用系统资源
运行数量 一台服务器上能启动1000+容器 一台服务器上一般不超过100台虚拟机
应用 DevOps、微服务等 用于硬件资源划分
镜像 分层镜像 非分层镜像

如图所示,由于所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。另外,启动容器不需要启动整个操作系统,所以容器部署和启动速度更快,开销更小,也更容易迁移。

Why - 为什么需要容器?

为什么需要容器?容器到底解决的是什么问题?

简要的答案是:容器使软件具备了超强的可移植能力

容器解决的问题

我们来看看今天的软件开发面临着怎样的挑战?

如今的系统在架构上较十年前已经变得非常复杂了。以前几乎所有的应用都采用三层架构(Presentation/Application/Data),系统部署到有限的几台物理服务器上(Web Server/Application Server/Database Server)。

而今天,开发人员通常使用多种服务(比如 MQ,Cache,DB)构建和组装应用,而且应用很可能会部署到不同的环境,比如虚拟服务器,私有云和公有云。

一方面应用包含多种服务,这些服务有自己所依赖的库和软件包;另一方面存在多种部署环境,服务在运行时可能需要动态迁移到不同的环境中。这就产生了一个问题:

如何让每种服务能够在所有的部署环境中顺利运行?

于是我们得到了下面这个矩阵:

各种服务和环境通过排列组合产生了一个大矩阵。开发人员在编写代码时需要考虑不同的运行环境,运维人员则需要为不同的服务和平台配置环境。对他们双方来说,这都是一项困难而艰巨的任务。

如何解决这个问题呢?

聪明的技术人员从传统的运输行业找到了答案。

几十年前,运输业面临着类似的问题。

每一次运输,货主与承运方都会担心因货物类型的不同而导致损失,比如几个铁桶错误地压在了一堆香蕉上。另一方面,运输过程中需要使用不同的交通工具也让整个过程痛苦不堪:货物先装上车运到码头,卸货,然后装上船,到岸后又卸下船,再装上火车,到达目的地,最后卸货。一半以上的时间花费在装、卸货上,而且搬上搬下还容易损坏货物。

这同样也是一个 NxM 的矩阵。

幸运的是,集装箱的发明解决这个难题。

任何货物,无论钢琴还是保时捷,都被放到各自的集装箱中。集装箱在整个运输过程中都是密封的,只有到达最终目的地才被打开。标准集装箱可以被高效地装卸、重叠和长途运输。现代化的起重机可以自动在卡车、轮船和火车之间移动集装箱。集装箱被誉为运输业与世界贸易最重要的发明。

Docker 将集装箱思想运用到软件打包上,为代码提供了一个基于容器的标准化运输系统。Docker 可以将任何应用及其依赖打包成一个轻量级、可移植、自包含的容器。容器可以运行在几乎所有的操作系统上。

其实,"集装箱" 和 "容器" 对应的英文单词都是 "Container"。"容器" 是国内约定俗成的叫法,可能是因为容器比集装箱更抽象,更适合软件领域的原故吧。

我个人认为:在老外的思维中,"Container" 只用到了集装箱这一个意思,Docker 的 Logo 不就是一堆集装箱吗?

Docker 的特性

我们可以看看集装箱思想是如何与 Docker 各种特性相对应的。

特性 集装箱 Docker
打包对象 几乎任何货物 任何软件及其依赖
硬件依赖 标准形状和接口允许集装箱被装卸到各种交通工具,整个运输过程无需打开 容器无需修改便可运行在几乎所有的平台上 -- 虚拟机、物理机、公有云、私有云
隔离性 集装箱可以重叠起来一起运输,香蕉再也不会被铁桶压烂了 资源、网络、库都是隔离的,不会出现依赖问题
自动化 标准接口使集装箱很容易自动装卸和移动 提供 run, start, stop 等标准化操作,非常适合自动化
高效性 无需开箱,可在各种交通工具间快速搬运 轻量级,能够快速启动和迁移
职责分工 货主只需考虑把什么放到集装箱里;承运方只需关心怎样运输集装箱 开发人员只需考虑怎么写代码;运维人员只需关心如何配置基础环境

容器的优势

对于开发人员 - Build Once, Run Anywhere

容器意味着环境隔离和可重复性。开发人员只需为应用创建一次运行环境,然后打包成容器便可在其他机器上运行。另外,容器环境与所在的 Host 环境是隔离的,就像虚拟机一样,但更快更简单。

对于运维人员 - Configure Once, Run Anything

只需要配置好标准的 runtime 环境,服务器就可以运行任何容器。这使得运维人员的工作变得更高效,一致和可重复。容器消除了开发、测试、生产环境的不一致性。

How - 容器是如何工作的?

从下节开始我们将学习容器核心知识的最主要部分。首先会介绍 Docker 的架构,然后分章节详细讨论 Docker 的镜像、容器、网络和存储。

容器与虚拟机比较

谈到容器,就不得不将它与虚拟机进行对比,因为两者都是为应用提供封装和隔离。

容器由两部分组成:

  1. 应用程序本身
  2. 依赖:比如应用程序需要的库或其他软件

容器在 Host 操作系统的用户空间中运行,与操作系统的其他进程隔离。这一点显著区别于的虚拟机。

传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。

下图展示了二者的区别:

如图所示:

  • 所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。
  • 另启动容器不需要启动整个操作系统,容器部署和启动速度更快,开销更小,也更容易迁移。

容器可以在核心CPU本地运行指令,而不需要任何专门的解释机制。与虚拟化相比,这样既不需要指令级模拟,也不需要即时编译。

特性 容器 虚拟机
启动速度 秒级 分钟级
性能 接近原生 较弱
内存代价 很小 较多
硬盘使用 一般为MB 一般为GB
运行密度 单击支持上千个容器 一般几十个
隔离性 安全隔离 完全隔离
迁移性 优秀 一般

Docker介绍

Docker是基于Go语言实现的开源容器项目,诞生于2013年年初,最初发起者是dotCloud公司。Docker自开源后受到广泛的关注和讨论,逐渐形成了围绕Docker容器的生态体系。由于Docker在业界造成的影响力实在太大,dotCloud公司后来也直接改名为Docker Inc,并专注于 Docker相关技术和产品的开发。

Docker提供了各种容器管理工具 (如分发、版本、移植等),让用户无需关注底层的操作,可以更简单明了地管理和使用容器;其次,Docker引入分层文件系统构建和高效的镜像机制降低了迁移难度,极大地提升了用户体验。用户操作Docker容器就像操作应用自身一样简单。

简单地讲,可以将Docker容器理解为一种轻量级的沙盒(sandbox)。**每个容器内运行着一个应用,不同的容器相互隔离,容器之间也可以通过网络互相通信。**容器的创建和停止都十分快速,几乎跟创建和终止原生应用一致;另外,容器自身对系统资源的额外需求也十分有限,远远低于传统虚拟机。很多时候,甚至直接把容器当作应用本身也没有任何问题。

Docker版本

Docker CE在17.03版本之前叫Docker Engine,Docker Engine的版本号范围:0.1.0~1.13.1。

在2017年3月2日,docker团队宣布企业版Docker Enterprise Edition(EE)发布.为了一致,免费的Docker Engine改名为Docker Community Edition(CE),并且采用基于时间的版本号方案.就在这一天,Docker EE和Docker CE的17.03版本发布,这也是第一个采用新的版本号方案的版本。

Docker CE/EE每个季度发布一次季度版本,也就是说每年会发布4个季度版本,17.03,17.06,17.09,17.12就是2017年的4个季度版本的版本号,同时Docker CE每个月还会发布一个EDGE版本,比如17.04,17.05,17.07,17.08,17.10,17.11.

Docker CE季度版本自发布后会有4个月的维护期;Docker EE季度版本自发布后会有12个月的维护期.

在基于时间的发布方案中,版本号格式为:YY.MM.,YY.MM代表年月,patch代表补丁号,从0开始,在季度版本(如17.03)的维护期内,bug修复相关的更新会以patch递增的方式发布,比如17.03.0->17.03.1->17.03.2.

Docker核心概念

Docker的大部分操作都围绕着它的三大核心概念------镜像、容器和仓库而展开。

Docker镜像

Docker镜像类似于虚拟机镜像,可以将它理解为一个只读的模板。例如,一个镜像可以包含一个基本的操作系统环境,里面仅安装了Apache应用程序(或用户需要的其他软件)。可以把它称为一个Apache镜像。镜像是创建Docker容器的基础。通过版本管理和增量的文件系统,Docker提供了一套十分简单的机制创建和更新现有的镜像,用户甚至可以从网上下载一个已经做好的应用镜像,并直接使用。

Docker容器

Docker容器类似于一个轻量级的沙箱,Docker利用容器来运行和隔离应用。容器是从镜像创建的应用运行实例。可以将其启动、开始、停止、删除,而这些容器都是彼此相互隔离的、互不可见的。可以把容器看做是一个简易版的Linux系统环境(包括root用户权限、进程空间、用户空间和网络空间等)以及运行在其中的应用程序打包而成的盒子。

Docker仓库

Docker仓库类似于代码仓库,它是Docker集中存放镜像文件的场所。仓库注册服务器存放着很多类镜像,每类镜像包括多个镜像文件,通过不同的标签(tag)来进行区分。例如存放Ubuntu操作系统镜像的分类中可能包括1804、1604、14.04、12.04等不同版本的镜像。

根据所存储的镜像公开分享与否,Docker仓库可以分为:

  • 公开仓库(Public)
  • 私有仓库(Private)

目前,最大的公开仓库是官方提供的Docker Hub,其中存放了数量庞大的镜像供用户下载。国内不少云服务提供商(如时速云、阿里云等)也提供了仓库的本地源,可以提供稳定的国内访问。

当然,用户如果不希望公开分享自己的镜像文件,Docker也支持用户在本地网络内创建一个只能自己访问的私有仓库。当用户创建了自己的镜像之后就可以使用push命令将它上传到指定的公有或者私有仓库。这样用户下次在另外一台机器上使用该镜像时,只需要将其从仓库上pull下来就可以了。

007 Docker架构详解

完整的Docker由以下几部分构成:

  • (1) **守护进程(Daemon):**Docker守护进程(dockerd)侦听Docker API请求并管理Docker对象,,如图像、容器、网络和卷。守护进程还可以与其他守护进程通信来管理Docker服务。
  • (2) REST API: 主要与Docker Daemon进行交互,比如Docker Cli或者直接调用REST API;
  • (3) 客户端(Docker Client): 它是与Docker交互的主要方式通过命令行接口(CLI)客户端(docker命令),客户机将命令通过REST API发送给并执行其命令;
  • (4) Register Repository 镜像仓库: Docker注册表存储Docker镜像,可以采用Docker Hub是公共注册仓库,或者采用企业内部自建的Harbor私有仓库;
  • (5) Image 镜像: 映像是一个只读模板,带有创建Docker容器的指令。映像通常基于另一个映像,还需要进行一些额外的定制,你可以通过Docker Hub公共镜像仓库进行拉取对应的系统或者应用镜像;
  • (6) Container 容器: 容器是映像的可运行实例。您可以使用Docker API或CLI创建、启动、停止、移动或删除容器。您可以将一个容器连接到一个或多个网络,将存储附加到它,甚至根据它的当前状态创建一个新映像。
  • (7) Services : Docker引擎支持集群模式服务允许您跨多个Docker守护进程()扩展管理容器,服务允许您定义所需的状态,例如在任何给定时间必须可用的服务副本的数量。默认情况下,服务在所有工作节点之间进行负载平衡。对于使用者来说Docker服务看起来是一个单独的应用程序;

Docker 的核心组件包括:

  1. Docker 客户端 - Client
  2. Docker 服务器 - Docker daemon
  3. Docker 镜像 - Image
  4. Registry
  5. Docker 容器 - Container

Docker 架构如下图所示:

Docker 采用的是 Client/Server 架构。客户端向服务器发送请求,服务器负责构建、运行和分发容器。客户端和服务器可以运行在同一个 Host 上,客户端也可以通过 socket 或 REST API 与远程的服务器通信。

Docker 内部具体实现:

  1. 用户是使用Docker Client与Docker Daemon建立通信,并发送请求给后者。
  2. Docker Daemon作为Docker架构中的主体部分,首先提供Docker Server的功能使其可以接受Docker Client的请求。
  3. Docker Engine执行Docker内部的一系列工作,每一项工作都是以一个Job的形式的存在。
  4. Job的运行过程中,当需要容器镜像时,则从DockerRegistry中下载镜像,并通过镜像管理驱动Graph driver将下载镜像以Graph的形式存储。
  5. 当需要为Docker创建网络环境时,通过网络管理驱动Networkdriver创建并配置Docker容器网络环境。
  6. 当需要限制Docker容器运行资源或执行用户指令等操作时,则通过Exec driver来完成。
  7. Libcontainer是一项独立的容器管理包,Networkdriver以及Execdriver都是通过Libcontainer来实现具体对容器进行的操作。

Docker 客户端

最常用的 Docker 客户端是 docker 命令。通过 docker 我们可以方便地在 Host 上构建和运行容器。

docker 支持很多操作(子命令),后面会逐步用到。

bash 复制代码
[root@docker ~ 17:25:21]# docker

Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Common Commands:
  run         Create and run a new container from an image
  exec        Execute a command in a running container
  ps          List containers
  build       Build an image from a Dockerfile
  pull        Download an image from a registry
  push        Upload an image to a registry
  images      List images
  login       Log in to a registry
  logout      Log out from a registry
  search      Search Docker Hub for images
  version     Show the Docker version information
  info        Display system-wide information

Management Commands:
  builder     Manage builds
  buildx*     Docker Buildx
  compose*    Docker Compose
  container   Manage containers
  context     Manage contexts
  image       Manage images
  manifest    Manage Docker image manifests and manifest lists
  network     Manage networks
  plugin      Manage plugins
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes

Swarm Commands:
  swarm       Manage Swarm

Commands:
  attach      Attach local standard input, output, and error streams to a running container
  commit      Create a new image from a container's changes
  cp          Copy files/folders between a container and the local filesystem
  create      Create a new container
  diff        Inspect changes to files or directories on a container's filesystem
  events      Get real time events from the server
  export      Export a container's filesystem as a tar archive
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Return low-level information on Docker objects
  kill        Kill one or more running containers
  load        Load an image from a tar archive or STDIN
  logs        Fetch the logs of a container
  pause       Pause all processes within one or more containers
  port        List port mappings or a specific mapping for the container
  rename      Rename a container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  start       Start one or more stopped containers
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop one or more running containers
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
  top         Display the running processes of a container
  unpause     Unpause all processes within one or more containers
  update      Update configuration of one or more containers
  wait        Block until one or more containers stop, then print their exit codes

Global Options:
      --config string      Location of client config files (default "/root/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides
                           DOCKER_HOST env var and default context set with "docker
                           context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket to connect to
  -l, --log-level string   Set the logging level ("debug", "info", "warn", "error",
                           "fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/root/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/root/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/root/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit

Run 'docker COMMAND --help' for more information on a command.

For more help on how to use Docker, head to https://docs.docker.com/go/guides/

除了 docker 命令行工具,用户也可以通过 REST API 与服务器通信。

Docker 服务器

Docker daemon 是服务器组件,以 Linux 后台服务的方式运行。

bash 复制代码
[root@docker ~ 18:53:04]# systemctl status docker
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: active (running) since Thu 2025-11-13 17:13:57 CST; 1h 39min ago
     Docs: https://docs.docker.com
 Main PID: 1290 (dockerd)
    Tasks: 28
   Memory: 275.5M
   CGroup: /system.slice/docker.service
           ├─1290 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
           ├─2303 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip>
           └─2309 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.>

Nov 13 17:13:56 docker dockerd[1290]: time="2025-11-13T17:13:56.929204444+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.330617410+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.615919236+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.729536843+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.849356438+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.896729834+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.896820044+08:00" level=info msg>
Nov 13 17:13:57 docker dockerd[1290]: time="2025-11-13T17:13:57.923153990+08:00" level=info msg>
Nov 13 17:13:57 docker systemd[1]: Started Docker Application Container Engine.
Nov 13 17:15:10 docker dockerd[1290]: time="2025-11-13T17:15:10.701775916+08:00" level=info msg>
lines 1-22/22 (END)

Docker 镜像

可将 Docker 镜像看着只读模板,通过它可以创建 Docker 容器。

例如某个镜像可能包含一个 Ubuntu 操作系统、一个 Apache HTTP Server 以及用户开发的 Web 应用。

镜像有多种生成方法:

  1. 可以从无到有开始创建镜像
  2. 也可以下载并使用别人创建好的现成的镜像
  3. 还可以在现有镜像上创建新的镜像

我们可以将镜像的内容和创建步骤描述在一个文本文件中,这个文件被称作 Dockerfile,通过执行 docker build <docker-file> 命令可以构建出 Docker 镜像,后面我们会讨论。

Docker 容器

Docker 容器就是 Docker 镜像的运行实例。

用户可以通过 CLI(docker)或是 API 启动、停止、移动或删除容器。可以这么认为,对于应用软件,镜像是软件生命周期的构建和打包阶段,而容器则是启动和运行阶段。

Registry

Registry 是存放 Docker 镜像的仓库,Registry 分私有和公有两种。

Docker Hub(https://hub.docker.com/) 是默认的 Registry,由 Docker 公司维护,上面有数以万计的镜像,用户可以自由下载和使用。

出于对速度或安全的考虑,用户也可以创建自己的私有 Registry。后面我们会学习如何搭建私有 Registry。

docker pull 命令可以从 Registry 下载镜像。
docker run 命令则是先下载镜像(如果本地没有),然后再启动容器。

下一节我们通过一个例子来看各个组件是如何协调工作的。

008 Docker组件如何协作?

还记得我们运行的第一个容器吗?现在通过它来体会一下 Docker 各个组件是如何协作的。

容器启动过程如下:

#不是 -h-p

  1. Docker 客户端执行 docker run 命令。
  2. Docker daemon 发现本地没有 httpd 镜像。
  3. daemon 从 Docker Hub 下载镜像。
  4. 下载完成,镜像 httpd 被保存到本地。
  5. Docker daemon 启动容器。

docker images 可以查看到 httpd 已经下载到本地。

bash 复制代码
[root@docker ~ 18:58:57]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
httpd         latest    6a4fe18d08d2   9 days ago     117MB
ubuntu        latest    97bed23a3497   6 weeks ago    78.1MB
hello-world   latest    1b44b5a3e06a   3 months ago   10.1kB
centos        7         eeb6ee3f44bd   4 years ago    204MB

docker ps 或者 docker container ls 显示容器正在运行。

小结

Docker 借鉴了集装箱的概念。标准集装箱将货物运往世界各地,Docker 将这个模型运用到自己的设计哲学中,唯一不同的是:集装箱运输货物,而 Docker 运输软件。

每个容器都有一个软件镜像,相当于集装箱中的货物。容器可以被创建、启动、关闭和销毁。和集装箱一样,Docker 在执行这些操作时,并不关心容器里到底装的什么,它不管里面是 Web Server,还是 Database。

用户不需要关心容器最终会在哪里运行,因为哪里都可以运行。

开发人员可以在笔记本上构建镜像并上传到 Registry,然后 测试人员将镜像下载到物理或虚拟机做测试,最终容器会部署到生产环境。

使用 Docker 以及容器技术,我们可以快速构建一个应用服务器、一个消息中间件、一个数据库、一个持续集成环境。因为 Docker Hub 上有我们能想到的几乎所有的镜像。

不知大家是否意识到,潘多拉盒子已经被打开。容器不但降低了我们学习新技术的门槛,更提高了效率。

如果你是一个运维人员,想研究负载均衡软件 HAProxy,只需要执行docker run haproxy,无需繁琐的手工安装和配置既可以直接进入实战。

如果你是一个开发人员,想学习怎么用 django 开发 Python Web 应用,执行 docker run django,在容器里随便折腾吧,不用担心会搞乱 Host 的环境。

不夸张的说:容器大大提升了 IT 人员的幸福指数。

第3章 镜像

009 最小的镜像

镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器。

本章内容安排如下:

  1. 首先通过研究几个典型的镜像,分析镜像的内部结构。
  2. 然后学习如何构建自己的镜像。
  3. 最后介绍怎样管理和分发镜像。

镜像的内部结构

为什么我们要讨论镜像的内部结构?

如果只是使用镜像,当然不需要了解,直接通过 docker 命令下载和运行就可以了。

但如果我们想创建自己的镜像,或者想理解 Docker 为什么是轻量级的,就非常有必要学习这部分知识了。

我们从一个最小的镜像开始吧。

hello-world - 最小的镜像

hello-world 是 Docker 官方提供的一个镜像,通常用来验证 Docker 是否安装成功。

我们先通过 docker pull 从 Docker Hub 下载它。

bash 复制代码
[root@docker ~ 15:58:24]# docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
17eec7bbc9d7: Pull complete
Digest: sha256:f7931603f70e13dbd844253370742c4fc4202d290c80442b2e68706d8f33ce26
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

docker images 命令查看镜像的信息。

bash 复制代码
[root@docker ~ 15:58:52]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   latest    1b44b5a3e06a   3 months ago   10.1kB

通过 docker run 运行。

bash 复制代码
[root@docker ~ 15:59:00]# docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

其实我们更关心 hello-world 镜像包含哪些内容。

Dockerfile 是镜像的描述文件,定义了如何构建 Docker 镜像。Dockerfile 的语法简洁且可读性强,后面我们会专门讨论如何编写 Dockerfile。

hello-world 的 Dockerfile 内容如下:

dockerhub hello-word网页:hello-world - Official Image | Docker Hub

github hello-wrold网页:hello-world/amd64/hello-world at master · docker-library/hello-world · GitHub

只有短短三条指令。

  1. FROM scratch
    此镜像是从白手起家,从 0 开始构建。
  2. COPY hello /
    将文件"hello"复制到镜像的根目录。
  3. CMD ["/hello"]
    容器启动时,执行 /hello

镜像 hello-world 中就只有一个可执行文件 "hello",其功能就是打印出 "Hello from Docker ..." 等信息。

/hello 就是文件系统的全部内容,连最基本的 /bin,/usr, /lib, /dev 都没有。

hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作 base 镜像。

我们下一节讨论 base 镜像。

010 base镜像

上一节我们介绍了最小的 Docker 镜像,本节讨论 base 镜像。

base 镜像有两层含义:

  1. 不依赖其他镜像,从 scratch 构建。
  2. 其他镜像可以之为基础进行扩展。

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

我们以 CentOS 为例考察 base 镜像包含哪些内容。

下载镜像:

docker pull centos:7

查看镜像信息:

bash 复制代码
[root@docker ~ 15:59:55]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
hello-world   latest    1b44b5a3e06a   3 months ago   10.1kB
centos        7         eeb6ee3f44bd   4 years ago    204MB

镜像大小不到 300MB。

等一下!

一个 CentOS 才 204MB ?

平时我们安装一个 CentOS 至少都有几个 GB,怎么可能才 204MB !

相信这是几乎所有 Docker 初学者都会有的疑问,包括我自己。下面我们来解释这个问题。

Linux 操作系统由内核空间和用户空间组成。如下图所示:

rootfs

内核空间是 kernel,Linux 刚启动时会加载 bootfs 文件系统,之后 bootfs 会被卸载掉。

用户空间的文件系统是 rootfs,包含我们熟悉的 /dev, /proc, /bin 等目录。

对于 base 镜像来说,底层直接用 Host 的 kernel,自己只需要提供 rootfs 就行了。

而对于一个精简的 OS,rootfs 可以很小,只需要包括最基本的命令、工具和程序库就可以了。相比其他 Linux 发行版,CentOS 的 rootfs 已经算臃肿的了,alpine 还不到 10MB。

我们平时安装的 CentOS 除了 rootfs 还会选装很多软件、服务、图形桌面等,需要好几个 GB 就不足为奇了。

base 镜像提供的是最小安装的 Linux 发行版

下面是 CentOS 镜像的 Dockerfile 的内容:

第二行 ADD 指令添加到镜像的 tar 包就是 CentOS 7 的 rootfs。在制作镜像时,这个 tar 包会自动解压到 / 目录下,生成 /dev, /porc, /bin 等目录。

注:可在 Docker Hub 的镜像描述页面中查看 Dockerfile 。

支持运行多种 Linux OS

不同 Linux 发行版的区别主要就是 rootfs。

比如 Ubuntu 14.04 使用 upstart 管理服务,apt 管理软件包;而 CentOS 7 使用 systemd 和 yum。这些都是用户空间上的区别,Linux kernel 差别不大。

所以 Docker 可以同时支持多种 Linux 镜像,模拟出多种操作系统环境。

上图 Debian 和 BusyBox(一种嵌入式 Linux)上层提供各自的 rootfs,底层共用 Docker Host 的 kernel。

这里需要说明的是:

  1. base 镜像只是在用户空间与发行版一致,kernel 版本与发型版是不同的。

    例如 ubuntu使用 3.x.x 的 kernel,如果 Docker Host 是 CentOS Stream 8(比如我们的实验环境),那么在 CentOS 容器中使用的实际是是 Host 4.18.0 的 kernel。

    bash 复制代码
    [root@docker ~ 16:00:09]# uname -r
    4.18.0-553.6.1.el8.x86_64

    启动一个ubuntu,ubuntu内核正常应该与host os(centos stream 8)不一致

    bash 复制代码
    [root@docker ~ 16:00:20]# docker run -it ubuntu
    Unable to find image 'ubuntu:latest' locally
    latest: Pulling from library/ubuntu
    4b3ffd8ccb52: Pull complete
    Digest: sha256:66460d557b25769b102175144d538d88219c077c678a49af4afca6fbfc1b5252
    Status: Downloaded newer image for ubuntu:latest
    root@2bb823dec778:/# uname -r
    4.18.0-553.6.1.el8.x86_64

    启动一个centos:7,centos7正常内核为3.10

    bash 复制代码
    [root@docker ~ 16:00:59]# docker run -it centos:7
    [root@59c260d60302 /]# uname -r
    4.18.0-553.6.1.el8.x86_64
  2. 容器只能使用 Host 的 kernel,并且不能修改。

    所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。如果容器对 kernel 版本有要求(比如应用只能在某个 kernel 版本下运行),则不建议用容器,这种场景虚拟机可能更合适。

011 镜像的分层结构

Docker 支持通过扩展现有镜像,创建新的镜像。

实际上,Docker Hub 中 99% 的镜像都是通过在 base 镜像中安装和配置需要的软件构建出来的。比如我们现在构建一个新的镜像,Dockerfile 如下:

① 新镜像不再是从 scratch 开始,而是直接在 Debian base 镜像上构建。

② 安装 emacs 编辑器。

③ 安装 apache2。

④ 容器启动时运行 bash。

构建过程如下图所示:

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

问什么 Docker 镜像要采用这种分层结构呢?

最大的一个好处就是 - 共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么 Docker Host 只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享,我们将在后面更深入地讨论这个特性。

这时可能就有人会问了:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?

答案是不会!

修改会被限制在单个容器内。

这就是我们接下来要学习的容器 Copy-on-Write 特性。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。

这一层通常被称作"容器层","容器层"之下的都叫"镜像层"。

所有对容器的改动 - 无论添加、删除、还是修改文件都只会发生在容器层中。

只有容器层是可写的,容器层下面的所有镜像层都是只读的

下面我们深入讨论容器层的细节。

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /a,上层的 /a 会覆盖下层的 /a,也就是说用户只能访问到上层中的文件 /a。在容器层中,用户看到的是一个叠加之后的文件系统。

对容器增删改差操作如下:

操作 具体执行
创建文件 新文件只能被添加在容器层中。
删除文件 依据容器分层结构由上往下依次查找。找到后,在容器层中记录该删除操作。 具体实现是,UnionFS会在容器层创建一个"whiteout"文件,将被删除的文件"遮挡"起来。
修改文件 依据容器分层结构由上往下依次查找。找到后,将镜像层中的数据复制到容器层进行修改,修改后的数据保存在容器层中。(copy-on-write)
读取文件 依据容器分层结构由上往下依次查找。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享

理解了镜像的原理和结构,下一节我们学习如何构建镜像。

012 构建镜像

对于 Docker 用户来说,最好的情况是不需要自己创建镜像。几乎所有常用的数据库、中间件、应用软件等都有现成的 Docker 官方镜像或其他人和组织创建的镜像,我们只需要稍作配置就可以直接使用。

使用现成镜像的好处除了省去自己做镜像的工作量外,更重要的是可以利用前人的经验。特别是使用那些官方镜像,因为 Docker 的工程师知道如何更好的在容器中运行软件。

当然,某些情况下我们也不得不自己构建镜像,比如:

  1. 找不到现成的镜像,比如自己开发的应用程序。
  2. 需要在镜像中加入特定的功能,比如官方镜像几乎都不提供 ssh。

所以本节我们将介绍构建镜像的方法。同时分析构建的过程也能够加深我们对前面镜像分层结构的理解。

Docker 容器文件系统

描述:从下面的图片可以看见出以下几点:

  • Docker 镜像代表了容器的文件系统里的内容,是容器的基础,镜像一般是通过 Dockerfile 生成的;
  • Docker 的镜像是分层的,所有的镜像(除了基础镜像)都是在之前镜像的基础上加上自己这层的内容生成的;
  • Docker 中每一层镜像的元数据都是存在 json 文件中的,除了静态的文件系统之外,还会包含动态的数据;
  • Docker 镜像生产容器后会在此基础之上加入挂载点到安装Docker宿主机文件系统之中,并提供一个读写层(Read-Write Layer),所以容器进程的所有操作都在读写层进行;

Docker 提供了两种构建镜像的方法:

  1. docker commit 命令
  2. Dockerfile 构建文件

docker commit

docker commit 命令是创建新镜像最直观的方法,其过程包含三个步骤:

  1. 运行容器
  2. 修改容器
  3. 将容器保存为新的镜像

举个例子:在 ubuntu base 镜像中安装 vim并保存为新镜像。

  1. 第一步, 运行容器

    bash 复制代码
    [root@docker ~ 09:46:39]# docker run -it ubuntu
    root@37fb2f834531:/#

    -it 参数的作用是以交互模式进入容器,并打开终端。d11014d4b667 是容器的内部 ID。

  2. 安装 vim

    确认 vim 没有安装。

    bash 复制代码
    root@37fb2f834531:/# vim
    bash: vim: command not found

    安装 vim。

    bash 复制代码
    root@37fb2f834531:/# apt-get update
    Get:1 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
    Get:2 http://archive.ubuntu.com/ubuntu noble InRelease [256 kB]
    ... ...
    
    
    root@37fb2f834531:/# apt-get install -y vim
    Reading package lists... Done
    Building dependency tree... Done
    Reading state information... Done
    ...
    
    debconf: (Can't locate Term/ReadLine.pm in @INC (you may need to install the Term::ReadLine module) (@INC entries checked: /etc/perl /usr/local/lib/x86_64-linux-gnu/perl/5.38.2 /usr/local/share/perl/5.38.2 /usr/lib/x86_64-linux-gnu/perl5/5.38 /usr/share/perl5 /usr/lib/x86_64-linux-gnu/perl-base /usr/lib/x86_64-linux-gnu/perl/5.38 /usr/share/perl/5.38 /usr/local/lib/site_perl) at /usr/share/perl5/Debconf/FrontEnd/Readline.pm line 8.)
    debconf: falling back to frontend: Teletype
    Configuring tzdata
    ------------------
    
    Please select the geographic area in which you live. Subsequent configuration questions will
    narrow this down by presenting a list of cities, representing the time zones in which they are
    located.
    
      1. Africa   3. Antarctica  5. Asia      7. Australia  9. Indian    11. Etc
      2. America  4. Arctic      6. Atlantic  8. Europe     10. Pacific  12. Legacy
    Geographic area: 5
    
    Please select the city or region corresponding to your time zone.
    
      1. Aden         19. Chongqing    37. Jerusalem     55. Novokuznetsk   73. Tashkent
      2. Almaty       20. Colombo      38. Kabul         56. Novosibirsk    74. Tbilisi
      3. Amman        21. Damascus     39. Kamchatka     57. Omsk           75. Tehran
      4. Anadyr       22. Dhaka        40. Karachi       58. Oral           76. Tel_Aviv
      5. Aqtau        23. Dili         41. Kashgar       59. Phnom_Penh     77. Thimphu
      6. Aqtobe       24. Dubai        42. Kathmandu     60. Pontianak      78. Tokyo
      7. Ashgabat     25. Dushanbe     43. Khandyga      61. Pyongyang      79. Tomsk
      8. Atyrau       26. Famagusta    44. Kolkata       62. Qatar          80. Ulaanbaatar
      9. Baghdad      27. Gaza         45. Krasnoyarsk   63. Qostanay       81. Urumqi
      10. Bahrain     28. Harbin       46. Kuala_Lumpur  64. Qyzylorda      82. Ust-Nera
      11. Baku        29. Hebron       47. Kuching       65. Riyadh         83. Vientiane
      12. Bangkok     30. Ho_Chi_Minh  48. Kuwait        66. Sakhalin       84. Vladivostok
      13. Barnaul     31. Hong_Kong    49. Macau         67. Samarkand      85. Yakutsk
      14. Beirut      32. Hovd         50. Magadan       68. Seoul          86. Yangon
      15. Bishkek     33. Irkutsk      51. Makassar      69. Shanghai       87. Yekaterinburg
      16. Brunei      34. Istanbul     52. Manila        70. Singapore      88. Yerevan
      17. Chita       35. Jakarta      53. Muscat        71. Srednekolymsk
      18. Choibalsan  36. Jayapura     54. Nicosia       72. Taipei
    Time zone: 69
    
    
    Current default time zone: 'Asia/Shanghai'
    Local time is now:      Fri Nov 14 09:51:10 CST 2025.
    Universal Time is now:  Fri Nov 14 01:51:10 UTC 2025.
    Run 'dpkg-reconfigure tzdata' if you wish to change it.
    
    Setting up vim-common (2:9.1.0016-1ubuntu7.9) ...
    ... ...
    /usr/bin/vim.basic to provide /usr/bin/vim (vim) in auto mode
    update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vimdiff (vimdiff) in auto mode
    Processing triggers for libc-bin (2.39-0ubuntu8.6) ...
  3. 保存为新镜像

    打开一个新窗口中查看当前运行的容器。

    bash 复制代码
    [root@docker ~ 09:52:20]# docker ps
    CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS         PORTS     NAMES
    37fb2f834531   ubuntu    "/bin/bash"   5 minutes ago   Up 5 minutes             awesome_dirac

    上面查看结果的解释如下:

    8dbdff6d3d88 是新创建容器的ID

    cool_darwin 是 Docker 为我们的容器随机分配的名字。

    执行 docker commit 命令将容器保存为镜像。

    bash 复制代码
    [root@docker ~ 09:52:24]# docker commit 37fb2f834531 ubuntu-with-vim
    sha256:d4cda41f77656fc39548ba539eed2bc44483d76e11414434d5cc7db0b94babcd

    新镜像命名为 ubuntu-with-vim

    查看新镜像的属性。

    bash 复制代码
    [root@docker ~ 09:52:59]# docker images
    REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
    ubuntu-with-vim   latest    d4cda41f7765   10 seconds ago   206MB
    httpd             latest    6a4fe18d08d2   9 days ago       117MB
    ubuntu            latest    97bed23a3497   6 weeks ago      78.1MB
    hello-world       latest    1b44b5a3e06a   3 months ago     10.1kB
    centos            7         eeb6ee3f44bd   4 years ago      204MB
    [root@docker ~ 09:53:09]# docker images ubuntu*
    REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
    ubuntu-with-vim   latest    d4cda41f7765   16 seconds ago   206MB
    ubuntu            latest    97bed23a3497   6 weeks ago      78.1MB

    从 size 上看到镜像因为安装了软件而变大了。

    从新镜像启动容器,验证 vim 已经可以使用。

    bash 复制代码
    [root@docker ~ 09:53:15]# docker run -it ubuntu-with-vim
    root@c71a49f14c04:/# which vim
    /usr/bin/vim

    以上演示了如何用 docker commit 创建新镜像。然而,Docker 并不建议用户通过这种方式构建镜像。原因如下:

    1. 这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vim,还得重复前面的所有步骤。
    2. 更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。

    既然 docker commit 不是推荐的方法,我们干嘛还要花时间学习呢?

    原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。学习 docker commit 能够帮助我们更加深入地理解构建过程和镜像的分层结构。

    下一节我们学习如何通过 Dockerfile 构建镜像。

013 Dockerfile构建镜像

Dockerfile 是一个文本文件,记录了镜像构建的所有步骤。

Dockerfile内容基础知识:

  1. 每条保留字指令都必须为大写字母且后面要跟随至少一个参数
  2. 指令按照从上到下,顺序执行
  3. #表示注释
  4. 每条指令都会创建一个新的镜像层并对镜像进行提交

常用参数:

bash 复制代码
docker build -f [Dockerfile路径] [构建上下文路径]
参数 作用
-f--file 标志符,声明要使用自定义 Dockerfile
[Dockerfile路径] 绝对路径相对于构建上下文的路径 (如 subdir/Dockerfile.dev
[构建上下文路径] Docker 打包发送给守护进程的目录(通常用 . 表示当前目录)

第一个 Dockerfile

用 Dockerfile 创建上节的 ubuntu-with-vim,其内容则为:

bash 复制代码
[root@docker ~ 10:31:58]# pwd
/root

[root@docker ~ 10:33:28]# vim Dockerfile
[root@docker ~ 10:33:59]# cat Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y vim

[root@docker ~ 10:38:56]# docker build -t ubuntu-with-vim-dockerfile .
[+] Building 42.8s (6/6) FINISHED                                                docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 94B                                                        0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                           0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest                                      0.0s
 => [2/2] RUN apt-get update && apt-get install -y vim                                    42.4s
 => exporting to image                                                                     0.4s
 => => exporting layers                                                                    0.4s
 => => writing image sha256:5274a4a9c236ff226c1f71abe25c350dd650afd2dac5998b80f5984a8c6f1  0.0s
 => => naming to docker.io/library/ubuntu-with-vim-dockerfile                              0.0s


① 当前目录为 /root。

② Dockerfile 准备就绪。

③ 运行 docker build 命令,-t 将新镜像命名为 ubuntu-with-vim-dockerfile,命令末尾的 . 指明 build context 为当前目录。Docker 默认会从 build context 中查找 Dockerfile 文件,我们也可以通过 -f 参数指定 Dockerfile 的位置。

④ 从这步开始就是镜像真正的构建过程。 首先 Docker 将 build context 中的所有文件发送给 Docker daemon。build context 为镜像构建提供所需要的文件或目录。

Dockerfile 中的 ADD、COPY 等命令可以将 build context 中的文件添加到镜像。此例中,build context 为当前目录 /root,该目录下的所有文件和子目录都会被发送给 Docker daemon。

所以,使用 build context 就得小心了,不要将多余文件放到 build context,特别不要把 //usr 作为 build context,否则构建过程会相当缓慢甚至失败。

⑤ Step 1:执行 FROM,将 ubuntu 作为 base 镜像。

⑥ Step 2:执行 RUN,安装 vim

⑦ 镜像构建成功。

⑧ 镜像重命名为ubuntu-with-vim-dockerfile

通过 docker images 查看镜像信息。

bash 复制代码
[root@docker ~ 10:57:05]# docker images
REPOSITORY                     TAG       IMAGE ID       CREATED              SIZE
ubuntu-with-vim-dockerfile     latest    5274a4a9c236   17 minutes ago       206MB
ubuntu-with-vim                latest    d4cda41f7765   About an hour ago    206MB
httpd                          latest    6a4fe18d08d2   9 days ago           117MB
ubuntu                         latest    97bed23a3497   6 weeks ago          78.1MB
hello-world                    latest    1b44b5a3e06a   3 months ago         10.1kB
centos                         7         eeb6ee3f44bd   4 years ago          204MB

镜像 ID 为 bbc08145d011,与构建时的输出一致。

在上面的构建过程中,我们要特别注意指令 RUN 的执行过程。Docker 会在启动的临时容器中执行操作,并通过 commit 保存为新的镜像。

查看镜像分层结构

ubuntu-with-vim-dockerfile 是通过在 base 镜像的顶部添加一个新的镜像层而得到的。

这个新镜像层的内容由 RUN apt-get update && apt-get install -y vim 生成。这一点我们可以通过 docker history 命令验证。

docker history 会显示镜像的构建历史,也就是 Dockerfile 的执行过程。

ubuntu-with-vi-dockerfile 与 ubuntu 镜像相比,确实只是多了顶部的一层 bbc08145d011,由 apt-get 命令创建,大小为 111MB。docker history 也向我们展示了镜像的分层结构,每一层由上至下排列。

注: 表示无法获取 IMAGE ID,通常从 Docker Hub 下载的镜像会有这个问题。

下一节我们学习镜像的缓存特性。

014 镜像的缓存特性

上一节我们学习了镜像的分层结构,接下来讨论镜像的缓存特性。

Docker 会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无需重新创建。

举例说明。

在前面的 Dockerfile 中添加一点新内容,往镜像中复制一个文件:

bash 复制代码
[root@docker ~ 10:54:54]# touch testfile
[root@docker ~ 10:55:04]# ls
anaconda-ks.cfg  Dockerfile  testfile

[root@docker ~ 10:39:42]# vim Dockerfile
[root@docker ~ 10:54:46]# cat Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y vim
COPY testfile /
[root@docker ~ 10:55:05]# docker build -t ubuntu-with-vim-dockerfile-2 .
[+] Building 0.1s (8/8) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 110B                                                       0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                           0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [1/3] FROM docker.io/library/ubuntu:latest                                             0.0s
 => [internal] load build context                                                          0.0s
 => => transferring context: 29B                                                           0.0s
 => CACHED [2/3] RUN apt-get update && apt-get install -y vim                              0.0s
 => [3/3] COPY testfile /                                                                  0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:421f8437d5a8a35aac8390aa4a6681b84f65d32e70b19f156458b51b0c457  0.0s
 => => naming to docker.io/library/ubuntu-with-vim-dockerfile-2                            0.0s

① 确保 testfile 已存在。可以通过touch创建

重点在这里:之前已经运行过相同的 RUN 指令,这次直接使用缓存中的镜像层

③ 执行 COPY 指令。

其过程是启动临时容器,复制 testfile,提交新的镜像层5561217926be,删除临时容器。

在 ubuntu-with-vi-dockerfile 镜像上直接添加一层就得到了新的镜像 ubuntu-with-vim-dockerfile-2。

如果我们希望在构建镜像时不使用缓存,可以在 docker build 命令中加上 --no-cache 参数。

Dockerfile 中每一个指令都会创建一个镜像层,上层是依赖于下层的。无论什么时候,只要某一层发生变化,其上面所有层的缓存都会失效。

也就是说,如果我们改变 Dockerfile 指令的执行顺序,或者修改或添加指令,都会使缓存失效。

举例说明,比如交换前面 RUN 和 COPY 的顺序:

bash 复制代码
[root@docker ~ 10:55:46]# vim Dockerfile
[root@docker ~ 10:56:05]# cat Dockerfile
FROM ubuntu
COPY testfile /
RUN apt-get update && apt-get install -y vim
[root@docker ~ 10:56:08]# docker build -t ubuntu-with-vim-dockerfile-3 .
[+] Building 44.7s (8/8) FINISHED                                                docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 110B                                                       0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                           0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [internal] load build context                                                          0.0s
 => => transferring context: 27B                                                           0.0s
 => CACHED [1/3] FROM docker.io/library/ubuntu:latest                                      0.0s
 => [2/3] COPY testfile /                                                                  0.0s
 => [3/3] RUN apt-get update && apt-get install -y vim                                    44.3s
 => exporting to image                                                                     0.4s
 => => exporting layers                                                                    0.4s
 => => writing image sha256:0bf68488b2d60282460268c68c5d93b2014f3eab26c338c7e14378df33871  0.0s
 => => naming to docker.io/library/ubuntu-with-vim-dockerfile-3                            0.0s

从上面的输出可以看到[2/3],[3/3]都没有使用缓存,最后生成了新的镜像层 33f20b2ec8fd,缓存已经失效。

除了构建时使用缓存,Docker 在下载镜像时也会使用。例如我们下载 httpd 镜像。

docker pull 命令输出显示第一层(base 镜像)已经存在,不需要下载。

由 Dockerfile 可知 httpd 的 base 镜像为 debian,正好之前已经下载过 debian 镜像,所以有缓存可用。通过 docker history 可以进一步验证。

bash 复制代码
[root@docker ~ 10:57:05]# docker images
REPOSITORY                     TAG       IMAGE ID       CREATED              SIZE
ubuntu-with-vim-dockerfile-3   latest    0bf68488b2d6   9 seconds ago        206MB
ubuntu-with-vim-dockerfile-2   latest    421f8437d5a8   About a minute ago   206MB
ubuntu-with-vim-dockerfile     latest    5274a4a9c236   17 minutes ago       206MB
ubuntu-with-vim                latest    d4cda41f7765   About an hour ago    206MB
httpd                          latest    6a4fe18d08d2   9 days ago           117MB
ubuntu                         latest    97bed23a3497   6 weeks ago          78.1MB
hello-world                    latest    1b44b5a3e06a   3 months ago         10.1kB
centos                         7         eeb6ee3f44bd   4 years ago          204MB

下一节我们学习如何调试 Dockerfile。

015 调试Dockerfile

包括 Dockerfile 在内的任何脚本和程序都会出错。有错并不可怕,但必须有办法排查,所以本节讨论如何 debug Dockerfile。

先回顾一下通过 Dockerfile 构建镜像的过程:

  1. 从 base 镜像运行一个容器。
  2. 执行一条指令,对容器做修改。
  3. 执行类似 docker commit 的操作,生成一个新的镜像层。
  4. Docker 再基于刚刚提交的镜像运行一个新容器。
  5. 重复 2-4 步,直到 Dockerfile 中的所有指令执行完毕。

从这个过程可以看出,如果 Dockerfile 由于某种原因执行到某个指令失败了,我们也将能够得到前一个指令成功执行构建出的镜像,这对调试 Dockerfile 非常有帮助。我们可以运行最新的这个镜像定位指令失败的原因。

我们来看一个调试的例子。Dockerfile 内容如下:

bash 复制代码
[root@docker ~ 10:58:22]# vim Dockerfile
[root@docker ~ 11:22:32]# ls
anaconda-ks.cfg  Dockerfile  testfile
[root@docker ~ 11:22:37]# cat Dockerfile
FROM busybox
RUN touch tmpfile
RUN /bin/bash -c "echo continue to build... "
COPY testfile /

#发现不对
[root@docker ~ 11:22:45]# docker build -t image-debug .
[+] Building 5.0s (7/8)                                                          docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 131B                                                       0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                          2.9s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [1/4] FROM docker.io/library/busybox:latest@sha256:e3652a00a2fabd16ce889f0aa32c38eec3  1.5s
 => => resolve docker.io/library/busybox:latest@sha256:e3652a00a2fabd16ce889f0aa32c38eec3  0.0s
 => => sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8 9.54kB / 9.54kB  0.0s
 => => sha256:870e815c3a50dd0f6b40efddb319c72c32c3ee340b5a3e8945904232ccd12f4 610B / 610B  0.0s
 => => sha256:08ef35a1c3f050afbbd64194ffd1b8d5878659f5491567f26d1c814513ae964 459B / 459B  0.0s
 => => sha256:e59838ecfec5e79eb4371e9995ef86c8000fe1c67d7b9fa7b57e996d9ba 2.21MB / 2.21MB  1.4s
 => => extracting sha256:e59838ecfec5e79eb4371e9995ef86c8000fe1c67d7b9fa7b57e996d9ba772ff  0.1s
 => [internal] load build context                                                          0.0s
 => => transferring context: 27B                                                           0.0s
 => [2/4] RUN touch tmpfile                                                                0.3s
 => ERROR [3/4] RUN /bin/bash -c "echo continue to build... "                              0.3s
------
 > [3/4] RUN /bin/bash -c "echo continue to build... ":
0.280 /bin/sh: /bin/bash: not found
------
Dockerfile:3
--------------------
   1 |     FROM busybox
   2 |     RUN touch tmpfile
   3 | >>> RUN /bin/bash -c "echo continue to build... "
   4 |     COPY testfile /
   5 |
--------------------
ERROR: failed to solve: process "/bin/sh -c /bin/bash -c \"echo continue to build... \"" did not complete successfully: exit code: 127

Dockerfile 在执行第三步 RUN 指令时失败。我们可以利用busybox的镜像进行调试,方式是通过 docker run -it 启动镜像的一个容器。

手工执行 RUN 指令很容易定位失败的原因是 busybox 镜像中没有 bash,busybox中用的是sh。虽然这是个极其简单的例子,但它很好地展示了调试 Dockerfile 的方法。

bash 复制代码
#登录找出原因
[root@docker ~ 11:23:13]# docker run -it busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
e59838ecfec5: Already exists
Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Status: Downloaded newer image for busybox:latest
/ # /bin/bash -c "echo continue to build... "
sh: /bin/bash: not found
/ # /bin/sh -c "echo continue to build... "
continue to build...
/ # exit


#修改
[root@docker ~ 11:23:52]# vim Dockerfile
[root@docker ~ 11:24:10]# cat Dockerfile
FROM busybox
RUN touch tmpfile
RUN /bin/sh -c "echo continue to build... "
COPY testfile /
[root@docker ~ 11:24:14]# docker build -t image-debug .
[+] Building 0.3s (9/9) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 129B                                                       0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                          0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [1/4] FROM docker.io/library/busybox:latest                                            0.0s
 => [internal] load build context                                                          0.0s
 => => transferring context: 27B                                                           0.0s
 => CACHED [2/4] RUN touch tmpfile                                                         0.0s
 => [3/4] RUN /bin/sh -c "echo continue to build... "                                      0.2s
 => [4/4] COPY testfile /                                                                  0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:49109ec280a2b91e6f6c54125870e3fe095b01d136073489be48d893c770e  0.0s
 => => naming to docker.io/library/image-debug  

到这里相信大家对 Dockerfile 的功能和使用流程有了比较完整的印象,但还没有系统学习 Dockerfile 的各种指令和实际用法,下节会开始这个主题。

016 Dockerfile常用指令

是时候系统学习 Dockerfile 了。

下面列出了 Dockerfile 中最常用的指令,完整列表和说明可参看官方文档。

FROM

指定 base 镜像。第一条必须是FROM

MAINTAINER

设置镜像的作者,可以是任意字符串。

COPY

将文件从 build context 复制到镜像。

COPY 支持两种形式:

  1. COPY src dest
  2. COPY ["src", "dest"]

注意:src 只能指定 build context 中的文件或目录。

ADD

与 COPY 类似,从 build context 复制文件到镜像。不同的是,如果 src 是归档文件(tar, zip, tgz, xz 等),文件会被自动解压到 dest。

ENV

设置环境变量,环境变量可被后面的指令使用。例如:

dockerfile 复制代码
...

ENV MY_VERSION 1.3

RUN apt-get install -y mypackage=$MY_VERSION

...
EXPOSE

指定容器中的进程会监听某个端口,Docker 可以将该端口暴露出来。我们会在容器网络部分详细讨论。

VOLUME

将文件或目录声明为 volume。我们会在容器存储部分详细讨论。

WORKDIR

为后面的 RUN, CMD, ENTRYPOINT, ADD 或 COPY 指令设置镜像中的当前工作目录。

RUN

在容器中运行指定的命令。

CMD

容器启动时运行指定的命令。

Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效。CMD 可以被 docker run 之后的参数替换。

ENTRYPOINT

设置容器启动时运行的命令。

Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或 docker run 之后的参数会被当做参数传递给 ENTRYPOINT。

下面我们来看一个较为全面的 Dockerfile:

bash 复制代码
# my dockerfile从busybox开始构建
FROM busybox                     
#声明作者信息
MAINTAINER 3186973288@qq.com         
#设置工作目录为/testdir
WORKDIR /testdir            
#在新镜像中创建tmpfille1
RUN touch tmpfile1              
#将Dockerfile文件所在目录中的tmpfile2文件拷贝到新镜像中
COPY ["tmpfile2","."]       
#将Dockerfile文件所在目录中的passwd.tar.gz拷贝到新镜像中并解压缩
ADD ["passwd.tar.gz","."]     
#设置环境变量
ENV WELCOME "You are in my container,welcome!"        WELOCME

完整的操作步骤如下:

bash 复制代码
[root@docker ~]# pwd                 #确定Dockerfile工作目录
/root
[root@docker ~]# ls                    #当前/root目录下空的
[root@docker ~]# touch tmpfile2             #创建空文档tmpfile2
[root@docker ~]# cp /etc/passwd .              #将/etc/passwd文件拷贝到/root
[root@docker ~]# tar -cvzf passwd.tar.gz passwd      #将passwd文件做出归档文件passwd.tar.gz
passwd
[root@docker ~]# rm passwd                  #删除passwd文件
rm: remove regular file 'passwd'? y
[root@docker ~]# vim Dockerfile          #编辑Dockerfile写入如下内容
# my dockerfile
FROM busybox
MAINTAINER 6946630@qq.com
WORKDIR /testdir
RUN touch tmpfile1
COPY ["tmpfile2","."]
ADD ["passwd.tar.gz","."]
ENV WELCOME "You are in my container,welcome!"

[root@docker ~]# ls                       #最后目录中有三个文件
Dockerfile  passwd.tar.gz  tmpfile2

[root@docker ~]# docker build -t my-image .    #构建新镜像my-image

① 构建前确保 build context 中存在需要的文件。

② 依次执行 Dockerfile 指令,完成构建。

运行容器,验证镜像内容:

bash 复制代码
docker run -it my-image

① 进入容器,当前目录即为 WORKDIR。

如果 WORKDIR 不存在,Docker 会自动为我们创建。

② WORKDIR 中保存了我们希望的文件和目录:

文件passwd:由 ADD 指令从 build context 复制的归档文件passwd.tar.gz,已经自动解压。

文件 tmpfile1:由 RUN 指令创建。

文件 tmpfile2:由 COPY 指令从 build context 复制。

③ ENV 指令定义的环境变量已经生效。

在上面这些指令中,RUN、CMD、ENTRYPOINT 很重要且容易混淆,下节专门讨论。

017 RUN vs CMD vs ENTRYPOINT

RUN、CMD 和 ENTRYPOINT 这三个 Dockerfile 指令看上去很类似,很容易混淆。本节将通过实践详细讨论它们的区别。

简单的说:

  1. RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。
  2. CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。
  3. ENTRYPOINT 配置容器启动时运行的命令。

下面我们详细分析。

Shell 和 Exec 格式

我们可用两种方式指定 RUN、CMD 和 ENTRYPOINT 要运行的命令:Shell 格式和 Exec 格式,二者在使用上有细微的区别。

Shell 格式

bash 复制代码
<instruction> <command>

例如:

bash 复制代码
RUN apt-get install python3  

CMD echo "Hello world"  

ENTRYPOINT echo "Hello world" 

当指令执行时,shell 格式底层会调用 /bin/sh -c 。

例如下面的 Dockerfile :

bash 复制代码
[root@docker ~]# vim Dockerfile
FROM busybox
ENV name dyx
ENTRYPOINT echo "Hello, $name" 

用上面的Dockerfile创建镜像dockerfile1用于测试

bash 复制代码
[root@docker ~]# docker build -t dockerfile1 .

执行 docker run dockerfile1:

bash 复制代码
[root@docker ~]# docker run dockerfile1
Hello,dyx

注意环境变量 name 已经被值 gqd 替换。

下面来看 Exec 格式。

Exec 格式

bash 复制代码
<instruction> ["executable", "param1", "param2", ...]

例如:

dockerfile 复制代码
RUN ["apt-get", "install", "python3"]  

CMD ["/bin/echo", "Hello world"]  

ENTRYPOINT ["/bin/echo", "Hello world"]

当指令执行时,会直接调用 ,不会被 shell 解析。

例如下面的 Dockerfile :

dockerfile 复制代码
[root@docker ~]# vim Dockerfile
FROM busybox
ENV name dyx  
ENTRYPOINT ["/bin/echo", "Hello, $name"]

用上面的Dockerfile创建镜像dockerfile2用于测试

bash 复制代码
[root@docker ~]# docker build -t dockerfile2 .

执行 docker run dockerfile2:

bash 复制代码
[root@docker ~]# docker run dockerfile2
hello,$name

注意环境变量"name"没有被替换。

如果希望使用环境变量,照如下修改

dockerfile 复制代码
[root@docker ~]# vim Dockerfile
FROM busybox
ENV name dyx  
ENTRYPOINT ["/bin/sh", "-c", "echo Hello, $name"]

用上面的Dockerfile创建镜像dockerfile3用于测试

bash 复制代码
[root@docker ~]# docker build -t dockerfile3 .

执行 docker run dockerfile3:

bash 复制代码
[root@docker ~]# docker run dockerfile3
Hello, dyx

CMD 和 ENTRYPOINT 推荐使用 Exec 格式,因为指令可读性更强,更容易理解。RUN 则两种格式都可以。

RUN

RUN 指令通常用于安装应用和软件包。

RUN 在当前镜像的顶部执行命令,并通过创建新的镜像层。Dockerfile 中常常包含多个 RUN 指令。

RUN 有两种格式:

  1. Shell 格式:RUN
  2. Exec 格式:RUN ["executable", "param1", "param2"]

下面的Dockerfile是使用 RUN 安装多个包的例子:

dockerfile 复制代码
[root@docker ~]# vim Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y \  
bzr \
cvs \
git \
mercurial \
subversion

用上面的Dockerfile创建镜像dockerfile4用于测试

bash 复制代码
[root@docker ~ 14:05:31]# docker build -t dockerfile4 .
[+] Building 159.0s (6/6) FINISHED                                               docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 134B                                                       0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                           0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/2] FROM docker.io/library/ubuntu:latest                                      0.0s
 => [2/2] RUN apt-get update && apt-get install -y bzr cvs git mercurial subversion      157.7s
 => exporting to image                                                                     1.0s
 => => exporting layers                                                                    1.0s
 => => writing image sha256:d4b433579fbab291fb1ce95f813d72fbd10bc87fc2d34dc3781020a4154fe  0.0s
 => => naming to docker.io/library/dockerfile4                                             0.0s

执行 docker run -it dockerfile4:

bash 复制代码
[root@docker ~ 14:08:12]# docker run -it dockerfile4
root@5d6a35b31025:/# apt list install brz cvs git mercurial subversion
Listing... Done
brz/noble,now 3.3.5-6build2 amd64 [installed,automatic]
cvs/noble,now 2:1.12.13+real-30build1 amd64 [installed]
git/noble-updates,noble-security,now 1:2.43.0-1ubuntu7.3 amd64 [installed]
mercurial/noble-updates,now 6.7.2-1ubuntu2.2 amd64 [installed]
subversion/noble,now 1.14.3-1build4 amd64 [installed]
root@5d6a35b31025:/# exit
exit

注意:apt-get update 和 apt-get install 被放在一个 RUN 指令中执行,这样能够保证每次安装的是最新的包。如果 apt-get install 在单独的 RUN 中执行,则会使用 apt-get update 创建的镜像层,而这一层可能是很久以前缓存的。

CMD

CMD 指令允许用户指定容器的默认执行的命令。

此命令会在容器启动且 docker run 没有指定其他命令时运行。

  1. 如果 docker run 指定了其他命令,CMD 指定的默认命令将被忽略。
  2. 如果 Dockerfile 中有多个 CMD 指令,只有最后一个 CMD 有效。

CMD 有三种格式:

  1. Exec 格式:CMD ["executable","param1","param2"] 这是 CMD 的推荐格式。
  2. CMD ["param1","param2"] 为 ENTRYPOINT 提供额外的参数,此时 ENTRYPOINT 必须使用 Exec 格式。
  3. Shell 格式:CMD command param1 param2

Exec 和 Shell 格式前面已经介绍过了。

第二种格式 CMD ["param1","param2"] 要与 Exec 格式 的 ENTRYPOINT 指令配合使用,其用途是为 ENTRYPOINT 设置默认的参数。我们将在后面讨论 ENTRYPOINT 时举例说明。

下面看看 CMD 是如何工作的。Dockerfile 如下:

bash 复制代码
[root@docker ~ 14:09:05]# vim Dockerfile
[root@docker ~ 14:09:45]# cat Dockerfile
FROM ubuntu
CMD echo "hello,world"

用上面的Dockerfile创建镜像dockerfile5用于测试

bash 复制代码
[root@docker ~ 14:09:32]# docker build -t dockerfile5 .
[+] Building 0.2s (5/5) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 73B                                                        0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                           0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/1] FROM docker.io/library/ubuntu:latest                                      0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:4c15a044b4b3eed60be970992d4bcf69a5d3315c8c7e5c0122664a2037feb  0.0s
 => => naming to docker.io/library/dockerfile5                                             0.0s

运行容器docker run -it dockerfile5将输出:

bash 复制代码
[root@docker ~ 14:09:50]# docker run -it dockerfile5
hello,world

但当后面加上一个命令,比如docker run -it dockerfile5 /bin/sh,CMD 会被忽略掉,命令 sh 将被执行:

bash 复制代码
[root@docker ~ 14:10:07]# docker run -it dockerfile5 /bin/sh
# h^Hpwd
/bin/sh: 1: pwd: not found
# pwd
/
# hostname
832a22007607
# exit

ENTRYPOINT

ENTRYPOINT 指令可让容器以应用程序或者服务的形式运行。

ENTRYPOINT 看上去与 CMD 很像,它们都可以指定要执行的命令及其参数。不同的地方在于 ENTRYPOINT 不会被忽略,一定会被执行,即使运行 docker run 时指定了其他命令。

ENTRYPOINT 有两种格式:

  1. Exec 格式:ENTRYPOINT ["executable", "param1", "param2"] 这是 ENTRYPOINT 的推荐格式。
  2. Shell 格式:ENTRYPOINT command param1 param2

在为 ENTRYPOINT 选择格式时必须小心,因为这两种格式的效果差别很大。

Exec 格式

ENTRYPOINT 的 Exec 格式用于设置要执行的命令及其参数,同时可通过 CMD 提供额外的参数。

ENTRYPOINT 中的参数始终会被使用,而 CMD 的额外参数可以在容器启动时动态替换掉。

比如下面的 Dockerfile :

bash 复制代码
[root@docker ~ 14:10:53]# vim Dockerfile
[root@docker ~ 14:25:08]# cat Dockerfile
FROM busybox
ENTRYPOINT ["/bin/echo","hello"]
CMD ["world"]

用上面的Dockerfile创建镜像dockerfile6用于测试

bash 复制代码
[root@docker ~ 14:24:27]# docker build -t dockerfile6 .
[+] Building 0.2s (5/5) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 98B                                                        0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                          0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/1] FROM docker.io/library/busybox:latest                                     0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:606a8cce24a08bdffda164c52a24eeb55a7c8fbae584a30da421a856fbe78  0.0s
 => => naming to docker.io/library/dockerfile6                                             0.0s

当容器通过 docker run -it dockerfile6 启动时,输出为:

bash 复制代码
[root@docker ~ 14:24:49]# docker run -it dockerfile6
hello world

而如果通过 docker run -it dockerfile6 gqd 启动,则输出为:

bash 复制代码
[root@docker ~ 14:25:03]# docker run -it dockerfile6 dyx
hello dyx

Shell 格式

ENTRYPOINT 的 Shell 格式会忽略任何 CMD 或 docker run 提供的参数。

比如下面的 Dockerfile :

bash 复制代码
[root@docker ~ 14:25:18]# vim Dockerfile
[root@docker ~ 14:26:15]# cat Dockerfile
FROM busybox
ENTRYPOINT echo "hello"
CMD ["world"]

用上面的Dockerfile创建镜像dockerfile7用于测试

bash 复制代码
[root@docker ~ 14:25:47]# docker build -t dockerfile7 .
[+] Building 0.2s (5/5) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 89B                                                        0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                          0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/1] FROM docker.io/library/busybox:latest                                     0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:83c33ca600bc910b79b091304cad9d16c24892cdbce657b3b7ca3e15a4c77  0.0s
 => => naming to docker.io/library/dockerfile7                                             0.0s

当容器通过 docker run -it dockerfile7 启动时,输出为:

bash 复制代码
[root@docker ~ 14:25:58]# docker run -it dockerfile7
hello

而如果通过 docker run -it dockerfile7 gqd 启动,则输出为:

bash 复制代码
[root@docker ~ 14:26:10]# docker run -it dockerfile7 dyx
hello

Shell 格式

最佳实践

  1. 使用 RUN 指令安装应用和软件包,构建镜像。
  2. 如果 Docker 镜像的用途是运行应用程序或服务,比如运行一个 MySQL,应该优先使用 Exec 格式的 ENTRYPOINT 指令。CMD 可为 ENTRYPOINT 提供额外的默认参数,同时可利用 docker run 命令行替换默认参数。
  3. 如果想为容器设置默认的启动命令,可使用 CMD 指令。用户可在 docker run 命令行中替换此默认命令。

到这里,我们已经具备编写 Dockerfile 的能力了。如果大家还觉得没把握,推荐一个快速掌握 Dockerfile 的方法:去 Docker Hub 上参考那些官方镜像的 Dockerfile

好了,我们已经学习完如何创建自己的 image,下一节讨论如何分发 image。

Dockerfile案例:配置SSH镜像

项目背景:官方下载的centos镜像默认不带ssh,管理起来不方便,自己制作一个带SSH功能的centos镜像

创建dockerfile

bash 复制代码
[root@docker ~ 15:06:45]# vim centos.ssh.dockerfile
[root@docker ~ 15:10:32]# cat centos.ssh.dockerfile
FROM centos:8.4.2105
MAINTAINER dengyunxin
RUN  minorver=8.4.2105 && \
sed -e "s|^mirrorlist=|#mirrorlist=|g" \
-e "s|^#baseurl=http://mirror.centos.org/\$contentdir/\$releasever|baseurl=https://mirrors.aliyun.com/centos-vault/$minorver|g" \
-i.bak \
/etc/yum.repos.d/CentOS-*.repo
RUN yum install -y openssh-server
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
RUN ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key
RUN echo "root:123" |chpasswd
EXPOSE 22
CMD ["/usr/sbin/sshd","-D"]

构建镜像

bash 复制代码
[root@docker ~ 15:11:44]# docker build -t centos:ssh -f centos.ssh.dockerfile .
[+] Building 16.9s (10/10) FINISHED                                              docker:default
 => [internal] load build definition from centos.ssh.dockerfile                            0.0s
 => => transferring dockerfile: 540B                                                       0.0s
 => [internal] load metadata for docker.io/library/centos:8.4.2105                         0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [1/6] FROM docker.io/library/centos:8.4.2105                                           0.0s
 => [2/6] RUN  minorver=8.4.2105 && sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#baseur  0.7s
 => [3/6] RUN yum install -y openssh-server                                               13.7s
 => [4/6] RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key                               0.7s
 => [5/6] RUN ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key                           0.7s
 => [6/6] RUN echo "root:123" |chpasswd                                                    0.8s
 => exporting to image                                                                     0.1s
 => => exporting layers                                                                    0.1s
 => => writing image sha256:b30c050a37a1255fb04f79470931d20014a60527f107ebbe8e0126e76eaf2  0.0s
 => => naming to docker.io/library/centos:ssh                                              0.0s

查看现象

bash 复制代码
[root@docker ~ 15:12:15]# docker history centos:ssh
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
b30c050a37a1   11 seconds ago   CMD ["/usr/sbin/sshd" "-D"]                     0B        buildkit.dockerfile.v0
<missing>      11 seconds ago   EXPOSE map[22/tcp:{}]                           0B        buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c echo "root:123" |chpasswd # b...   1.77kB    buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c ssh-keygen -t ecdsa -f /etc/s...   695B      buildkit.dockerfile.v0
<missing>      12 seconds ago   RUN /bin/sh -c ssh-keygen -t rsa -f /etc/ssh...   3.18kB    buildkit.dockerfile.v0
<missing>      13 seconds ago   RUN /bin/sh -c yum install -y openssh-server...   51.9MB    buildkit.dockerfile.v0
<missing>      26 seconds ago   RUN /bin/sh -c minorver=8.4.2105 && sed -e "...   17.6kB    buildkit.dockerfile.v0
<missing>      26 seconds ago   MAINTAINER dengyunxin                           0B        buildkit.dockerfile.v0
<missing>      4 years ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      4 years ago      /bin/sh -c #(nop)  LABEL org.label-schema.sc...   0B
<missing>      4 years ago      /bin/sh -c #(nop) ADD file:805cb5e15fb6e0bb0...   231MB

测试

bash 复制代码
#基于刚才dockerfile创建的镜像centos:ssh创建容器sshtest
[root@docker ~ 15:12:25]# docker run -d -p 2022:22 --name sshtest centos:ssh
11c821d561823349f0e1fc3daae225e7e3c74689a3f9a3069a4f63437aa5eef8

#创建出来的容器
[root@docker ~ 15:12:50]# docker ps
CONTAINER ID   IMAGE        COMMAND               CREATED         STATUS         PORTS                                   NAMES
11c821d56182   centos:ssh   "/usr/sbin/sshd -D"   5 seconds ago   Up 4 seconds   0.0.0.0:2022->22/tcp, :::2022->22/tcp   sshtest

#ssh登录容器测试ssh,能够成功登录
[root@docker ~ 15:12:54]# ssh root@localhost -p 2022
The authenticity of host '[localhost]:2022 ([::1]:2022)' can't be established.
ECDSA key fingerprint is SHA256:0Nk+VgvLlmkf9hyRGJDMI0B0po3TbfukD+lVBEUswLg.
Are you sure you want to continue connecting (yes/no/[fingerprint])? `yes`
Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts.
root@localhost's password:
"System is booting up. Unprivileged users are not permitted to log in yet. Please come back later. For technical details, see pam_nologin(8)."
Last failed login: Fri Nov 14 07:13:20 UTC 2025 from 172.17.0.1 on ssh:notty
There was 1 failed login attempt since the last successful login.
[root@11c821d56182 ~]# pwd
/root
[root@11c821d56182 ~]# ls
anaconda-ks.cfg  anaconda-post.log  original-ks.cfg
[root@11c821d56182 ~]# exit
logout
Connection to localhost closed.
Dockerfile案例:自定义httpd镜像

创建dockerfile

bash 复制代码
[root@docker ~ 15:42:56]# vim httpd.dockerfile
[root@docker ~ 15:45:52]# cat httpd.dockerfile
FROM centos:8.4.2105
MAINTAINER dengyunxin
RUN minorver=8.4.2105 \
&& sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#baseurl=http://mirror.centos.org/\$contentdir/\$releasever|baseurl=https://mirrors.aliyun.com/centos-vault/$minorver|g" -i.bak /etc/yum.repos.d/CentOS-*.repo
RUN yum install -y httpd && yum clean all && rm -rf /var/cache/yum
COPY index.html /var/www/html/
EXPOSE 80
CMD ["/usr/sbin/httpd","-DFOREGROUND"]
[root@docker ~ 15:45:55]# echo hello world > index.html

构建镜像

bash 复制代码
[root@docker ~ 15:46:36]# docker build -t httpd:centos -f httpd.dockerfile .
[+] Building 20.3s (9/9) FINISHED                                                docker:default
 => [internal] load build definition from httpd.dockerfile                                 0.0s
 => => transferring dockerfile: 468B                                                       0.0s
 => [internal] load metadata for docker.io/library/centos:8.4.2105                         0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => [1/4] FROM docker.io/library/centos:8.4.2105                                           0.0s
 => [internal] load build context                                                          0.0s
 => => transferring context: 49B                                                           0.0s
 => CACHED [2/4] RUN minorver=8.4.2105 && sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#  0.0s
 => [3/4] RUN yum install -y httpd && yum clean all && rm -rf /var/cache/yum              20.0s
 => [4/4] COPY index.html /var/www/html/                                                   0.1s
 => exporting to image                                                                     0.1s
 => => exporting layers                                                                    0.1s
 => => writing image sha256:6171136454ff36fec7ef836e5e4caab6d03e8b89f3ad8307882574711445b  0.0s
 => => naming to docker.io/library/httpd:centos                                            0.0s

查看现象

bash 复制代码
[root@docker ~ 15:47:20]# docker history httpd:centos
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
6171136454ff   11 seconds ago   CMD ["/usr/sbin/httpd" "-DFOREGROUND"]          0B        buildkit.dockerfile.v0
<missing>      11 seconds ago   EXPOSE map[80/tcp:{}]                           0B        buildkit.dockerfile.v0
<missing>      11 seconds ago   COPY index.html /var/www/html/ # buildkit       12B       buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c yum install -y httpd && yum c...   21.6MB    buildkit.dockerfile.v0
<missing>      35 minutes ago   RUN /bin/sh -c minorver=8.4.2105 && sed -e "...   17.6kB    buildkit.dockerfile.v0
<missing>      35 minutes ago   MAINTAINER dengyunxin                           0B        buildkit.dockerfile.v0
<missing>      4 years ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      4 years ago      /bin/sh -c #(nop)  LABEL org.label-schema.sc...   0B
<missing>      4 years ago      /bin/sh -c #(nop) ADD file:805cb5e15fb6e0bb0...   231MB

测试

bash 复制代码
#基于刚才dockerfile创建的镜像httpd:centos创建容器myweb
[root@docker ~ 15:47:31]# docker run -d -p 80:80 --name myweb httpd:centos
59606785f5fe4eb11ac630f319b68327b286b1abf9b0a03be0d2d6e028e3cee6

#创建出来的容器
[root@docker ~ 15:47:58]# docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                                   NAMES
59606785f5fe   httpd:centos   "/usr/sbin/httpd -DF..."   5 seconds ago    Up 4 seconds    0.0.0.0:80->80/tcp, :::80->80/tcp       myweb
11c821d56182   centos:ssh     "/usr/sbin/sshd -D"      35 minutes ago   Up 35 minutes   0.0.0.0:2022->22/tcp, :::2022->22/tcp   sshtest

#访问测试
[root@docker ~ 15:48:02]#curl localhost
Hello World
Dockerfile案例:自定义mycentosjava8

项目背景:java工程师需要我们给他发个带JAVA的centos

要求CentOS8镜像具备vim+ifconfig+jdk8

JDK的下载地址:https://www.oracle.com/java/technologies/downloads/#java8

编写Dockerfile文件

bash 复制代码
[root@docker ~ 16:20:05]# mkdir myfile ;cd myfile

[root@docker myfile 16:27:13]# vim Dockerfile
dockerfile 复制代码
FROM centos:8.4.2105
MAINTAINER dengyunxin<3186973288@qq.com>

ENV MYPATH /usr/local
WORKDIR $MYPATH

#配置yum源
RUN minorver=8.4.2105 \
&& sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#baseurl=http://mirror.centos.org/\$contentdir/\$releasever|baseurl=https://mirrors.aliyun.com/centos-vault/$minorver|g" -i.bak /etc/yum.repos.d/CentOS-*.repo

#安装vim编辑器,安装ifconfig命令查看网络IP,java8及lib库
RUN yum install -y vim && yum -y install net-tools && yum install -y glibc.i686
RUN mkdir /usr/local/java

#ADD 是相对路径jar,把jdk-8u461-linux-x64.tar.gz添加到容器中,安装包必须要和Dockerfile文件在同一位置
ADD jdk-8u461-linux-x64.tar.gz /usr/local/java/

#配置java环境变量
ENV JAVA_HOME /usr/local/java/jdk1.8.0_461
ENV JRE_HOME $JAVA_HOME/jre
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

EXPOSE 80
CMD echo $MYPATH
CMD echo "success---------ok"
CMD /bin/bash
bash 复制代码
# 将jdk-8u461-linux-x64.tar.gz与Dockerfile放到同一目录
[root@docker myfile 16:28:42]# ls
Dockerfile  jdk-8u461-linux-x64.tar.gz

# 构建镜像为centosjava8:461
[root@docker myfile 16:29:54]# docker build -t centosjava8:461 .
[+] Building 48.7s (11/11) FINISHED                                              docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 789B                                                       0.0s
 => [internal] load metadata for docker.io/library/centos:8.4.2105                         0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/6] FROM docker.io/library/centos:8.4.2105                                    0.0s
 => [internal] load build context                                                          0.8s
 => => transferring context: 79.45MB                                                       0.8s
 => [2/6] WORKDIR /usr/local                                                               0.1s
 => [3/6] RUN minorver=8.4.2105 && sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#baseurl  0.6s
 => [4/6] RUN yum install -y vim && yum -y install net-tools && yum install -y glibc.i68  45.3s
 => [5/6] RUN mkdir /usr/local/java                                                        0.7s
 => [6/6] ADD jdk-8u461-linux-x64.tar.gz /usr/local/java/                                  1.3s
 => exporting to image                                                                     0.5s
 => => exporting layers                                                                    0.5s
 => => writing image sha256:f6376d8a33de78eae77821a7bac9b15ea0685e52faf32e06ab6032fdc2e60  0.0s
 => => naming to docker.io/library/centosjava8:461                                         0.0s

查看构建的镜像

bash 复制代码
[root@docker myfile 16:30:48]# docker images
REPOSITORY                     TAG        IMAGE ID       CREATED             SIZE
centosjava8                    461        f6376d8a33de   9 seconds ago       509MB
httpd                          centos     6171136454ff   43 minutes ago      253MB
centos                         ssh        b30c050a37a1   About an hour ago   283MB
dockerfile4                    latest     d4b433579fba   2 hours ago         356MB
image-debug                    latest     49109ec280a2   5 hours ago         4.43MB
ubuntu-with-vim-dockerfile-3   latest     0bf68488b2d6   6 hours ago         206MB
ubuntu-with-vim-dockerfile-2   latest     421f8437d5a8   6 hours ago         206MB
ubuntu-with-vim-dockerfile     latest     5274a4a9c236   6 hours ago         206MB
ubuntu-with-vim                latest     d4cda41f7765   7 hours ago         206MB
httpd                          latest     6a4fe18d08d2   10 days ago         117MB
dockerfile5                    latest     4c15a044b4b3   6 weeks ago         78.1MB
ubuntu                         latest     97bed23a3497   6 weeks ago         78.1MB
hello-world                    latest     1b44b5a3e06a   3 months ago        10.1kB
busybox                        latest     08ef35a1c3f0   13 months ago       4.43MB
dockerfile6                    latest     606a8cce24a0   13 months ago       4.43MB
dockerfile7                    latest     83c33ca600bc   13 months ago       4.43MB
centos                         7          eeb6ee3f44bd   4 years ago         204MB
centos                         8.4.2105   5d0da3dc9764   4 years ago         231MB

用创建的镜像运行容器测试

bash 复制代码
[root@docker myfile 16:30:56]# docker run -it centosjava8:461
[root@17e73308d96e local]# pwd
/usr/local
[root@17e73308d96e local]# ls
bin  etc  games  include  java  lib  lib64  libexec  sbin  share  src
[root@17e73308d96e local]# java -version
java version "1.8.0_461"
Java(TM) SE Runtime Environment (build 1.8.0_461-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.461-b11, mixed mode)
[root@17e73308d96e local]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.4  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:ac:11:00:04  txqueuelen 0  (Ethernet)
        RX packets 8  bytes 656 (656.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

[root@17e73308d96e local]# vim file1
[root@17e73308d96e local]#

018 镜像命名的最佳实践

我们已经学会构建自己的镜像了。接下来的问题是如何在多个 Docker Host 上使用镜像。

这里有几种可用的方法:

  1. 用相同的 Dockerfile 在其他 host 构建镜像。
  2. 将镜像上传到公共 Registry(比如 Docker Hub),Host 直接下载使用。
  3. 搭建私有的 Registry 供本地 Host 使用。

第一种方法没什么特别的,前面已经讨论很多了。我们将讨论如何使用公共和私有 Registry 分发镜像。

为镜像命名

无论采用何种方式保存和分发镜像,首先都得给镜像命名。

当我们执行 docker build命令时已经为镜像取了个名字,例如前面:

docker build -t ubuntu-with-vim

这里的 ubuntu-with-vim 就是镜像的名字。通过 dock images 可以查看镜像的信息。

这里注意到 ubuntu-with-vim 对应的是 REPOSITORY ,而且还有一个叫 latestTAG

实际上一个特定镜像的名字由两部分组成:repository 和 tag。

[image name] = [repository]:[tag]

全称如下:

镜像名称格式:Image hub address/Namespace/Repository:tag054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com/library/nginx:latest

如果执行 docker build 时没有指定 tag,会使用默认值 latest。其效果相当于:

docker build -t ubuntu-with-vim:latest

tag 常用于描述镜像的版本信息,比如 httpd 镜像:

当然 tag 可以是任意字符串,比如 ubuntu 镜像:

小心 latest tag

千万别被 latest tag 给误导了。latest 其实并没有什么特殊的含义。当没指明镜像 tag 时,Docker 会使用默认值 latest,仅此而已。

虽然 Docker Hub 上很多 repository 将 latest 作为最新稳定版本的别名,但这只是一种约定,而不是强制规定。

所以我们在使用镜像时最好还是避免使用 latest,明确指定某个 tag,比如 httpd:2.3,ubuntu:xenial。

tag 使用最佳实践

借鉴软件版本命名方式能够让用户很好地使用镜像。

一个高效的版本命名方案可以让用户清楚地知道当前使用的是哪个镜像,同时还可以保持足够的灵活性。

每个 repository 可以有多个 tag,而多个 tag 可能对应的是同一个镜像。下面通过例子为大家介绍 Docker 社区普遍使用的 tag 方案。

假设我们现在发布了一个镜像 myimage,版本为 v1.9.1。那么我们可以给镜像打上四个 tag:1.9.1、1.9、1 和 latest。

我们可以通过 docker tag 命令方便地给镜像打 tag。

docker tag myimage-v1.9.1 myimage:1

docker tag myimage-v1.9.1 myimage:1.9

docker tag myimage-v1.9.1 myimage:1.9.1

docker tag myimage-v1.9.1 myimage:latest

过了一段时间,我们发布了 v1.9.2。这时可以打上 1.9.2 的 tag,并将 1.9、1 和 latest 从 v1.9.1 移到 v1.9.2。

命令为:

docker tag myimage-v1.9.2 myimage:1

docker tag myimage-v1.9.2 myimage:1.9

docker tag myimage-v1.9.2 myimage:1.9.2

docker tag myimage-v1.9.2 myimage:latest

之后,v2.0.0 发布了。这时可以打上 2.0.0、2.0 和 2 的 tag,并将 latest 移到 v2.0.0。

命令为:

docker tag myimage-v2.0.0 myimage:2

docker tag myimage-v2.0.0 myimage:2.0

docker tag myimage-v2.0.0 myimage:2.0.0

docker tag myimage-v2.0.0 myimage:latest

这种 tag 方案使镜像的版本很直观,用户在选择非常灵活:

  1. myimage:1 始终指向 1 这个分支中最新的镜像。
  2. myimage:1.9 始终指向 1.9.x 中最新的镜像。
  3. myimage:latest 始终指向所有版本中最新的镜像。
  4. 如果想使用特定版本,可以选择 myimage:1.9.1、myimage:1.9.2 或 myimage:2.0.0。

Docker Hub 上很多 repository 都采用这种方案,所以大家一定要熟悉。

下一节讨论如何使用使用公共 Registry。

019 使用公共Registry-dockerhub

Docker仓库类似于代码仓库,它是Docker集中存放镜像文件的场所。仓库注册服务器存放着很多类镜像,每类镜像包括多个镜像文件,通过不同的标签(tag)来进行区分。

根据所存储的镜像公开分享与否,Docker仓库可以分为:

  • 公开仓库(Public)
  • 私有仓库(Private)

目前,最大的公开仓库是官方提供的Docker Hub,其中存放了数量庞大的镜像供用户下载。国内不少云服务提供商(如华为、阿里云等)也提供了仓库的本地源,可以提供稳定的国内访问。

当然,用户如果不希望公开分享自己的镜像文件,Docker也支持用户在本地网络内创建一个只能自己访问的私有仓库。当用户创建了自己的镜像之后就可以使用push命令将它上传到指定的公有或者私有仓库。这样用户下次在另外一台机器上使用该镜像时,只需要将其从仓库上pull下来就可以了。

保存和分发镜像的最直接方法就是使用 Docker Hub。

Docker Hub 是 Docker 公司维护的公共 Registry。用户可以将自己的镜像保存到 Docker Hub 免费的 repository 中。如果不希望别人访问自己的镜像,也可以购买私有 repository。

除了 Docker Hub,quay.io 是另一个公共 Registry,提供与 Docker Hub 类似的服务。

下面介绍如何用 Docker Hub 存取我们的镜像。

  1. 首先得在 Docker Hub 上注册一个账号。

  2. 在 Docker Host 上登录。

    这里用的是我自己的账号,用户名为 gaoqd,输入密码后登录成功。

  3. 修改镜像的 repository 使之与 Docker Hub 账号匹配。

    Docker Hub 为了区分不同用户的同名镜像,镜像的 registry 中要包含用户名,完整格式为:[username]/xxx:tag

    我们通过 docker tag 命令重命名镜像。

    注:Docker 官方自己维护的镜像没有用户名,比如 httpd。

  4. 通过 docker push 将镜像上传到 Docker Hub。

    Docker 会上传镜像的每一层。因为 gaoqd/httpd:v1 这个镜像实际上跟官方的 httpd 镜像一模一样,Docker Hub 上已经有了全部的镜像层,所以真正上传的数据很少。同样的,如果我们的镜像是基于 base 镜像的,也只有新增加的镜像层会被上传。如果想上传同一 repository 中所有镜像,省略 tag 部分就可以了,例如:

    docker push gaoqd/httpd

  5. 登录 https://hub.docker.com,在Public Repository 中就可以看到上传的镜像。

    如果要删除上传的镜像,只能在 Docker Hub 界面上操作。

  6. 这个镜像可被其他 Docker host 下载使用了。

使用公共Registry-华为云

登录华为云,找到容器镜像服务SWR,点击控制台,选择区域,选择离自己近的数据中心,创建组织,进入新创建的组织,接下来登录华为云,上传自己的镜像,复制登录指令。

通过上面获取的登录指令,回到docker上登陆

bash 复制代码
[root@docker myfile 17:02:24]# docker login -u cn-east-3@HST3WEWRJ2XGY38FSM55 -p 8e43b4c06effae838a6f9831a9b3d73553207e9b7f459f1d6c8d26d swr.cn-east-3.myhuaweicloud.com
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

上传镜像

bash 复制代码
#命令格式
sudo docker tag {镜像名称}:{版本名称} swr.cn-east-3.myhuaweicloud.com/{组织名称}/{镜像名称}:{版本名称}
sudo docker push swr.cn-east-3.myhuaweicloud.com/{组织名称}/{镜像名称}:{版本名称}
bash 复制代码
[root@docker ~ 17:06:33]# docker build -t busybox:123 .
[+] Building 0.2s (5/5) FINISHED                                                 docker:default
 => [internal] load build definition from Dockerfile                                       0.0s
 => => transferring dockerfile: 89B                                                        0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                          0.0s
 => [internal] load .dockerignore                                                          0.0s
 => => transferring context: 2B                                                            0.0s
 => CACHED [1/1] FROM docker.io/library/busybox:latest                                     0.0s
 => exporting to image                                                                     0.0s
 => => exporting layers                                                                    0.0s
 => => writing image sha256:83c33ca600bc910b79b091304cad9d16c24892cdbce657b3b7ca3e15a4c77  0.0s
 => => naming to docker.io/library/busybox:123                                             0.0s
[root@docker ~ 17:06:58]# docker tag busybox:123 swr.cn-east-3.myhuaweicloud/dengyunxin/busybox:v1
[root@docker ~ 17:08:30]# docker images
REPOSITORY                                       TAG        IMAGE ID       CREATED             SIZE
centosjava8                                      461        f6376d8a33de   37 minutes ago      509MB
httpd                                            centos     6171136454ff   About an hour ago   253MB
centos                                           ssh        b30c050a37a1   2 hours ago         283MB
dockerfile4                                      latest     d4b433579fba   3 hours ago         356MB
image-debug                                      latest     49109ec280a2   6 hours ago         4.43MB
ubuntu-with-vim-dockerfile-3                     latest     0bf68488b2d6   6 hours ago         206MB
ubuntu-with-vim-dockerfile-2                     latest     421f8437d5a8   6 hours ago         206MB
ubuntu-with-vim-dockerfile                       latest     5274a4a9c236   6 hours ago         206MB
ubuntu-with-vim                                  latest     d4cda41f7765   7 hours ago         206MB
httpd                                            latest     6a4fe18d08d2   10 days ago         117MB
dockerfile5                                      latest     4c15a044b4b3   6 weeks ago         78.1MB
ubuntu                                           latest     97bed23a3497   6 weeks ago         78.1MB
hello-world                                      latest     1b44b5a3e06a   3 months ago        10.1kB
dockerfile7                                      latest     83c33ca600bc   13 months ago       4.43MB
swr.cn-east-3.myhuaweicloud/dengyunxin/busybox   v1         83c33ca600bc   13 months ago       4.43MB
busybox                                          123        83c33ca600bc   13 months ago       4.43MB
busybox                                          latest     08ef35a1c3f0   13 months ago       4.43MB
dockerfile6                                      latest     606a8cce24a0   13 months ago       4.43MB
centos                                           7          eeb6ee3f44bd   4 years ago         204MB
centos                                           8.4.2105   5d0da3dc9764   4 years ago         231MB


#上传镜像
[root@docker ~ 17:08:36]# docker push swr.cn-east-3.myhuaweicloud/dengyunxin/busybox:v1
The push refers to repository [swr.cn-east-3.myhuaweicloud/dengyunxin/busybox]
Get "https://swr.cn-east-3.myhuaweicloud/v2/": dial tcp: lookup swr.cn-east-3.myhuaweicloud on 192.168.108.2:53: no such host
bash 复制代码
[root@docker ~ 17:09:03]# docker tag busybox:123 swr.cn-east-3.myhuaweicloud.com/dengyunxin/busybox:v1
[root@docker ~ 17:09:49]# docker push swr.cn-east-3.myhuaweicloud.com/dengyunxin/busybox:v1     The push refers to repository [swr.cn-east-3.myhuaweicloud.com/dengyunxin/busybox]
e14542cc0629: Pushed
v1: digest: sha256:2820d161ff144926c1db5eb2ec4aae41119951c6e1628fed0b419248f6d78ff9 size: 527

上传成功登录华为云查看:

下载自己上传的镜像

本地先把镜像删除再下载

bash 复制代码
[root@docker ~]# docker pull swr.cn-east-3.myhuaweicloud.com/dengyunxin/busybox:v1

设置为公开**

点击右上角**...**,修改成公开

020 搭建本地Registry

Docker Hub 虽然非常方便,但还是有些限制,比如:

  1. 需要 internet 连接,而且下载和上传速度慢。

  2. 上传到 Docker Hub 的镜像任何人都能够访问,虽然可以用私有 repository,但不是免费的。

  3. 安全原因很多组织不允许将镜像放到外网。

解决方案就是搭建本地的 Registry。

Registry

Docker 已经将 Registry 开源了,同时在 Docker Hub 上也有官方的镜像 registry。下面我们就在 Docker 中运行自己的 registry。

  1. 启动 registry 容器。

    bash 复制代码
    [root@docker ~ 09:32:16]# docker run -d -p 5000:5000 -v /myregistry:/val/lib/regUnable to find image 'registry:2' locally
    2: Pulling from library/registry
    44cf07d57ee4: Pull complete
    bbbdd6c6894b: Pull complete
    8e82f80af0de: Pull complete
    3493bf46cdec: Pull complete
    6d464ea18732: Pull complete
    Digest: sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373
    Status: Downloaded newer image for registry:2
    294ce1ae55735333a9081b040c82969afe743c92c11f03b8e186d1a27daa33c4

    们使用的镜像是 registry:2

    -d 是后台启动容器。

    -p 将容器的 5000 端口映射到 Host 的 5000 端口。5000 是 registry 服务端口。端口映射我们会在容器网络章节详细讨论。

    -v 将容器 /var/lib/registry 目录映射到 Host 的 /myregistry,用于存放镜像数据。-v 的使用我们会在容器存储章节详细讨论。

  2. 通过 docker tag 重命名镜像,使之与 registry 匹配。

    bash 复制代码
    [root@docker ~ 09:35:38]# docker pull busybox
    Using default tag: latest
    latest: Pulling from library/busybox
    e59838ecfec5: Pull complete
    Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
    Status: Downloaded newer image for busybox:latest
    docker.io/library/busybox:latest
    [root@docker ~ 09:36:43]# docker pull httpd
    Using default tag: latest
    latest: Pulling from library/httpd
    d7ecded7702a: Pull complete
    fab98c44430d: Pull complete
    4f4fb700ef54: Pull complete
    13c22d886563: Pull complete
    4870f70a8556: Pull complete
    233dec01418c: Pull complete
    Digest: sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
    Status: Downloaded newer image for httpd:latest
    docker.io/library/httpd:latest
    
    
    [root@docker ~ 09:41:19]# docker images
    REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
    httpd        latest    6a4fe18d08d2   12 days ago     117MB
    busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
    registry     2         26b2eb03618e   2 years ago     25.4MB
    
    
    [root@docker ~ 09:41:34]# docker tag httpd:latest localhost:5000/httpd:v1
    [root@docker ~ 09:42:04]# docker images
    REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
    httpd                  latest    6a4fe18d08d2   12 days ago     117MB
    localhost:5000/httpd   v1        6a4fe18d08d2   12 days ago     117MB
    busybox                latest    08ef35a1c3f0   13 months ago   4.43MB
    registry               2         26b2eb03618e   2 years ago     25.4MB

    我们在镜像的前面加上了运行 registry 的主机名称和端口。

    前面已经讨论了镜像名称由 repository 和 tag 两部分组成。而 repository 的完整格式为**:[registry-host]:[port]/[username]/xxx**

    只有 Docker Hub 上的镜像可以省略 [registry-host]:[port]

  3. 通过 docker push 上传镜像。

    bash 复制代码
    [root@docker ~ 09:42:33]# docker push localhost:5000/httpd:v1
    The push refers to repository [localhost:5000/httpd]
    846dc7d28355: Pushed
    28962f8cc60f: Pushed
    13d3747eece7: Pushed
    5f70bf18a086: Pushed
    fecb258af9fe: Pushed
    36d06fe0cbc6: Pushed
    v1: digest: sha256:33a646b7ccd4a553de8a452213c3132392faf8a7e1000951c1a06db4ed615676 size: 1572
    [root@docker ~ 09:42:57]# curl http://localhost:5000/v2/_catalog
    {"repositories":["httpd"]}
    [root@docker ~ 09:43:23]# docker images
    REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
    httpd                  latest    6a4fe18d08d2   12 days ago     117MB
    localhost:5000/httpd   v1        6a4fe18d08d2   12 days ago     117MB
    busybox                latest    08ef35a1c3f0   13 months ago   4.43MB
    registry               2         26b2eb03618e   2 years ago     25.4MB
  4. 现在已经可通过 docker pull 从本地 registry 下载镜像了。

    bash 复制代码
    #下载之前先删除本地的镜像
    [root@docker ~ 09:43:31]# docker rmi httpd:latest
    Untagged: httpd:latest
    Untagged: httpd@sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
    [root@docker ~ 09:43:44]# docker rmi localhost:5000/httpd:v1
    Untagged: localhost:5000/httpd:v1
    Untagged: localhost:5000/httpd@sha256:33a646b7ccd4a553de8a452213c3132392faf8a7e1000951c1a06db4ed615676
    Deleted: sha256:6a4fe18d08d26a61b32d6aa5d17b2417c4f4de63e8460abf4cd824b911b9de88
    Deleted: sha256:c78f79d223643af30c382eefcd1efc285a994a2895a4fad225330a6c3e1d1797
    Deleted: sha256:655ea42223e5a87df151e8f2a0199419176fdf645c5d1504467f12df46f66a09
    Deleted: sha256:c9dccfd4d12e5bac98e0c53df0fb6964e2cc26020cbd4dcf57419961db1dcf6b
    Deleted: sha256:ed6e7e3a72de02ccd775dcd7b09dbc0b8dc41bc1e683027f3b7ab8764977be78
    Deleted: sha256:006a1ccbeba5f22631ee410cb46c1c5293e3804f2e352dd32b99c3df49a26aef
    Deleted: sha256:36d06fe0cbc654e5f67d58c960ed33e53127e4a3288d8ce6f6a60a9c311794d4
    [root@docker ~ 09:44:00]# docker images
    REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
    busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
    registry     2         26b2eb03618e   2 years ago     25.4MB
    
    
    #从自建仓库下载镜像
    [root@docker ~ 09:44:16]# docker pull  localhost:5000/httpd:v1
    v1: Pulling from httpd
    fb60efab8139: Pull complete
    fab98c44430d: Pull complete
    4f4fb700ef54: Pull complete
    13c22d886563: Pull complete
    4870f70a8556: Pull complete
    233dec01418c: Pull complete
    Digest: sha256:33a646b7ccd4a553de8a452213c3132392faf8a7e1000951c1a06db4ed615676
    Status: Downloaded newer image for localhost:5000/httpd:v1
    localhost:5000/httpd:v1
    
    #验证从自建仓库下载的镜像
    [root@docker ~ 09:44:43]# docker images
    REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
    localhost:5000/httpd   v1        6a4fe18d08d2   12 days ago     117MB
    busybox                latest    08ef35a1c3f0   13 months ago   4.43MB
    registry               2         26b2eb03618e   2 years ago     25.4MB

    除了镜像的名称长一些(包含 registry host 和 port),使用方式完全一样。

以上是搭建本地 registry 的简要步骤。当然 registry 也支持认证,https 安全传输等特性,具体可以参考官方文档 https://docs.docker.com/registry/configuration/

企业级私有仓库Harbor

我们如果需要部署一个私有镜像仓库来使用,最简单的就是 registry ,一行命令就可以运行在 Docker 中,但功能也比较弱,如果想要私有镜像仓库功能更丰富些,可以使用 Harbor。

本文简单介绍下 Harbor 的安装和使用。

Harbor是构建企业级私有docker镜像的仓库的开源解决方案,它是Docker Registry的更高级封装,除了提供友好的Web UI界面,角色和用户权限管理,用户操作审计等功能外,它还整合了K8s的插件(Add-ons)仓库。

harbor下载:https://github.com/goharbor/harbor/releases

安装

bash 复制代码
[root@docker ~]#wget https://github.com/goharbor/harbor/releases/download/v2.9.1/harbor-offline-installer-v2.9.1.tgz

如果无法通过 wget 进行下载,可以直接到 Github 网站:https://github.com/goharbor/harbor/releases/ 进行下载,然后拷贝到服务器中

bash 复制代码
[root@docker ~ 10:41:38]# ls
anaconda-ks.cfg
[root@docker ~ 10:41:39]# ls
anaconda-ks.cfg  harbor-offline-installer-v2.9.1.tgz

执行下面命令进行解压

bash 复制代码
[root@docker ~ 10:42:23]# tar -xvf harbor-offline-installer-v2.9.1.tgz
harbor/harbor.v2.9.1.tar.gz
harbor/prepare
harbor/LICENSE
harbor/install.sh
harbor/common.sh
harbor/harbor.yml.tmpl
[root@docker ~ 10:42:43]# ls
anaconda-ks.cfg  harbor  harbor-offline-installer-v2.9.1.tgz

执行下面命令新建目录,并将程序文件复制到目录中:

bash 复制代码
[root@docker ~ 10:42:46]# mkdir /opt/harbor
[root@docker ~ 10:42:57]# mv harbor/* /opt/harbor/

导入Harbor镜像

bash 复制代码
[root@docker ~ 10:43:14]# cd /opt/harbor/
[root@docker harbor 10:43:20]# ls
common.sh  harbor.v2.9.1.tar.gz  harbor.yml.tmpl  install.sh  LICENSE  prepare
[root@docker harbor 10:43:23]# docker load -i harbor.v2.9.1.tar.gz
a1dcbad8836c: Loading layer  40.11MB/40.11MB
4349dad1c75c: Loading layer  10.89MB/10.89MB
... ...
Loaded image: goharbor/prepare:v2.9.1
d0dcb5740755: Loading layer  115.1MB/115.1MB
a68394b34761: Loading layer   6.46MB/6.46MB
e47863752870: Loading layer  245.8kB/245.8kB
eb0d64571e29: Loading layer  1.233MB/1.233MB
Loaded image: goharbor/harbor-portal:v2.9.1

修改 Harbor 配置文件

bash 复制代码
[root@docker harbor 10:44:35]# cp -ar harbor.yml.tmpl harbor.yml
[root@docker harbor 10:45:41]# vim harbor.yml
[root@docker harbor 10:47:29]# cat harbor.yml
# Configuration file of Harbor

# The IP address or hostname to access admin UI and registry service.
# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
`修改主机名`
hostname: 192.168.108.30

# http related config
http:
  # port for http, default is 80. If https enabled, this port will redirect to https port
  port: 80

# https related config
`#https:``
  # https port for harbor, default is 443
  # port: 443
  # The path of cert and key files for nginx
  #certificate: /your/certificate/path
 ` #private_key: /your/private/key/path`
  `之间的都注释掉`

# # Uncomment following will enable tls communication between all harbor components
# internal_tls:
#   # set enabled to true means internal tls is enabled
#   enabled: true
#   # put your cert and key files on dir
#   dir: /etc/harbor/tls/internal
#   # enable strong ssl ciphers (default: false)
#   strong_ssl_ciphers: false

# Uncomment external_url if you want to enable external proxy
# And when it enabled the hostname will no longer used
# external_url: https://reg.mydomain.com:8433

# The initial password of Harbor admin
# It only works in first time to install harbor
# Remember Change the admin password from UI after launching Harbor.
`修改密码`
harbor_admin_password: 123

... ...

**hostname:**如果只是内网访问,设置为内网 IP,如果需要外网访问,就必须设置为外网域名或 IP

https: #注释掉

harbor_admin_password: harbor登录密码

编辑完配置文件,接下来在 harbor 目录下安装 Harbor。先进行预处理更新配置文件

bash 复制代码
[root@docker harbor 10:47:37]# ./prepare
prepare base dir is set to /opt/harbor
WARNING:root:WARNING: HTTP protocol is insecure. Harbor will deprecate http protocol in the future. Please make sure to upgrade to https
Generated configuration file: /config/portal/nginx.conf
Generated configuration file: /config/log/logrotate.conf
Generated configuration file: /config/log/rsyslog_docker.conf
Generated configuration file: /config/nginx/nginx.conf
Generated configuration file: /config/core/env
Generated configuration file: /config/core/app.conf
Generated configuration file: /config/registry/config.yml
Generated configuration file: /config/registryctl/env
Generated configuration file: /config/registryctl/config.yml
Generated configuration file: /config/db/env
Generated configuration file: /config/jobservice/env
Generated configuration file: /config/jobservice/config.yml
Generated and saved secret to file: /data/secret/keys/secretkey
Successfully called func: create_root_cert
Generated configuration file: /compose_location/docker-compose.yml
Clean up the input dir

执行下面命令进行安装

bash 复制代码
[root@docker harbor 10:47:50]# ./install.sh

[Step 0]: checking if docker is installed ...

Note: docker version: 26.1.3

[Step 1]: checking docker-compose is installed ...

Note: Docker Compose version v2.27.0

[Step 2]: loading Harbor images ...
Loaded image: goharbor/harbor-jobservice:v2.9.1
Loaded image: goharbor/harbor-registryctl:v2.9.1
Loaded image: goharbor/harbor-core:v2.9.1
Loaded image: goharbor/harbor-log:v2.9.1
Loaded image: goharbor/harbor-db:v2.9.1
Loaded image: goharbor/harbor-exporter:v2.9.1
Loaded image: goharbor/redis-photon:v2.9.1
Loaded image: goharbor/nginx-photon:v2.9.1
Loaded image: goharbor/registry-photon:v2.9.1
Loaded image: goharbor/trivy-adapter-photon:v2.9.1
Loaded image: goharbor/prepare:v2.9.1
Loaded image: goharbor/harbor-portal:v2.9.1


[Step 3]: preparing environment ...

[Step 4]: preparing harbor configs ...
prepare base dir is set to /opt/harbor
WARNING:root:WARNING: HTTP protocol is insecure. Harbor will deprecate http protocol in the future. Please make sure to upgrade to https
Clearing the configuration file: /config/portal/nginx.conf
Clearing the configuration file: /config/log/logrotate.conf
Clearing the configuration file: /config/log/rsyslog_docker.conf
Clearing the configuration file: /config/nginx/nginx.conf
Clearing the configuration file: /config/core/env
Clearing the configuration file: /config/core/app.conf
Clearing the configuration file: /config/registry/passwd
Clearing the configuration file: /config/registry/config.yml
Clearing the configuration file: /config/registryctl/env
Clearing the configuration file: /config/registryctl/config.yml
Clearing the configuration file: /config/db/env
Clearing the configuration file: /config/jobservice/env
Clearing the configuration file: /config/jobservice/config.yml
Generated configuration file: /config/portal/nginx.conf
Generated configuration file: /config/log/logrotate.conf
Generated configuration file: /config/log/rsyslog_docker.conf
Generated configuration file: /config/nginx/nginx.conf
Generated configuration file: /config/core/env
Generated configuration file: /config/core/app.conf
Generated configuration file: /config/registry/config.yml
Generated configuration file: /config/registryctl/env
Generated configuration file: /config/registryctl/config.yml
Generated configuration file: /config/db/env
Generated configuration file: /config/jobservice/env
Generated configuration file: /config/jobservice/config.yml
loaded secret from file: /data/secret/keys/secretkey
Generated configuration file: /compose_location/docker-compose.yml
Clean up the input dir


Note: stopping existing Harbor instance ...
WARN[0000] /opt/harbor/docker-compose.yml: `version` is obsolete


[Step 5]: starting Harbor ...
WARN[0000] /opt/harbor/docker-compose.yml: `version` is obsolete
[+] Running 10/10
 ✔ Network harbor_harbor        Created                                    0.2s
 ✔ Container harbor-log         Started                                    0.4s
 ✔ Container harbor-db          Started                                    1.3s
 ✔ Container harbor-portal      Started                                    1.3s
 ✔ Container redis              Started                                    1.3s
 ✔ Container registry           Started                                    1.5s
 ✔ Container registryctl        Started                                    1.4s
 ✔ Container harbor-core        Started                                    2.0s
 ✔ Container nginx              Started                                    2.7s
 ✔ Container harbor-jobservice  Started                                    2.6s
✔ ----Harbor has been installed and started successfully.----

稍等一会,如果所有容器的状态都是 healthy ,说明正常

登录WEB界面:http://192.168.108.30

使用

Harbor 里功能比较多,常用的有项目、用户管理、项目定额。

  • 项目:可以针对不同的项目单独创建,每个项目都有自己的镜像地址
  • 用户管理:可以维护用户,不同的项目可以设置不同的维护人员
  • 项目定额:设置项目对应的镜像仓库最大空间容量

下面就按照步骤将一个镜像推送到 Harbor 中。

1、在用户管理中创建名称为 images_admin 的用户

在项目中创建名称为cloud的项目,并添加 images_admin 为项目管理员

将内网服务器 IP 和端口配置到 daemon.json 文件中,执行下面命令进行配置

bash 复制代码
[root@docker harbor 10:53:20]# vim /etc/docker/daemon.json
[root@docker harbor 10:53:52]# cat /etc/docker/daemon.json
{
    "insecure-registries": ["192.168.108.30"],
    "registry-mirrors": [ "https://19adffc09b4f4fcbad0603a171dd0419.mirror.swr.myhuaweicloud.com" ]
}


[root@docker harbor 10:53:38]# systemctl restart docker

登录服务器

bash 复制代码
#登陆失败,需要重新安装
[root@docker harbor 10:54:02]# docker login 192.168.108.30
Username: images_admin
Password:
Error response from daemon: Get "http://192.168.108.30/v2/": dial tcp 192.168.108.30:80: connect: connection refused

#重新执行安装命令
[root@docker harbor 10:54:33]# ./install.sh

[Step 0]: checking if docker is installed ...

Note: docker version: 26.1.3

[Step 1]: checking docker-compose is installed ...

Note: Docker Compose version v2.27.0

[Step 2]: loading Harbor images ...
Loaded image: goharbor/harbor-jobservice:v2.9.1
Loaded image: goharbor/harbor-registryctl:v2.9.1
Loaded image: goharbor/harbor-core:v2.9.1
Loaded image: goharbor/harbor-log:v2.9.1
Loaded image: goharbor/harbor-db:v2.9.1
Loaded image: goharbor/harbor-exporter:v2.9.1
Loaded image: goharbor/redis-photon:v2.9.1
Loaded image: goharbor/nginx-photon:v2.9.1
Loaded image: goharbor/registry-photon:v2.9.1
Loaded image: goharbor/trivy-adapter-photon:v2.9.1
Loaded image: goharbor/prepare:v2.9.1
Loaded image: goharbor/harbor-portal:v2.9.1


[Step 3]: preparing environment ...

[Step 4]: preparing harbor configs ...
prepare base dir is set to /opt/harbor
WARNING:root:WARNING: HTTP protocol is insecure. Harbor will deprecate http protocol in the future. Please make sure to upgrade to https
Clearing the configuration file: /config/portal/nginx.conf
Clearing the configuration file: /config/log/logrotate.conf
Clearing the configuration file: /config/log/rsyslog_docker.conf
Clearing the configuration file: /config/nginx/nginx.conf
Clearing the configuration file: /config/core/env
Clearing the configuration file: /config/core/app.conf
Clearing the configuration file: /config/registry/passwd
Clearing the configuration file: /config/registry/config.yml
Clearing the configuration file: /config/registry/root.crt
Clearing the configuration file: /config/registryctl/env
Clearing the configuration file: /config/registryctl/config.yml
Clearing the configuration file: /config/db/env
Clearing the configuration file: /config/jobservice/env
Clearing the configuration file: /config/jobservice/config.yml
Generated configuration file: /config/portal/nginx.conf
Generated configuration file: /config/log/logrotate.conf
Generated configuration file: /config/log/rsyslog_docker.conf
Generated configuration file: /config/nginx/nginx.conf
Generated configuration file: /config/core/env
Generated configuration file: /config/core/app.conf
Generated configuration file: /config/registry/config.yml
Generated configuration file: /config/registryctl/env
Generated configuration file: /config/registryctl/config.yml
Generated configuration file: /config/db/env
Generated configuration file: /config/jobservice/env
Generated configuration file: /config/jobservice/config.yml
loaded secret from file: /data/secret/keys/secretkey
Generated configuration file: /compose_location/docker-compose.yml
Clean up the input dir


Note: stopping existing Harbor instance ...
WARN[0000] /opt/harbor/docker-compose.yml: `version` is obsolete
[+] Running 10/10
 ✔ Container registryctl        Removed                                    0.0s
 ✔ Container harbor-jobservice  Removed                                    0.0s
 ✔ Container nginx              Removed                                    0.0s
 ✔ Container harbor-core        Removed                                    0.0s
 ✔ Container harbor-portal      Removed                                    0.0s
 ✔ Container harbor-db          Removed                                    0.1s
 ✔ Container registry           Removed                                    0.0s
 ✔ Container redis              Removed                                    0.0s
 ✔ Container harbor-log         Removed                                   10.1s
 ✔ Network harbor_harbor        Removed                                    0.1s


[Step 5]: starting Harbor ...
WARN[0000] /opt/harbor/docker-compose.yml: `version` is obsolete
[+] Running 10/10
 ✔ Network harbor_harbor        Created                                    0.2s
 ✔ Container harbor-log         Started                                    0.4s
 ✔ Container registryctl        Started                                    1.9s
 ✔ Container harbor-db          Started                                    1.7s
 ✔ Container registry           Started                                    1.6s
 ✔ Container redis              Started                                    1.8s
 ✔ Container harbor-portal      Started                                    1.7s
 ✔ Container harbor-core        Started                                    2.1s
 ✔ Container harbor-jobservice  Started                                    2.7s
 ✔ Container nginx              Started                                    2.8s
✔ ----Harbor has been installed and started successfully.----

#登陆成功
[root@docker harbor 10:55:20]# docker login 192.168.108.30
Username: admin
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

登录后家目录下会有一个.docker文件夹

bash 复制代码
[root@docker harbor 10:55:54]# cd
[root@docker ~ 10:55:57]# ls -a
.                .bash_logout   .docker                              .tcshrc
..               .bash_profile  harbor                               .viminfo
anaconda-ks.cfg  .bashrc        harbor-offline-installer-v2.9.1.tgz
.bash_history    .cshrc         .lesshst
[root@docker ~ 10:56:00]# cd ./.docker/
[root@docker .docker 10:56:17]# pwd
/root/.docker
[root@docker .docker 10:56:19]# cat config.json
{
        "auths": {
                "192.168.108.30": {
                        "auth": "YWRtaW46MTIz"
                }
        }
}

上传镜像

bash 复制代码
[root@docker .docker 10:58:07]# docker tag nginx:latest 192.168.108.30/cloud/nginx:latest

[root@docker .docker 10:59:51]# docker images
REPOSITORY                      TAG       IMAGE ID       CREATED         SIZE
localhost:5000/httpd            v1        6a4fe18d08d2   12 days ago     117MB
nginx                           latest    d261fd19cb63   12 days ago     152MB
192.168.108.30/cloud/nginx      latest    d261fd19cb63   12 days ago     152MB
busybox                         latest    08ef35a1c3f0   13 months ago   4.43MB
goharbor/harbor-exporter        v2.9.1    37bfd4fa26bc   2 years ago     105MB
goharbor/redis-photon           v2.9.1    67827413c0fd   2 years ago     209MB
goharbor/trivy-adapter-photon   v2.9.1    a02695b8f8ea   2 years ago     469MB
goharbor/harbor-registryctl     v2.9.1    a076218bb631   2 years ago     148MB
goharbor/registry-photon        v2.9.1    2f01ea8b1853   2 years ago     82.7MB
goharbor/nginx-photon           v2.9.1    5200203dd7ef   2 years ago     153MB
goharbor/harbor-log             v2.9.1    ac1cdcc94a5f   2 years ago     162MB
goharbor/harbor-jobservice      v2.9.1    d9ff6fc98cc8   2 years ago     139MB
goharbor/harbor-core            v2.9.1    0a3a7953409c   2 years ago     166MB
goharbor/harbor-portal          v2.9.1    345284db8ca1   2 years ago     161MB
goharbor/harbor-db              v2.9.1    69606d285be1   2 years ago     358MB
goharbor/prepare                v2.9.1    adb2d804c458   2 years ago     253MB
registry                        2         26b2eb03618e   2 years ago     25.4MB

[root@docker .docker 10:59:56]# docker push 192.168.108.30/cloud/nginx:latest
The push refers to repository [192.168.108.30/cloud/nginx]
d7217c60dca4: Pushed
d81df94f8d07: Pushed
99cd1b1b6a43: Pushed
2ced4cd78a7b: Pushed
8feb164cd673: Pushed
6e19587ac541: Pushed
36d06fe0cbc6: Pushed
latest: digest: sha256:707d6194e372337c708ca4a8da8bd87085b5626f60a3e2c52bc164e64ed8682b size: 1778

镜像格式:SERVER/PROJECT /PATH/TO/IMAGE/IMAGE:TAG

下载镜像

bash 复制代码
[root@docker .docker 10:56:33]# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
d7ecded7702a: Already exists
266626526d42: Pull complete
320b0949be89: Pull complete
d921c57c6a81: Pull complete
9def903993e4: Pull complete
52bc359bcbd7: Pull complete
e2f8e296d9df: Pull complete
Digest: sha256:1beed3ca46acebe9d3fb62e9067f03d05d5bfa97a00f30938a0a3580563272ad
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest

卸载harbor

清理容器

bash 复制代码
[root@docker .docker 11:00:24]# cd /opt/harbor/
[root@docker harbor 11:01:10]# docker compose down
WARN[0000] /opt/harbor/docker-compose.yml: `version` is obsolete
[+] Running 10/10
 ✔ Container harbor-jobservice  Removed                                    0.1s
 ✔ Container registryctl        Removed                                    0.1s
 ✔ Container nginx              Removed                                    0.2s
 ✔ Container harbor-portal      Removed                                    0.1s
 ✔ Container harbor-core        Removed                                    0.1s
 ✔ Container redis              Removed                                    0.2s
 ✔ Container harbor-db          Removed                                    0.1s
 ✔ Container registry           Removed                                    0.1s
 ✔ Container harbor-log         Removed                                   10.1s
 ✔ Network harbor_harbor        Removed                                    0.2s

清理镜像

bash 复制代码
[root@docker harbor 11:01:25]# docker images |grep harbor|awk '{print $1":"$2}' | xargs docker rmi
Untagged: goharbor/harbor-exporter:v2.9.1
Deleted: sha256:37bfd4fa26bc718e3342a9a33c9455e7d3b75a0c23bf459f5c5143531c0f698b
... ...
Deleted: sha256:a1dcbad8836c54246278629e71fd3849e01438a7fe27ac63d5c7af9855b027ef

清理harbor使用的目录/data,由prepare脚本定义

bash 复制代码
[root@docker harbor 11:04:46]# rm -rf /data

删除软件包

bash 复制代码
[root@docker harbor 11:04:49]# cd
[root@docker ~ 11:04:53]# ls
anaconda-ks.cfg  harbor  harbor-offline-installer-v2.9.1.tgz
[root@docker ~ 11:04:53]# rm -f harbor-offline-installer-v2.9.1.tgz
[root@docker ~ 11:04:58]# rm -f harbor*
[root@docker ~ 11:05:02]# rm -rf /opt/harbor/
[root@docker ~ 11:05:07]# ls
anaconda-ks.cfg 

至此,Docker 镜像的内容就讨论完了,下节我们对这部分做个小结。

021 Docker镜像小结

本节我们对 Docker 镜像做个小结。

这一部分我们首先讨论了镜像的分层结构,然后学习了如何构建镜像,最后实践使用 Docker Hub 和本地 registry。

下面是镜像的常用操作子命令:

images 显示镜像列表

bash 复制代码
[root@docker ~ 17:19:25]# docker images
REPOSITORY           TAG       IMAGE ID       CREATED         SIZE
ubuntu               latest    c3a134f2ace4   4 weeks ago     78.1MB
busybox              latest    08ef35a1c3f0   13 months ago   4.43MB

history 显示镜像构建历史

bash 复制代码
[root@docker ~ 17:19:38]# docker history ubuntu
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
c3a134f2ace4   4 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      4 weeks ago   /bin/sh -c #(nop) ADD file:ddf1aa62235de6657...   78.1MB
<missing>      4 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers....   0B
<missing>      4 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers....   0B
<missing>      4 weeks ago   /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
<missing>      4 weeks ago   /bin/sh -c #(nop)  ARG RELEASE                  0B

commit 从容器创建新镜像

bash 复制代码
[root@docker ~ 17:23:34]# docker commit 7150 bbox:v1
sha256:7ec9f82dc373cc81c14c2c414f5b8f2906e628bb4fc122224c02a16171aa479b
[root@docker ~ 17:24:18]# docker images
REPOSITORY           TAG       IMAGE ID       CREATED         SIZE
bbox                 v1        7ec9f82dc373   9 seconds ago   4.43MB

build 从 Dockerfile 构建镜像

bash 复制代码
[root@docker ~ 10:38:56]# docker build -t ubuntu-with-vim-dockerfile .

tag 给镜像打 tag

bash 复制代码
[root@docker ~ 17:26:39]# docker tag bbox:v1 bbox:v2
[root@docker ~ 17:26:55]# docker images
REPOSITORY           TAG       IMAGE ID       CREATED         SIZE
bbox                 v1        7ec9f82dc373   2 minutes ago   4.43MB
bbox                 v2        7ec9f82dc373   2 minutes ago   4.43MB

pull 从 registry 下载镜像

push 将 镜像 上传到 registry

rmi 删除 Docker host 中的镜像

search 搜索 Docker Hub 中的镜像

除了 rmi 和 search,其他命令都已经用过了。

rmi

rmi 只能删除 host 上的镜像,不会删除 registry 的镜像。

如果一个镜像对应了多个 tag,只有当最后一个 tag 被删除时,镜像才被真正删除。例如 host 中 busybox镜像有三个 tag:

bash 复制代码
[root@docker ~ 11:39:05]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
[root@docker ~ 11:39:19]# docker tag busybox:latest busybox:v1
[root@docker ~ 11:39:40]# docker tag busybox:latest busybox:v2
[root@docker ~ 11:39:44]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
busybox      v1        08ef35a1c3f0   13 months ago   4.43MB
busybox      v2        08ef35a1c3f0   13 months ago   4.43MB

删除其中 busybox:latest 只是删除了 latest tag,镜像本身没有删除。

只有当 busybox:v1busybox:v2也被删除时,整个镜像才会被删除。

bash 复制代码
[root@docker ~ 11:39:47]# docker rmi busybox:latest
Untagged: busybox:latest
[root@docker ~ 11:40:07]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
busybox      v1        08ef35a1c3f0   13 months ago   4.43MB
busybox      v2        08ef35a1c3f0   13 months ago   4.43MB
[root@docker ~ 11:40:14]# docker rmi busybox:v1
Untagged: busybox:v1
[root@docker ~ 11:40:31]# docker rmi busybox:v2
Untagged: busybox:v2
Untagged: busybox@sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Deleted: sha256:08ef35a1c3f050afbbd64194ffd1b8d5878659f5491567f26d1c814513ae9649
Deleted: sha256:e14542cc062958c3bfada9c260a6ae47bb2906fd8b514999774760710dbce3cb

search

search 让我们无需打开浏览器,在命令行中就可以搜索 Docker Hub 中的镜像。

当然,如果想知道镜像都有哪些 tag,还是得访问 Docker Hub。

保存本地镜像为文件-save

docker默认使用overlay2存储驱动存储镜像。

bash 复制代码
[root@docker ~ 11:40:33]# docker info
Client: Docker Engine - Community
 Version:    26.1.3
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.14.0
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v2.27.0
    Path:     /usr/libexec/docker/cli-plugins/docker-compose

Server:
 Containers: 1
  Running: 0
  Paused: 0
  Stopped: 1
 Images: 0
 Server Version: 26.1.3
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: cgroupfs
 Cgroup Version: 1
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 8b3b7ca2e5ce38e8f31a34f35b2b68ceb8470d89
 runc version: v1.1.12-0-g51d5e94
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
 Kernel Version: 4.18.0-553.6.1.el8.x86_64
 Operating System: CentOS Stream 8
 OSType: linux
 Architecture: x86_64
 CPUs: 4
 Total Memory: 7.486GiB
 Name: docker
 ID: 425d4420-fa3e-4fcb-8bb1-4d92f2970921
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  192.168.108.30
  127.0.0.0/8
 Registry Mirrors:
  https://19adffc09b4f4fcbad0603a171dd0419.mirror.swr.myhuaweicloud.com/
 Live Restore Enabled: false

[root@docker ~ 11:40:43]# docker info |grep 'Storage Driver'
 Storage Driver: overlay2

镜像存储在本地/var/lib/docker/overlay2,通过文件系统层面拷贝image,操作复杂。可以使用save命令,将本地镜像保存为单个文件,并分享给他人使用。

bash 复制代码
[root@docker ~ 11:41:06]# docker save --help

Usage:  docker save [OPTIONS] IMAGE [IMAGE...]

Save one or more images to a tar archive (streamed to STDOUT by default)

Aliases:
  docker image save, docker save

Options:
  -o, --output string   Write to a file, instead of STDOUT

示例:

准备镜像

bash 复制代码
[root@docker ~ 11:41:26]# docker pull httpd
Using default tag: latest
latest: Pulling from library/httpd
d7ecded7702a: Pull complete
fab98c44430d: Pull complete
4f4fb700ef54: Pull complete
13c22d886563: Pull complete
4870f70a8556: Pull complete
233dec01418c: Pull complete
Digest: sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
Status: Downloaded newer image for httpd:latest
docker.io/library/httpd:latest
[root@docker ~ 11:42:04]# docker pull hello-world
Using default tag: latest
Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
[root@docker ~ 11:42:48]# docker pull busybox
Using default tag: latest
latest: Pulling from library/busybox
e59838ecfec5: Pull complete
Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Status: Downloaded newer image for busybox:latest
docker.io/library/busybox:latest
[root@docker ~ 11:43:16]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
httpd        latest    6a4fe18d08d2   13 days ago     117MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
bash 复制代码
[root@docker ~ 11:43:21]# docker save httpd busybox -o images.tar
[root@docker ~ 11:43:40]# ls
anaconda-ks.cfg  harbor  images.tar
将本地镜像文件导入本地-load
bash 复制代码
[root@docker ~ 11:43:41]# docker load --help

Usage:  docker load [OPTIONS]

Load an image from a tar archive or STDIN

Aliases:
  docker image load, docker load

Options:
  -i, --input string   Read from tar archive file, instead of STDIN
  -q, --quiet          Suppress the load output

示例:删除本地镜像,并导入本地镜像文件

bash 复制代码
[root@docker ~ 11:43:51]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
httpd        latest    6a4fe18d08d2   13 days ago     117MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
[root@docker ~ 11:44:02]# docker rmi -f $(docker images -aq)
Untagged: httpd:latest
Untagged: httpd@sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
Deleted: sha256:6a4fe18d08d26a61b32d6aa5d17b2417c4f4de63e8460abf4cd824b911b9de88
Deleted: sha256:c78f79d223643af30c382eefcd1efc285a994a2895a4fad225330a6c3e1d1797
Deleted: sha256:655ea42223e5a87df151e8f2a0199419176fdf645c5d1504467f12df46f66a09
Deleted: sha256:c9dccfd4d12e5bac98e0c53df0fb6964e2cc26020cbd4dcf57419961db1dcf6b
Deleted: sha256:ed6e7e3a72de02ccd775dcd7b09dbc0b8dc41bc1e683027f3b7ab8764977be78
Deleted: sha256:006a1ccbeba5f22631ee410cb46c1c5293e3804f2e352dd32b99c3df49a26aef
Deleted: sha256:36d06fe0cbc654e5f67d58c960ed33e53127e4a3288d8ce6f6a60a9c311794d4
Untagged: busybox:latest
Untagged: busybox@sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Deleted: sha256:08ef35a1c3f050afbbd64194ffd1b8d5878659f5491567f26d1c814513ae9649
Deleted: sha256:e14542cc062958c3bfada9c260a6ae47bb2906fd8b514999774760710dbce3cb
[root@docker ~ 11:44:14]# docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

删除本地正在使用的镜像需要选项-f

导入镜像

bash 复制代码
[root@docker ~ 11:44:17]# docker load -i images.tar
36d06fe0cbc6: Loading layer  81.04MB/81.04MB
fecb258af9fe: Loading layer   2.56kB/2.56kB
5f70bf18a086: Loading layer  1.024kB/1.024kB
13d3747eece7: Loading layer  6.018MB/6.018MB
28962f8cc60f: Loading layer  33.08MB/33.08MB
846dc7d28355: Loading layer  3.584kB/3.584kB
Loaded image: httpd:latest
e14542cc0629: Loading layer  4.669MB/4.669MB
Loaded image: busybox:latest
[root@docker ~ 11:44:32]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
httpd        latest    6a4fe18d08d2   13 days ago     117MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB

说明:如果本地镜像名与导入的镜像重名,则本地的镜像会被覆盖。

至此,Docker 镜像已经讨论完了,下节我们深入讨论容器。

第4章 容器

022 如何运行容器?

上一章我们学习了如何构建 Docker 镜像,并通过镜像运行容器。本章将深入讨论容器:学习容器的各种操作,容器各种状态之间如何转换,以及实现容器的底层技术。

运行容器

docker run=docker create + docker start

docker run 是启动容器的方法。在讨论 Dockerfile 时我们已经学习到,可用三种方式指定容器启动时执行的命令:

  1. CMD 指令。
  2. ENTRYPOINT 指令。
  3. docker run 命令行中指定。

例如下面的例子:

bash 复制代码
#使用ubuntu镜像创建容器
[root@docker ~ 14:04:11]# docker create ubuntu
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
20043066d3d5: Pull complete
Digest: sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54
Status: Downloaded newer image for ubuntu:latest
#新创建的容器长ID
60f640acdd3e135518b6c97c9ae7f41b80e50ff33d6acb464d33de0ce36284ef

#create的容器状态时Created
[root@docker ~ 14:05:24]# docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS    PORTS     NAMES
60f640acdd3e   ubuntu    "/bin/bash"   15 seconds ago   `Created`             boring_khayyam

#启动容器,刚创建的容器ID
[root@docker ~ 14:05:39]# docker start 60
60

#查看容器状态,启动了又退出了
[root@docker ~ 14:05:55]# docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS                     PORTS     NAMES
60f640acdd3e   ubuntu    "/bin/bash"   37 seconds ago   Exited (0) 6 seconds ago             boring_khayyam
bash 复制代码
[root@docker ~ 14:06:01]# docker run ubuntu pwd
/

容器启动时执行 pwd,返回的 / 是容器中的当前目录。 执行 docker psdocker container ls 可以查看 Docker host 中当前运行的容器:

bash 复制代码
 #查看所有正在运行中的容器
[root@docker ~ 14:06:17]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

咦,怎么没有容器?用 docker ps -adocker container ls -a 看看。

bash 复制代码
[root@docker ~ 14:06:22]# docker ps -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS                      PORTS     NAMES
e9bdfe85f9ad   ubuntu    "pwd"         9 seconds ago        Exited (0) 8 seconds ago              happy_jang
60f640acdd3e   ubuntu    "/bin/bash"   About a minute ago   Exited (0) 29 seconds ago             boring_khayyam
[root@docker ~ 14:06:25]# docker container ls
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[root@docker ~ 14:06:36]# docker container ls -a
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS                      PORTS     NAMES
e9bdfe85f9ad   ubuntu    "pwd"         32 seconds ago       Exited (0) 31 seconds ago             happy_jang
60f640acdd3e   ubuntu    "/bin/bash"   About a minute ago   Exited (0) 52 seconds ago             boring_khayyam

-a 会显示所有状态的容器,可以看到,之前的容器已经退出了,状态为Exited

这种"一闪而过"的容器通常不是我们想要的结果,我们希望容器能够保持 runing 状态,这样才能被我们使用。

让容器长期运行

如何让容器保存运行呢?

因为容器的生命周期依赖于启动时执行的命令,只要该命令不结束,容器也就不会退出。

理解了这个原理,我们就可以通过执行一个长期运行的命令来保持容器的运行状态。例如执行下面的命令:

bash 复制代码
[root@docker ~ 14:06:48]# docker run ubuntu /bin/bash -c "while true ; do sleep 1 ;echo haha; done"
haha
haha
haha
haha
haha
haha
haha
haha

while 语句让 bash 不会退出。我们可以打开另一个终端查看容器的状态

可见容器仍处于运行状态。不过这种方法有个缺点:它占用了一个终端。

我们可以加上参数 -d 以后台方式启动容器。

bash 复制代码
[root@docker ~ 14:08:54]# docker run -d ubuntu /bin/bash -c "while true ; do sleep 1 ; echo hahaha; done"
6af16db66ed1025336fd89d08e46b8b75b7ee6f8380529ae2089f70c463d1b96

容器启动后回到了 docker host 的终端。这里看到 docker 返回了一串字符,这是容器的 ID。通过 docker ps 查看容器

现在我们有了两个正在运行的容器。这里注意一下容器的 CONTAINER IDNAMES 这两个字段。

CONTAINER ID 是容器的 "短ID",前面启动容器时返回的是 "长ID"。短ID是长ID的前12个字符。

NAMES 字段显示容器的名字,在启动容器时可以通过 --name 参数显示地为容器命名,如果不指定,docker 会自动为容器分配名字。

对于容器的后续操作,我们需要通过 "长ID"、"短ID" 或者 "名称" 来指定要操作的容器。比如下面停止一个容器:

bash 复制代码
[root@docker ~ 14:09:11]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS                     PORTS     NAMES
6af16db66ed1   ubuntu    "/bin/bash -c 'while..."   8 seconds ago        Up 8 seconds                         quirky_kowalevski
931d5a19c09e   ubuntu    "/bin/bash -c 'while..."   About a minute ago   Up About a minute                    blissful_saha
e9bdfe85f9ad   ubuntu    "pwd"                    3 minutes ago        Exited (0) 3 minutes ago             happy_jang
60f640acdd3e   ubuntu    "/bin/bash"              3 minutes ago        Exited (0) 3 minutes ago             boring_khayyam


[root@docker ~ 14:09:35]# docker stop 931
931
[root@docker ~ 14:09:50]# docker stop 6a
6a

这一次我们用 --name 指定了容器的名字。 我们还看到容器运行的命令是httpd-foreground,通过 docker history 可知这个命令是通过 CMD 指定的。

我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。下一节学习如何进入容器内部。

023 两种进入容器的方法

我们经常需要进到容器里去做一些工作,比如查看日志、调试、启动其他进程等。有两种方法进入容器:attach 和 exec。

docker attach

通过 docker attach 可以 attach 到容器启动命令的终端,例如:

bash 复制代码
[root@docker ~ 14:43:49]# docker run -d ubuntu /bin/bash -c "while true; do sleep 1 ; echo I_am_in_container ; done"
740b6b10b574c87d43f99d08799f732c6fc39cca6f7defc893dfd8c8c87e4b17
[root@docker ~ 14:43:53]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
740b6b10b574   ubuntu    "/bin/bash -c 'while..."   6 seconds ago   Up 5 seconds             cranky_knuth

[root@docker ~ 14:44:13]# docker attach 74
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container

这次我们通过 "长ID" attach 到了容器的启动命令终端,之后看到的是echo 每隔一秒打印的信息。

注:可通过 Ctrl+p 然后 Ctrl+q 组合键退出 attach 终端。

docker exec

通过 docker exec 进入相同的容器:

bash 复制代码
[root@docker ~ 14:45:37]# docker exec -it 0b  bash
root@0b3c92ec5772:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
root@0b3c92ec5772:/# pwd
/
root@0b3c92ec5772:/# exit
exit

说明如下:

-it 以交互模式打开 pseudo-TTY,执行 bash,其结果就是打开了一个 bash 终端。

② 进入到容器中,容器的 hostname 就是其 "短ID"。

③ 可以像在普通 Linux 中一样执行命令。ps -elf 显示了容器启动进程while 以及当前的 bash 进程。

④ 执行 exit 退出容器,回到 docker host。

docker exec -it <container> bash|sh 是执行 exec 最常用的方式。

attach VS exec

attach 与 exec 主要区别如下:

  1. attach 直接进入容器 启动命令 的终端,不会启动新的进程。
  2. exec 则是在容器中打开新的终端,并且可以启动新的进程。
  3. 如果想直接在终端中查看启动命令的输出,用 attach;其他情况使用 exec。

当然,如果只是为了查看启动命令的输出,可以使用 docker logs 命令:

bash 复制代码
[root@docker ~ 14:46:12]# docker logs -f 0b
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container
I_am_in_container

-f 的作用与 tail -f 类似,能够持续打印输出。

下一节聊聊运行容器的最佳实践。

024 运行容器的最佳实践

按用途容器大致可分为两类:服务类容器和工具类的容器。

  1. 服务类容器以 daemon 的形式运行,对外提供服务。比如 web server,数据库等。通过 -d 以后台方式启动这类容器是非常合适的。如果要排查问题,可以通过 exec -it 进入容器。
  2. 工具类容器通常给能我们提供一个临时的工作环境,通常以 run -it 方式运行,比如:
bash 复制代码
[root@docker ~ 14:46:31]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                        PORTS     NAMES
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   14 minutes ago   Up 14 minutes                           eloquent_pascal
740b6b10b574   ubuntu    "/bin/bash -c 'while..."   15 minutes ago   Exited (137) 14 minutes ago             cranky_knuth


[root@docker ~ 14:59:32]# docker run -it busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
e59838ecfec5: Pull complete
Digest: sha256:e3652a00a2fabd16ce889f0aa32c38eec347b997e73bd09e69c962ec7f8732ee
Status: Downloaded newer image for busybox:latest
/ # ls
bin    etc    lib    proc   sys    usr
dev    home   lib64  root   tmp    var
/ # pwd
/
/ # wget www.baidu.com
Connecting to www.baidu.com (180.101.49.44:80)
saving to 'index.html'
index.html           100% |********************************|  2381  0:00:00 ETA
'index.html' saved
/ # ls
bin         home        lib64       sys         var
dev         index.html  proc        tmp
etc         lib         root        usr
/ # exit

运行 busybox,run -it 的作用是在容器启动后就直接进入。我们这里通过 wget 验证了在容器中访问 internet 的能力。执行 exit 退出终端,同时容器停止。

工具类容器多使用基础镜像,例如 busybox、debian、ubuntu 等。

  1. 前端
bash 复制代码
[root@docker ~ 15:00:17]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                        PORTS     NAMES
56f8a5746689   busybox   "sh"                     29 seconds ago   Exited (0) 5 seconds ago                agitated_snyder
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   14 minutes ago   Up 14 minutes                           eloquent_pascal
740b6b10b574   ubuntu    "/bin/bash -c 'while..."   16 minutes ago   Exited (137) 15 minutes ago             cranky_knuth
[root@docker ~ 15:00:22]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
ubuntu       latest    c3a134f2ace4   4 weeks ago     78.1MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
[root@docker ~ 15:00:37]# docker run  httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
d7ecded7702a: Pull complete
fab98c44430d: Pull complete
4f4fb700ef54: Pull complete
13c22d886563: Pull complete
4870f70a8556: Pull complete
233dec01418c: Pull complete
Digest: sha256:ecfd5ca1bfe1fc5e44a5836c5188bde7f397b50c7a5bb603a017543e29948a01
Status: Downloaded newer image for httpd:latest
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.3. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.3. Set the 'ServerName' directive globally to suppress this message
[Mon Nov 17 07:01:28.188597 2025] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.65 (Unix) configured -- resuming normal operations
[Mon Nov 17 07:01:28.188754 2025] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
^C[Mon Nov 17 07:01:42.310094 2025] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down
[root@docker ~ 15:01:42]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
httpd        latest    6a4fe18d08d2   13 days ago     117MB
ubuntu       latest    c3a134f2ace4   4 weeks ago     78.1MB
busybox      latest    08ef35a1c3f0   13 months ago   4.43MB
[root@docker ~ 15:01:45]# docker run -d httpd
aea93624cb8b9459d532d69e780babb4257a97163dee53acfba5ece8c4de72ca
[root@docker ~ 15:01:56]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"       7 seconds ago    Up 7 seconds    80/tcp    zealous_villani
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   16 minutes ago   Up 16 minutes             eloquent_pascal

容器运行小结

容器运行相关的知识点:

  1. 当 CMD 或 Entrypoint 或 docker run 命令行指定的命令运行结束时,容器停止。
  2. 通过 -d 参数在后台启动容器。
  3. 通过 exec -it 可进入容器并执行命令。

指定容器的三种方法:

  1. 短ID。
  2. 长ID。
  3. 容器名称。 可通过 --name 为容器命名。重命名容器可执行docker rename

容器按用途可分为两类:

  1. 服务类的容器。
  2. 工具类的容器。

下一节讨论容器的其他操作,比如 stop, restart, pause, delete。

025 容器常用操作

前面讨论了如何运行容器,本节学习容器的其他常用操作。

stop/start/restart 容器

通过 docker stop 可以停止运行的容器。

bash 复制代码
[root@docker ~ 15:21:27]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"       19 minutes ago   Up 19 minutes   80/tcp    zealous_villani
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   36 minutes ago   Up 36 minutes             eloquent_pascal
[root@docker ~ 15:21:31]# docker stop ae
ae
[root@docker ~ 15:21:53]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS     NAMES
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   36 minutes ago   Up 36 minutes             eloquent_pascal

容器在 docker host 中实际上是一个进程,docker stop 命令本质上是向该进程发送一个 SIGTERM 信号。如果想快速停止容器,可使用 docker kill 命令,其作用是向容器进程发送 SIGKILL 信号。

bash 复制代码
[root@docker ~ 15:21:55]# docker kill 0b
0b
[root@docker ~ 15:22:10]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
[root@docker ~ 15:22:12]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                        PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"       20 minutes ago   Exited (0) 29 seconds ago               zealous_villani
ec22c86ade62   httpd     "httpd-foreground"       20 minutes ago   Exited (0) 20 minutes ago               heuristic_euclid
56f8a5746689   busybox   "sh"                     22 minutes ago   Exited (0) 22 minutes ago               agitated_snyder
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   36 minutes ago   Exited (137) 12 seconds ago             eloquent_pascal
740b6b10b574   ubuntu    "/bin/bash -c 'while..."   38 minutes ago   Exited (137) 37 minutes ago             cranky_knuth

对于处于停止状态的容器,可以通过 docker start 重新启动。

bash 复制代码
[root@docker ~ 15:22:23]# docker restart ae
ae
[root@docker ~ 15:22:41]# docker ps -a
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS                        PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"       20 minutes ago   Up 2 seconds                  80/tcp    zealous_villani
ec22c86ade62   httpd     "httpd-foreground"       21 minutes ago   Exited (0) 21 minutes ago               heuristic_euclid
56f8a5746689   busybox   "sh"                     22 minutes ago   Exited (0) 22 minutes ago               agitated_snyder
0b3c92ec5772   ubuntu    "/bin/bash -c 'while..."   37 minutes ago   Exited (137) 32 seconds ago             eloquent_pascal
740b6b10b574   ubuntu    "/bin/bash -c 'while..."   38 minutes ago   Exited (137) 37 minutes ago             cranky_knuth

docker start 会保留容器的第一次启动时的所有参数。

docker restart 可以重启容器,其作用就是依次执行 docker stopdocker start

容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。启动容器时设置 --restart 就可以达到这个效果。

如果docker run -d httpd不加--restart=always参数,attach进去ctrl_c(终止进程)不会重启。

bash 复制代码
[root@docker ~ 15:22:43]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS         PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"   20 minutes ago   Up 5 seconds   80/tcp    zealous_villani
[root@docker ~ 15:22:46]# docker run -d --restart=always httpd
a0ad6958b83c9d93aa5be2427be939d5bc0bb9def72d21265f487ac36024f14f
[root@docker ~ 15:23:10]# docker run httpd
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.4. Set the 'ServerName' directive globally to suppress this message
[Mon Nov 17 07:23:24.172355 2025] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.65 (Unix) configured -- resuming normal operations
[Mon Nov 17 07:23:24.172462 2025] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
^C[Mon Nov 17 07:23:26.150375 2025] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down

--restart=always 意味着无论容器因何种原因退出(包括正常退出),就立即重启。该参数的形式还可以是 --restart=on-failure:3,意思是如果启动进程退出代码非0,则重启容器,最多重启3次。

pause/unpause 容器

有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者 dcoker host 需要使用 CPU,这时可以执行 docker pause

bash 复制代码
[root@docker ~ 15:24:20]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED              STATUS              PORTS     NAMES
a0ad6958b83c   httpd     "httpd-foreground"   About a minute ago   Up About a minute   80/tcp    musing_tu
aea93624cb8b   httpd     "httpd-foreground"   22 minutes ago       Up About a minute   80/tcp    zealous_villani
[root@docker ~ 15:24:22]# docker pause a0
a0
[root@docker ~ 15:24:36]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED              STATUS                       PORTS     NAMES
a0ad6958b83c   httpd     "httpd-foreground"   About a minute ago   Up About a minute (Paused)   80/tcp    musing_tu
aea93624cb8b   httpd     "httpd-foreground"   22 minutes ago       Up About a minute            80/tcp    zealous_villani

处于暂停状态的容器不会占用 CPU 资源,直到通过 docker unpause 恢复运行。

bash 复制代码
[root@docker ~ 15:26:00]# docker unpause a0
a0
[root@docker ~ 15:26:14]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS         PORTS     NAMES
a0ad6958b83c   httpd     "httpd-foreground"   3 minutes ago    Up 3 minutes   80/tcp    musing_tu
aea93624cb8b   httpd     "httpd-foreground"   24 minutes ago   Up 3 minutes   80/tcp    zealous_villani
[root@docker ~ 15:26:20]# docker rm a0
Error response from daemon: cannot remove container "/musing_tu": container is running: stop the container before removing or force remove

删除容器

使用 docker 一段时间后,host 上可能会有大量已经退出了的容器。

bash 复制代码
[root@docker ~ 15:25:27]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS                  PORTS     NAMES
a0ad6958b83c   httpd     "httpd-foreground"   2 minutes ago    Up 2 minutes (Paused)   80/tcp    musing_tu
aea93624cb8b   httpd     "httpd-foreground"   23 minutes ago   Up 2 minutes            80/tcp    zealous_villani

这些容器依然会占用 host 的文件系统资源,如果确认不会再重启此类容器,可以通过 docker rm 删除。

bash 复制代码
[root@docker ~ 15:26:23]# docker stop a0
a0
[root@docker ~ 15:26:44]# docker rm a0
a0
[root@docker ~ 15:26:47]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS         PORTS     NAMES
aea93624cb8b   httpd     "httpd-foreground"   24 minutes ago   Up 4 minutes   80/tcp    zealous_villani

docker rm 一次可以指定多个容器,如果希望批量删除所有已经退出的容器,可以执行如下命令:

bash 复制代码
# !!!慎用,删除所有状态容器
[root@docker ~]# docker rm -f $(docker ps -aq)

顺便说一句:docker rm 是删除容器,而 docker rmi 是删除镜像。

一下学了这么多操作,很有必要做个总结。下一节我们会用一张图来描述容器的状态机。

026 一张图搞懂容器所有操作

前面我们已经讨论了容器的各种操作,对容器的生命周期有了大致的理解,下面这张状态机很好地总结了容器各种状态之间是如何转换的。

如果掌握了前面的知识,要看懂这张图应该不难。不过有两点还是需要补充一下:

  1. 可以先创建容器,稍后再启动。

    bash 复制代码
    [root@docker ~ 15:54:31]# docker ps -a
    CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
    [root@docker ~ 15:54:32]# docker create httpd
    a37a437914b4349afaa54c0c81e425d77ab72a9f173d7672a3a4e198b64f4030
    [root@docker ~ 15:54:39]# docker ps
    CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
    [root@docker ~ 15:54:47]# docker ps -a
    CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS    PORTS     NAMES
    a37a437914b4   httpd     "httpd-foreground"   11 seconds ago   Created             loving_brahmagupta
    [root@docker ~ 15:54:50]# docker start a3
    a3
    [root@docker ~ 15:55:06]# docker ps
    CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS         PORTS     NAMES
    a37a437914b4   httpd     "httpd-foreground"   30 seconds ago   Up 2 seconds   80/tcp    loving_brahmagupta

    docker create 创建的容器处于 Created 状态。

    docker start 将以后台方式启动容器。 docker run 命令实际上是 docker createdocker start 的组合。

  2. 只有当容器的启动进程 退出 时,--restart 才生效。

bash 复制代码
#运行容器,查看运行状态,然后停止,查看依旧 运行
[root@docker ~ 15:55:09]# docker run -d --restart=always --name httpd11 httpd
994f0f87932b636c101d30572b184c031fc8e81c6aa4d22158cb7518e90cda44
[root@docker ~ 15:55:45]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED              STATUS          PORTS     NAMES
994f0f87932b   httpd     "httpd-foreground"   7 seconds ago        Up 6 seconds    80/tcp    httpd11
a37a437914b4   httpd     "httpd-foreground"   About a minute ago   Up 44 seconds   80/tcp    loving_brahmagupta
[root@docker ~ 15:55:51]# docker attach httpd11
ls
pws

^C[Mon Nov 17 07:56:11.913172 2025] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down
[root@docker ~ 15:56:12]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED              STATUS              PORTS     NAMES
994f0f87932b   httpd     "httpd-foreground"   35 seconds ago       Up 7 seconds        80/tcp    httpd11
a37a437914b4   httpd     "httpd-foreground"   About a minute ago   Up About a minute   80/tcp    loving_brahmagupta
[root@docker ~ 15:56:19]# docker stop httpd11
httpd11
[root@docker ~ 15:56:37]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS                     PORTS     NAMES
994f0f87932b   httpd     "httpd-foreground"   56 seconds ago   Exited (0) 2 seconds ago             httpd11
a37a437914b4   httpd     "httpd-foreground"   2 minutes ago    Up About a minute          80/tcp    loving_brahmagupta
[root@docker ~ 15:56:40]#

退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时 docker 会根据 --restart 的策略判断是否需要重启容器。但如果容器是因为执行 docker stopdocker kill 退出,则不会自动重启。

好了,容器操作就讨论到这里,下一节我们将学习如何限制容器对资源的使用。

bash 复制代码

027 限制容器对内存的使用

cgroup简介

docker 通过 cgroup 来控制容器使用的资源配额,包括 CPU、内存、磁盘三大方面,基本覆盖了常见的资源配额和使用量控制。

cgroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如 cpu、memory、磁盘IO等等) 的机制,被 LXC、docker 等很多项目用于实现进程资源控制。cgroup 将任意进程进行分组化管理的 Linux 内核功能。cgroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为 cgroup 子系统,有以下几大子系统实现:

blkio:设置限制每个块设备的输入输出控制。例如:磁盘,光盘以及 usb 等等。

cpu:使用调度程序为 cgroup 任务提供 cpu 的访问。

cpuacct:产生 cgroup 任务的 cpu 资源报告。

cpuset:如果是多核心的 cpu,这个子系统会为 cgroup 任务分配单独的 cpu 和内存。

devices:允许或拒绝 cgroup 任务对设备的访问。

freezer:暂停和恢复 cgroup 任务。

memory:设置每个 cgroup 的内存限制以及产生内存资源报告。

net_cls:标记每个网络包以供 cgroup 方便使用。

ns:命名空间子系统。

perf_event:增加了对每 group 的监测跟踪的能力,可以监测属于某个特定的 group 的所有线程以及运行在特定CPU上的线程。

stress是什么?

是模拟压力测试的工具

在机器上模拟cpu、内存等使用率

来检测不同状态下的运行情况

目前 docker 只是用了其中一部分子系统,实现对资源配额和使用的控制。

可以使用 stress 工具来测试 CPU 和内存。使用下面的 Dockerfile 来创建一个基于 Ubuntu 的 stress 工具镜像。

Dockerfile

bash 复制代码
[root@docker stress 16:34:34]# vim Dockerfile
# Version 1
FROM ubuntu
MAINTAINER gaoqd "6946630@qq.com"
RUN apt-get -y update && apt-get -y install stress
 # 以服务或进程的形式运行
ENTRYPOINT ["/usr/bin/stress"]     

使用Dockerfile构建镜像ubuntu-with-stress

bash 复制代码
[root@docker stress 16:35:24]# docker build -t ubuntu-with-stress .             
[+] Building 0.1s (6/6) FINISHED                                        docker:default
 => [internal] load build definition from Dockerfile                              0.0s
 => => transferring dockerfile: 180B                                              0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                  0.0s
 => [internal] load .dockerignore                                                 0.0s
 => => transferring context: 2B                                                   0.0s
 => [1/2] FROM docker.io/library/ubuntu:latest                                    0.0s
 => CACHED [2/2] RUN apt-get -y update && apt-get -y install stress               0.0s
 => exporting to image                                                            0.0s
 => => exporting layers                                                           0.0s
 => => writing image sha256:def3199e6bc21bc7b6715c81d6643958e92da3bce2c9947bbc16  0.0s
 => => naming to docker.io/library/ubuntu-with-stress                             0.0s

内存限额

与操作系统类似,容器可使用的内存包括两部分:物理内存和 swap。 Docker 通过下面两组参数来控制容器内存的使用量。

  1. -m--memory:设置内存的使用限额,例如 100M, 2G。
  2. --memory-swap:设置 内存+swap 的使用限额。

当我们执行如下命令:

docker run -m 200M --memory-swap=300M ubuntu

其含义是允许该容器最多使用 200M 的内存和 100M 的 swap。

正常情况下,--memory-swap 的值包含容器可用内存和可用swap。所以 -m 300m --memory-swap=1g 的含义为:容器可用使用300M的物理内存,并且可以使用700M(1G-300)的swap。

如果--memory-swap 设置为0 或者不设置,则容器可以使用的swap大小为-m值的两倍。

如果 --memory-swap 的值和-m 值相同,则容器不能使用swap

如果 --memory-swap值为-1。它表示容器程序使用的内存受限,而可以使用的swap空间不受限制(宿主机有多少swap空间该容器就可以使用多少)

下面我们将使用ubuntu-with-stress镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:

bash 复制代码
[root@docker stress 16:35:27]# docker run -it -m 200M --memory-swap=300M ubuntu-with-stress --vm 1 --vm-bytes 280M -v
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [6] forked
stress: dbug: [6] allocating 293601280 bytes ...
stress: dbug: [6] touching bytes in strides of 4096 bytes ...
stress: dbug: [6] freed 293601280 bytes
stress: dbug: [6] allocating 293601280 bytes ...
stress: dbug: [6] touching bytes in strides of 4096 bytes ...
stress: dbug: [6] freed 293601280 bytes
stress: dbug: [6] allocating 293601280 bytes ...
stress: dbug: [6] touching bytes in strides of 4096 bytes ...
^Cstress: FAIL: [1] (425) <-- worker 6 got signal 2
stress: WARN: [1] (427) now reaping child worker processes
stress: FAIL: [1] (431) kill error: No such process
stress: FAIL: [1] (461) failed run completed in 5s

--vm 1:启动 1 个内存工作线程。

--vm-bytes 280M:每个线程分配 280M 内存。

因为 280M 在可分配的范围(300M)内,所以工作线程能够正常工作,其过程是:

  1. 分配 280M 内存。
  2. 释放 280M 内存。
  3. 再分配 280M 内存。
  4. 再释放 280M 内存。
  5. 一直循环...

如果让工作线程分配的内存超过 300M,结果如下:

复制代码
[root@docker stress 16:35:36]# docker run -it -m 200M --memory-swap=300M ubuntu-with-stress --vm 1 --vm-bytes 310M -v
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogvm worker 1 [7] forked
stress: dbug: [7] allocating 325058560 bytes ...
stress: dbug: [7] touching bytes in strides of 4096 bytes ...
stress: FAIL: [1] (425) <-- worker 7 got signal 9
stress: WARN: [1] (427) now reaping child worker processes
stress: FAIL: [1] (431) kill error: No such process
stress: FAIL: [1] (461) failed run completed in 0s

分配的内存超过限额,stress 线程报错,容器退出。

如果在启动容器时只指定 -m 而不指定 --memory-swap,那么 --memory-swap 默认为 -m 的两倍,比如:

docker run -it -m 200M ubuntu-with-stress

容器最多使用 200M 物理内存和 200M swap。

内存限额就讨论到这里,下一节我们学习如何限制容器对 CPU 资源的使用。

028 限制容器对CPU的使用

上节学习了如何限制容器对内存的使用,本节我们来看CPU。

默认设置下,所有容器可以平等地使用 host CPU 资源并且没有限制。

Docker 可以通过 -c--cpu-shares 设置容器使用 CPU 的权重。如果不指定,默认值为 1024。

--cpu-shares的值不能保证可以获得1个 vcpu 或者多少 GHz 的 CPU 资源,仅仅只是一个弹性的加权值。

默认情况下,每个 docker 容器的 cpu 份额都是1024。单独一个容器的份额是没有意义的,只有在同时运行多个容器时,容器的 CPU 加权的效果才能体现出来。例如,两个容器A、B的 CPU 份额分别为1000和500,在 CPU 进行时间片分配的时候,容器 A 比容器 B 多一倍的机会获得 CPU 的时间片,但分配的结果取决于当时主机和其他容器的运行状态,实际上也无法保证容器A一定能获得 CPU 时间片。比如容器A的进程一直是空闲的,那么容器B是可以获取比容器A更多的 CPU 时间片的。极端情况下,比如说主机上只运行了一个容器,即使它的 CPU 份额只有 50,它也可以独占整个主机的 CPU 资源。

cgroups 只在容器分配的资源紧缺时,也就是说在需要对容器使用的资源进行限制时,才会生效。因此,无法单纯根据某个容器的 CPU 份额来确定有多少 CPU 资源分配给它,资源分配结果取决于同时运行的其他容器的 CPU 分配和容器中进程运行情况。

换句话说:通过 cpu share 可以设置容器使用 CPU 的优先级。

比如在 host 中启动了两个容器:

docker run --name "container_A" -c 1024 ubuntu

docker run --name "container_B" -c 512 ubuntu

container_A 的 cpu share 1024,是 container_B 的两倍。当两个容器都需要 CPU 资源时,container_A 可以得到的 CPU 是 container_B 的两倍。

需要特别注意的是,这种按权重分配 CPU 只会发生在 CPU 资源紧张的情况下。如果 container_A 处于空闲状态,这时,为了充分利用 CPU 资源,container_B 也可以分配到全部可用的 CPU。

下面我们继续用 ubuntu-with-stress 做实验。

  1. 启动 container_A,cpu share 为 1024:

    bash 复制代码
    [root@docker stress 16:35:41]# docker run --name "container_A" -it -c 1024 ubuntu-with-stress --cpu 4 -v
    stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
    stress: dbug: [1] using backoff sleep of 12000us
    stress: dbug: [1] --> hogcpu worker 4 [7] forked
    stress: dbug: [1] using backoff sleep of 9000us
    stress: dbug: [1] --> hogcpu worker 3 [8] forked
    stress: dbug: [1] using backoff sleep of 6000us
    stress: dbug: [1] --> hogcpu worker 2 [9] forked
    stress: dbug: [1] using backoff sleep of 3000us
    stress: dbug: [1] --> hogcpu worker 1 [10] forked
    
    #再开一个窗口
    #使用如下命令,创建容器,则最终生成的 cgroup 的 CPU 份额配置可以下面的文件中找到。
    [root@docker ~]# cat /sys/fs/cgroup/cpu/docker/<容器长ID>/cpu.shares
    1024

    --cpu 用来设置工作线程的数量。因为当前 host 有 4颗 CPU,所以要4个工作线程才能将 CPU 压满。如果 host 有多颗 CPU,则需要相应增加 --cpu 的数量。

  2. 再开一个窗口,启动 container_B,cpu share 为 512:

    bash 复制代码
    [root@docker ~ 16:48:56]# docker run --name "container_B" -it -c 512 ubuntu-with-stress --cpu 4 -v
    stress: info: [1] dispatching hogs: 4 cpu, 0 io, 0 vm, 0 hdd
    stress: dbug: [1] using backoff sleep of 12000us
    stress: dbug: [1] --> hogcpu worker 4 [7] forked
    stress: dbug: [1] using backoff sleep of 9000us
    stress: dbug: [1] --> hogcpu worker 3 [8] forked
    stress: dbug: [1] using backoff sleep of 6000us
    stress: dbug: [1] --> hogcpu worker 2 [9] forked
    stress: dbug: [1] using backoff sleep of 3000us
    stress: dbug: [1] --> hogcpu worker 1 [10] forked
    
    [root@docker ~]# cat /sys/fs/cgroup/cpu/docker/<容器长ID>/cpu.shares
    512
  3. 在 host 中执行 top,查看容器对 CPU 的使用情况:

    bash 复制代码
    top
     PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
       9364 root      20   0    3620    108      0 R  80.7   0.0   1:15.83 stress
       9366 root      20   0    3620    108      0 R  61.5   0.0   1:17.30 stress
       9365 root      20   0    3620    108      0 R  57.8   0.0   1:13.40 stress
       9367 root      20   0    3620    108      0 R  50.8   0.0   1:11.29 stress
       9495 root      20   0    3620    108      0 R  44.9   0.0   0:21.89 stress
       9496 root      20   0    3620    108      0 R  43.2   0.0   0:21.03 stress
       9494 root      20   0    3620    108      0 R  30.6   0.0   0:22.53 stress
       9493 root      20   0    3620    108      0 R  28.6   0.0   0:20.64 stress

    因为我们host是4核所以开启了4个进程,为的就是充分让系统资源变得紧张,只有这样竞争资源,我们设定的资源比例才可以显现出来,如果只运行一个进程,他们会自动分配到空闲的CPU,这样比例就无法看出来。目前可以看到总比例是2:1。

    container_A 消耗的 CPU 是 container_B 的两倍。

  4. 现在暂停 container_A:

    bash 复制代码
    [root@docker ~]# docker pause container_A
    container_A
  5. top 显示 container_B 在 container_A 空闲的情况下能够用满整颗 CPU:

    bash 复制代码
        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
       9494 root      20   0    3620    108      0 R  99.7   0.0   0:34.54 stress
       9493 root      20   0    3620    108      0 R  99.3   0.0   0:32.37 stress
       9496 root      20   0    3620    108      0 R  99.3   0.0   0:33.03 stress
       9495 root      20   0    3620    108      0 R  99.0   0.0   0:32.98 stress

029 export和import容器

export-容器导出

将容器导出为一个tar包

bash 复制代码
[root@docker ~ 09:58:32]# docker export --help

Usage:  docker export [OPTIONS] CONTAINER

Export a container's filesystem as a tar archive

Aliases:
  docker container export, docker export

Options:
  -o, --output string   Write to a file, instead of STDOUT

不管此时这个容器是否处于运行状态,都可以导出为文件。

示例:

bash 复制代码
# 创建容器httpd1用于测试
[root@docker ~ 10:00:04]# docker run -d --name httpd1 httpd
0bdd67f2303aab9d674fd252a2433fc82122ddc67d7de25ab2ac0cf17b76d2f6
[root@docker ~ 10:00:19]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS         PORTS     NAMES
0bdd67f2303a   httpd     "httpd-foreground"   6 seconds ago   Up 6 seconds   80/tcp    httpd1

[root@docker ~ 10:00:47]# docker export httpd1 -o myhttpd.tar
[root@docker ~ 10:00:58]# ls
anaconda-ks.cfg  myhttpd.tar  stress
import-容器tar包导入

import将export导出的tar包导入成为镜像

bash 复制代码
[root@docker ~ 10:00:59]# docker import --help

Usage:  docker import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]

Import the contents from a tarball to create a filesystem image

Aliases:
  docker image import, docker import

Options:
  -c, --change list       Apply Dockerfile instruction to the created image
  -m, --message string    Set commit message for imported image
      --platform string   Set platform if server is multi-platform capable

示例:

bash 复制代码
[root@docker ~ 10:01:13]# mkdir  myhttpd
[root@docker ~ 10:01:49]# cd myhttpd
[root@docker myhttpd 10:01:52]# docker import myhttpd.tar
open myhttpd.tar: no such file or directory
[root@docker myhttpd 10:02:05]# cd
[root@docker ~ 10:02:29]# ls
anaconda-ks.cfg  myhttpd  myhttpd.tar  stress
[root@docker ~ 10:02:32]# docker import myhttpd.tar
sha256:0179d22cd5a2aa1f639021a928966c52e2f9798cfa8f291dbf4aa53ba247c033
[root@docker ~ 10:02:41]# docker images
REPOSITORY           TAG       IMAGE ID       CREATED         SIZE
<none>               <none>    0179d22cd5a2   7 seconds ago   115MB
<none>               <none>    c1278276b2dd   17 hours ago    135MB
ubuntu-with-stress   latest    def3199e6bc2   17 hours ago    135MB
httpd                latest    6a4fe18d08d2   13 days ago     117MB
ubuntu               latest    c3a134f2ace4   4 weeks ago     78.1MB
busybox              latest    08ef35a1c3f0   13 months ago   4.43MB

[root@docker ~ 10:02:48]# docker import myhttpd.tar myweb:v1
sha256:8984f767c7298ea61936952d1e0e63fde6d92bfdb95b0649f62323f0d026af25
[root@docker ~ 10:03:11]# docker images
REPOSITORY           TAG       IMAGE ID       CREATED          SIZE
myweb                v1        8984f767c729   2 seconds ago    115MB
<none>               <none>    0179d22cd5a2   32 seconds ago   115MB
ubuntu-with-stress   latest    def3199e6bc2   17 hours ago     135MB
<none>               <none>    c1278276b2dd   17 hours ago     135MB
httpd                latest    6a4fe18d08d2   13 days ago      117MB
ubuntu               latest    c3a134f2ace4   4 weeks ago      78.1MB
busybox              latest    08ef35a1c3f0   13 months ago    4.43MB

docker savedocker export对比:

  • docker save:将镜像保存为文件,save会保存该镜像的所有元数据和历史记录。
  • docker export:将容器导出为文件,文件会丢失所有元数据和历史记录,仅保存容器当时的状态,再次导入会当作全新的镜像。

思考:docker export导出的文件是否可以使用docker load导入?

bash 复制代码
[root@docker ~ 10:03:13]# docker load -i myhttpd.tar
open /var/lib/docker/tmp/docker-import-3923997408/boot/json: no such file or directory
[root@docker ~ 10:03:31]# ls
anaconda-ks.cfg  myhttpd  myhttpd.tar  stress

思考:docker save导出的文件是否可以使用docker import导入?

提示:我们可以将docker save保存出来的文件解压后分析文件结构

bash 复制代码
[root@docker ~ 10:03:36]# rm -r myhttpd
rm: remove directory 'myhttpd'? y
[root@docker ~ 10:03:45]# ls
anaconda-ks.cfg  myhttpd.tar  stress

030 实现容器的底层技术

为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。

cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额, namespace 实现资源隔离。

cgroup

cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的限额。相信你已经猜到了:前面我们看到的--cpu-shares-m--device-write-bps 实际上就是在配置 cgroup。

cgroup 到底长什么样子呢?我们可以在 /sys/fs/cgroup 中找到它。还是用例子来说明,启动一个容器,设置 --cpu-shares=512

bash 复制代码
[root@docker docker]# docker run -it --cpu-shares 512 ubuntu-with-stress -c 1 -v
stress: info: [1] dispatching hogs: 1 cpu, 0 io, 0 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 3000us
stress: dbug: [1] --> hogcpu worker 1 [7] forked

查看容器的 ID:

在 /sys/fs/cgroup/cpu/docker 目录中,Linux 会为每个容器创建一个 cgroup 目录,以容器长ID 命名:

目录中包含所有与 cpu 相关的 cgroup 配置,文件 cpu.shares 保存的就是 --cpu-shares 的配置,值为 512。

同样的,/sys/fs/cgroup/memory/docker 和 /sys/fs/cgroup/blkio/docker 中保存的是内存以及 Block IO 的 cgroup 配置。

namespace

在每个容器中,我们都可以看到文件系统,网卡等资源,这些资源看上去是容器自己的。拿网卡来说,每个容器都会认为自己有一块独立的网卡,即使 host 上只有一块物理网卡。这种方式非常好,它使得容器更像一个独立的计算机。

Linux 实现这种方式的技术是 namespace。namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。换句话说,namespace 实现了容器间资源的隔离

Linux 使用了六种 namespace,分别对应六种资源:Mount、UTS、IPC、PID、Network 和 User,下面我们分别讨论。

Mount namespace

Mount namespace 让容器看上去拥有整个文件系统。

容器有自己的 / 目录,可以执行 mountumount 命令。当然我们知道这些操作只在当前容器中生效,不会影响到 host 和其他容器。

UTS namespace

简单的说,UTS namespace 让容器有自己的 hostname。 默认情况下,容器的 hostname 是它的短ID,可以通过 -h--hostname 参数设置。

IPC namespace

IPC namespace 让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与 host 和其他容器的 IPC 混在一起。

PID namespace

我们前面提到过,容器在 host 中以进程的形式运行。例如当前 host 中运行了两个容器:

bash 复制代码
[root@docker ~]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS          PORTS     NAMES
a80f1f8b692c   ubuntu    "/bin/bash"          26 seconds ago   Up 4 seconds              interesting_davinci
835dd4eebda2   httpd     "httpd-foreground"   56 seconds ago   Up 55 seconds   80/tcp    adoring_pare

通过 ps axf 可以查看容器进程:

所有容器的进程都挂在 dockerd 进程下,同时也可以看到容器自己的子进程。 如果我们进入到某个容器,ps 就只能看到自己的进程了:

bash 复制代码
[root@docker ~]# docker exec -it a80f1f8b692c bash
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# ps axf
    PID TTY      STAT   TIME COMMAND
     17 pts/1    Ss     0:00 bash
     25 pts/1    R+     0:00  \_ ps axf
      1 pts/0    Ss+    0:00 /bin/bash

而且进程的 PID 不同于 host 中对应进程的 PID,容器中 PID=1 的进程当然也不是 host 的 init 进程。也就是说:容器拥有自己独立的一套 PID,这就是 PID namespace 提供的功能。

Network namespace

Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。我们会在后面网络章节详细讨论。

User namespace

User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户。

bash 复制代码
[root@docker ~]# docker exec -it a80f1f8b692c bash
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# useradd dyx       #容器中创建用户dyx
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# exit
exit
[root@docker ~]# su - dyx              #宿主机中并没有用户dyx
su: user gaoqd does not exist

在容器中创建了用户 gaoqd,但 host 中并不会创建相应的用户。

小结

本章首先通过大量实验学习了容器的各种操作以及容器状态之间如何转换,然后讨论了限制容器使用 CPU、内存和 Block IO 的方法,最后学习了实现容器的底层技术:cgroup 和 namespace。

下面是容器的常用操作命令:

create 创建容器

run 运行容器

pause 暂停容器

unpause 取消暂停继续运行容器

stop 发送 SIGTERM 停止容器

kill 发送 SIGKILL 快速停止容器

start 启动容器

restart 重启容器

attach attach 到容器启动进程的终端

exec 在容器中启动新进程,通常使用 "-it" 参数

logs 显示容器启动进程的控制台输出,用 "-f" 持续打印

rm 从磁盘中删除容器

到这里,我们已经学习完了容器章节。下一节开始讨论容器网络。

第5章 网络

031 none和host网络的适用场景

本章开始讨论 Docker 网络。

我们会首先学习 Docker 提供的几种原生网络,以及如何创建自定义网络。然后探讨容器之间如何通信,以及容器与外界如何交互。

Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本章重点讨论前一种。对于更为复杂的多 host 容器网络,我们会在后面进阶技术章节单独讨论。

Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls 命令查看:

bash 复制代码
[root@docker ~ 11:34:31]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
277c6b0af992   bridge    bridge    local
0a2de003f10e   host      host      local
f216aa9a5fb9   none      null      local

下面我们分别讨论它们。

none 网络

none网络的driver类型是null,IPAM字段为空。挂在none网络上的容器只有lo,无法与外界通信。

bash 复制代码
[root@docker ~ 13:45:02]# docker network inspect none
[
    {
        "Name": "none",
        "Id": "f216aa9a5fb9c21cb89a82fc053e9d78fcdba127b7ebe77444a14e48cd5f80e9",
        "Created": "2025-11-13T10:55:47.53139052+08:00",
        "Scope": "local",
        "Driver": "null",
        "EnableIPv6": false,
        "IPAM": {
            `查看`
            "Driver": "default",
            "Options": null,
            `查看`
            "Config": null
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

故名思议,none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过 --network=none 指定使用 none 网络。

bash 复制代码
[root@docker ~ 11:34:41]# docker run -it --network=none busybox
/ # ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

/ # hostname
2193acef7f8c
/ # exit

我们不禁会问,这样一个封闭的网络有什么用呢?

其实还真有应用场景。封闭意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用 none 网络。

比如某个容器的唯一用途是生成随机密码,就可以放到 none 网络中避免密码被窃取。

当然大部分容器是需要网络的,我们接着看 host 网络。

host 网络

挂在host网络上的容器共享宿主机的network namespace。即容器的网络配置与host网络配置完全一样。

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过 --network=host 指定使用 host 网络。

bash 复制代码
#容器网络与宿主机相同
[root@docker ~ 11:35:25]# docker run -it --network=host busybox
#容器主机与宿主机相同
/ # hostname
docker
/ # ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:7A:2E:86:C5
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          inet6 addr: fe80::42:7aff:fe2e:86c5/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:746 (746.0 B)

ens160    Link encap:Ethernet  HWaddr 00:0C:29:8C:97:27
          inet addr:192.168.108.30  Bcast:192.168.108.255  Mask:255.255.255.0
          inet6 addr: fe80::20c:29ff:fe8c:9727/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:5521 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1640 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:467985 (457.0 KiB)  TX bytes:258269 (252.2 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

vethe6fdd3b Link encap:Ethernet  HWaddr 52:1F:C2:4D:E0:38
          inet6 addr: fe80::501f:c2ff:fe4d:e038/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:18 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:1436 (1.4 KiB)

/ # ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
7: vethe6fdd3b@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0
    link/ether 52:1f:c2:4d:e0:38 brd ff:ff:ff:ff:ff:ff
bash 复制代码
[root@docker ~ 11:35:55]# hostname
docker
[root@docker ~ 11:35:59]# ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
7: vethe6fdd3b@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether 52:1f:c2:4d:e0:38 brd ff:ff:ff:ff:ff:ff link-netnsid 0

在容器中可以看到 host 的所有网卡,并且连 hostname 也是 host 的。host 网络的使用场景又是什么呢?

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择 host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。

Docker host 的另一个用途是让容器可以直接配置 host 网路。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。

下一节讨论应用更广的 bridge 网络。

032 学容器必须懂brige网络

上一节我们讨论了 none 和 host 类型的容器网络,本节学习应用最广泛也是默认的 bridge 网络。

Docker 安装时会创建一个 命名为 docker0 的 linux bridge,实际上它是 Linux 的一个 bridge (网桥),可以理解为一个软件交换机,它会在挂载到它的网口之间进行转发。如果不指定--network,创建的容器默认都会挂到 docker0 上。

Docker 就创建了在主机和所有容器之间一个虚拟共享网络 当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包), 这对接口

  • 一端在容器内即 eth0;
  • 另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)

通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。

下面演示:

先配置yum源用于安装软件

bash 复制代码
[root@docker ~ 11:37:55]# cd /etc/yum.repos.d/
[root@docker yum.repos.d 11:50:14]# vim cloud.repo
[root@docker ~ 14:09:05]# cat /etc/yum.repos.d/cloud.repo
[centos-openstack-victoria]
name=CentOS 8 - OpenStack victoria
baseurl=https://mirrors.aliyun.com/centos-vault/8-stream/cloud/x86_64/openstack-victoria/
enabled=1
gpgcheck=0

[root@docker yum.repos.d 11:50:35]# yum install -y bridge-utils
CentOS 8 - OpenStack victoria                          5.0 MB/s | 3.3 MB     00:00
Last metadata expiration check: 0:00:01 ago on Tue 18 Nov 2025 11:50:48 AM CST.
Dependencies resolved.
=======================================================================================
 Package            Architecture Version         Repository                       Size
=======================================================================================
Installing:
 bridge-utils       x86_64       1.6-4.el8       centos-openstack-victoria        39 k

Transaction Summary
=======================================================================================
Install  1 Package

Total download size: 39 k
Installed size: 78 k
Downloading Packages:
bridge-utils-1.6-4.el8.x86_64.rpm                      587 kB/s |  39 kB     00:00
---------------------------------------------------------------------------------------
Total                                                  571 kB/s |  39 kB     00:00
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Preparing        :                                                               1/1
  Installing       : bridge-utils-1.6-4.el8.x86_64                                 1/1
  Running scriptlet: bridge-utils-1.6-4.el8.x86_64                                 1/1
  Verifying        : bridge-utils-1.6-4.el8.x86_64                                 1/1

Installed:
  bridge-utils-1.6-4.el8.x86_64

Complete!
bash 复制代码
[root@docker yum.repos.d 11:50:53]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02427a2e86c5       no              vethe6fdd3b

当前 docker0 上没有任何其他网络设备,我们创建一个容器看看有什么变化。

bash 复制代码
[root@docker ~ 13:59:33]# docker run -itd --name busybox1 busybox
31b695b4580f889b7238311dc2252176cc0cc5720784081467250648de268f51
[root@docker ~ 13:59:57]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02427a2e86c5       no              vethf70190a

一个新的网络接口 vethf70190a 被挂到了 docker0 上,vethf70190a就是新创建容器的虚拟网卡。

下面看一下容器的网络配置。

bash 复制代码
[root@docker ~ 14:00:10]# docker exec -it busybox1 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
       
# 容器里的网卡是22号网卡名字叫eth0,对面是23号网卡
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # exit

容器有一个网卡eth0@if23。大家可能会问了,为什么不是vethf70190a 呢?

实际上 eth0@if23vethf70190a 是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡,网卡的一头(eth0@if23)在容器中,另一头(vethf70190a)挂在网桥 docker0 上,其效果就是将 eth0@if23 也挂在了 docker0 上。

在宿主机上查看IP地址,可以证明Docker0上的vethddb2744和容器中的eth0@if23 是一对

在宿主机看到有块网卡:23: vethf70190a@if22

宿主机的23号网卡:23: vethf70190a@if22 含义就是宿主机23号网卡对面连接了一块22号网卡,23号网卡的名字叫vethf70190a

证明了容器busybox1里的eth0连接到了docker0网桥的vethf70190a

我们还看到eth0@if23 已经配置了 IP 172.17.0.2,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息:

bash 复制代码
[root@docker ~ 14:00:39]# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "277c6b0af992b762958c1746159abb60afdadda8daab8ba7d768e730a2f168ce",
        "Created": "2025-11-18T09:31:09.686537436+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "31b695b4580f889b7238311dc2252176cc0cc5720784081467250648de268f51": {
                "Name": "busybox1",
                "EndpointID": "beb872a77814aaeda36ef2ebc2d8da6ae013b91914709757029aa7f0c688d675",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

原来 bridge 网络配置的 subnet 就是 172.17.0.0/16,并且网关是 172.17.0.1。这个网关在哪儿呢?大概你已经猜出来了,就是 docker0。

bash 复制代码
[root@docker ~ 14:00:04]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe8c:9727/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7aff:fe2e:86c5/64 scope link
       valid_lft forever preferred_lft forever
23: vethf70190a@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 26:3f:4b:23:5a:95 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::243f:4bff:fe23:5a95/64 scope link
       valid_lft forever preferred_lft forever

容器创建时,docker 会自动从 172.17.0.0/16 中分配一个 IP,这里 16 位的掩码保证有足够多的 IP 可以供容器使用。

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络,下一节我们将详细讨论。

033 如何自定义容器网络?

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。

Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络,我们后面有章节单独讨论。

我们可通过 bridge 驱动创建类似前面默认的 bridge 网络。

bash 复制代码
[root@docker ~ 14:40:40]# docker network create --driver bridge my_net
6e74ad3cfe8816e5767cecb6d1cbb50a07bf4d66bde61893390e04bbbbf577dc

查看一下当前 host 的网络结构变化:

bash 复制代码
[root@docker ~ 14:41:01]# brctl show
bridge name     bridge id               STP enabled     interfaces
#my_net,看bridge  name和刚才创建的my_net id一样
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a

新增了一个网桥 br-6e74ad3cfe88,这里 br-6e74ad3cfe88 正好是新建 bridge 网络 my_net 的短 id。执行 docker network inspect 查看一下 my_net 的配置信息:

bash 复制代码
[root@docker ~ 14:41:07]# docker network inspect my_net
[
    {
        "Name": "my_net",
        "Id": "6e74ad3cfe8816e5767cecb6d1cbb50a07bf4d66bde61893390e04bbbbf577dc",
        "Created": "2025-11-18T14:41:01.23204675+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

这里 172.18.0.0/16 是 Docker 自动分配的 IP 网段。

我们可以自己指定 IP 网段吗?

答案是:可以。

只需在创建网段时指定 --subnet--gateway 参数:

bash 复制代码
[root@docker ~ 14:41:28]# docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 my_net2
270cb2fd71d9acde4a29159ebf0d77a4b12e2f4d5e4475b4e1aae5aeb3f7fd83
[root@docker ~ 14:42:20]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a
[root@docker ~ 14:42:25]# docker network inspect my_net2
[
    {
        "Name": "my_net2",
        "Id": "270cb2fd71d9acde4a29159ebf0d77a4b12e2f4d5e4475b4e1aae5aeb3f7fd83",
        "Created": "2025-11-18T14:42:19.973877795+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    #确实是指定的网段
                    "Subnet": "172.22.16.0/24",
                    #确实是指定的网关地址
                    "Gateway": "172.22.16.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

这里我们创建了新的 bridge 网络 my_net2,网段为 172.22.16.0/24,网关为 172.22.16.1。与前面一样,网关在 my_net2 对应的网桥 br-270cb2fd71d9 上:

bash 复制代码
[root@docker ~ 14:42:46]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a

#同时宿主机上出现了与网桥my_net,my_net2同名的网卡
#查看my_net2网卡
[root@docker ~ 14:43:08]# ip a |grep br-27
25: br-270cb2fd71d9: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-270cb2fd71d9

容器要使用新的网络,需要在启动时通过 --network 指定:

bash 复制代码
[root@docker ~ 14:43:31]# docker run -it --network=my_net2 --name busybox2 busybox
/ # ip  a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
26: eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:02 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.2/24 brd 172.22.16.255 scope global eth0
       valid_lft forever preferred_lft forever
/ # ctrl+P,ctrl+q退出容器

#my_net2上新增的接口veth02dd704连接容器busybox2
[root@docker ~ 14:44:50]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no              veth5f0502d
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a
[root@docker ~ 14:44:59]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe8c:9727/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7aff:fe2e:86c5/64 scope link
       valid_lft forever preferred_lft forever
23: vethf70190a@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 26:3f:4b:23:5a:95 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::243f:4bff:fe23:5a95/64 scope link
       valid_lft forever preferred_lft forever
24: br-6e74ad3cfe88: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:0d:5c:ea:18 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-6e74ad3cfe88
       valid_lft forever preferred_lft forever
25: br-270cb2fd71d9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:f1:d6:9d:6a brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-270cb2fd71d9
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f1ff:fed6:9d6a/64 scope link
       valid_lft forever preferred_lft forever
27: veth5f0502d@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 56:50:78:b6:73:29 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::5450:78ff:feb6:7329/64 scope link
       valid_lft forever preferred_lft forever

容器分配到的 IP 为 172.22.16.2。

到目前为止,容器的 IP 都是 docker 自动从 subnet 中分配,我们能否指定一个静态 IP 呢?

答案是:可以,通过--ip指定。

bash 复制代码
[root@docker ~ 14:45:11]# docker run -it --network=my_net2 --ip 172.22.16.8 --name busybox3 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:08 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.8/24 brd 172.22.16.255 scope global eth0 #确实是指定的IP
       valid_lft forever preferred_lft forever
/ #ctrl+P,ctrl+q退出容器

[root@docker ~ 14:50:17]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no              veth5f0502d
                                                        vethd8e4684
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a
[root@docker ~ 14:50:24]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe8c:9727/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7aff:fe2e:86c5/64 scope link
       valid_lft forever preferred_lft forever
23: vethf70190a@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 26:3f:4b:23:5a:95 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::243f:4bff:fe23:5a95/64 scope link
       valid_lft forever preferred_lft forever
24: br-6e74ad3cfe88: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:0d:5c:ea:18 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-6e74ad3cfe88
       valid_lft forever preferred_lft forever
25: br-270cb2fd71d9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:f1:d6:9d:6a brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-270cb2fd71d9
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f1ff:fed6:9d6a/64 scope link
       valid_lft forever preferred_lft forever
27: veth5f0502d@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 56:50:78:b6:73:29 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::5450:78ff:feb6:7329/64 scope link
       valid_lft forever preferred_lft forever
29: vethd8e4684@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 8a:ce:de:05:4b:8d brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::88ce:deff:fe05:4b8d/64 scope link
       valid_lft forever preferred_lft forever

注:只有使用 --subnet 创建的网络才能指定静态 IP

my_net 创建时没有指定 --subnet,如果指定静态 IP 报错如下:

bash 复制代码
[root@docker ~ 14:50:28]# docker run -it --network=mt_net -ip 172.18.0.8 busybox
docker: invalid containerPort: 172.18.0.8.
See 'docker run --help'.

好了,我们来看看当前 docker host 的网络拓扑结构。

下一节讨论这几个容器之间的连通性。

034 理解容器之间的连通性

通过前面小节的实践,当前 docker host 的网络拓扑结构如下图所示,今天我们将讨论这几个容器之间的连通性。

busybox2、busybox3 容器都挂在 my_net2 上,应该能够互通,我们验证一下:

bash 复制代码
# 登陆busybox2 ping busybox3
[root@docker ~ 15:51:17]# docker exec -it busybox2 sh
/ # ping 172.22.16.8
PING 172.22.16.8 (172.22.16.8): 56 data bytes
64 bytes from 172.22.16.8: seq=0 ttl=64 time=0.122 ms
64 bytes from 172.22.16.8: seq=1 ttl=64 time=0.119 ms
64 bytes from 172.22.16.8: seq=2 ttl=64 time=0.099 ms
64 bytes from 172.22.16.8: seq=3 ttl=64 time=0.105 ms
^C
--- 172.22.16.8 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.099/0.111/0.122 ms
/ # ^C

/ # ping 172.16.0.2
PING 172.16.0.2 (172.16.0.2): 56 data bytes
^C
--- 172.16.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
/ # ping  -c 3 172.22.16.1
PING 172.22.16.1 (172.22.16.1): 56 data bytes
64 bytes from 172.22.16.1: seq=0 ttl=64 time=0.089 ms
64 bytes from 172.22.16.1: seq=1 ttl=64 time=0.091 ms
64 bytes from 172.22.16.1: seq=2 ttl=64 time=0.119 ms

--- 172.22.16.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.089/0.099/0.119 ms

可见同一网络中的容器、网关之间都是可以通信的。

my_net2 与默认 bridge 网络(docker0)能通信吗?

从拓扑图可知,两个网络属于不同的网桥,应该不能通信,我们通过实验验证一下,让 busybox2 容器 ping buxybox1 容器:

bash 复制代码
/ # ping  -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
/ # exit

确实 ping 不通,符合预期。

"等等!不同的网络如果加上路由应该就可以通信了吧?"

这是一个非常非常好的想法。

确实,如果 host 上对每个网络的都有一条路由,同时操作系统上打开了 ip forwarding,host 就成了一个路由器,挂接在不同网桥上的网络就能够相互通信。下面我们来看看 docker host 满不满足这些条件呢?

ip r 查看 host 上的路由表:

bash 复制代码
[root@docker ~ 15:53:30]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev br-6e74ad3cfe88 proto kernel scope link src 172.18.0.1 linkdown
172.22.16.0/24 dev br-270cb2fd71d9 proto kernel scope link src 172.22.16.1
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100

172.17.0.0/16 和 172.22.16.0/24 两个网络的路由都定义好了。再看看 ip forwarding:

bash 复制代码
[root@docker ~ 15:53:33]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

ip forwarding 也已经启用了。

条件都满足,为什么不能通行呢?

我们还得看看 iptables:

bash 复制代码
[root@docker ~ 15:53:49]# iptables-save
# Generated by iptables-save v1.8.5 on Tue Nov 18 15:53:59 2025
*filter
:INPUT ACCEPT [6294:4008637]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [4211:489161]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-270cb2fd71d9 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-270cb2fd71d9 -j DOCKER
-A FORWARD -i br-270cb2fd71d9 ! -o br-270cb2fd71d9 -j ACCEPT
-A FORWARD -i br-270cb2fd71d9 -o br-270cb2fd71d9 -j ACCEPT
-A FORWARD -o br-6e74ad3cfe88 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-6e74ad3cfe88 -j DOCKER
-A FORWARD -i br-6e74ad3cfe88 ! -o br-6e74ad3cfe88 -j ACCEPT
-A FORWARD -i br-6e74ad3cfe88 -o br-6e74ad3cfe88 -j ACCEPT
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-270cb2fd71d9 ! -o br-270cb2fd71d9 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-6e74ad3cfe88 ! -o br-6e74ad3cfe88 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o br-270cb2fd71d9 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-6e74ad3cfe88 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
# Completed on Tue Nov 18 15:53:59 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 15:53:59 2025
*security
:INPUT ACCEPT [6218:3993184]
:FORWARD ACCEPT [11:952]
:OUTPUT ACCEPT [4211:489161]
COMMIT
# Completed on Tue Nov 18 15:53:59 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 15:53:59 2025
*raw
:PREROUTING ACCEPT [6308:4009841]
:OUTPUT ACCEPT [4211:489161]
COMMIT
# Completed on Tue Nov 18 15:53:59 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 15:53:59 2025
*mangle
:PREROUTING ACCEPT [6308:4009841]
:INPUT ACCEPT [6294:4008637]
:FORWARD ACCEPT [14:1204]
:OUTPUT ACCEPT [4211:489161]
:POSTROUTING ACCEPT [4222:490113]
COMMIT
# Completed on Tue Nov 18 15:53:59 2025
# Generated by iptables-save v1.8.5 on Tue Nov 18 15:53:59 2025
*nat
:PREROUTING ACCEPT [88:16269]
:INPUT ACCEPT [7:396]
:POSTROUTING ACCEPT [33:1942]
:OUTPUT ACCEPT [32:1858]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.22.16.0/24 ! -o br-270cb2fd71d9 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-6e74ad3cfe88 -j MASQUERADE
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i br-270cb2fd71d9 -j RETURN
-A DOCKER -i br-6e74ad3cfe88 -j RETURN
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Tue Nov 18 15:53:59 2025

原因就在这里了:iptables DROP 掉了网桥 docker0 与 br-ec761bc51778(my_net2) 之间双向的流量

bash 复制代码
[root@docker ~ 15:53:59]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no              veth5f0502d
                                                        vethd8e4684
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a

从规则的命名 DOCKER-ISOLATION 可知 docker 在设计上就是要隔离不同的 netwrok。

那么接下来的问题是:怎样才能让 busybox1与busybox2 通信呢?

答案是:为 busybox1 容器添加一块 my_net2 的网卡。这个可以通过docker network connect 命令实现。

bash 复制代码
[root@docker ~ 15:54:14]# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED             STATUS             PORTS     NAMES
c64cc2467714   busybox   "sh"      About an hour ago   Up About an hour             busybox3
d992df414dac   busybox   "sh"      About an hour ago   Up About an hour             busybox2
31b695b4580f   busybox   "sh"      2 hours ago         Up 2 hours                   busybox1
[root@docker ~ 15:54:24]# docker network connect my_net2 busybox1

我们在 httpd 容器中查看一下网络配置:

bash 复制代码
[root@docker ~ 15:54:46]# docker exec -it busybox1 sh
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
30: eth1@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:16:10:03 brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.3/24 brd 172.22.16.255 scope global eth1
       valid_lft forever preferred_lft forever
/ # exit
[root@docker ~ 15:57:46]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no              veth5f0502d
                                                        vethc8f8ea2
                                                        vethd8e4684
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              vethf70190a
[root@docker ~ 15:57:53]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe8c:9727/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7aff:fe2e:86c5/64 scope link
       valid_lft forever preferred_lft forever
23: vethf70190a@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 26:3f:4b:23:5a:95 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::243f:4bff:fe23:5a95/64 scope link
       valid_lft forever preferred_lft forever
24: br-6e74ad3cfe88: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:0d:5c:ea:18 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-6e74ad3cfe88
       valid_lft forever preferred_lft forever
25: br-270cb2fd71d9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:f1:d6:9d:6a brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-270cb2fd71d9
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f1ff:fed6:9d6a/64 scope link
       valid_lft forever preferred_lft forever
27: veth5f0502d@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 56:50:78:b6:73:29 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::5450:78ff:feb6:7329/64 scope link
       valid_lft forever preferred_lft forever
29: vethd8e4684@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 8a:ce:de:05:4b:8d brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::88ce:deff:fe05:4b8d/64 scope link
       valid_lft forever preferred_lft forever
31: vethc8f8ea2@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether ce:69:6e:c8:04:62 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::cc69:6eff:fec8:462/64 scope link
       valid_lft forever preferred_lft forever

容器中增加了一个网卡 ,分配了 my_net2 的 IP 172.22.16.3。现在 busybox2 应该能够访问busybox1 了,验证一下:

bash 复制代码
[root@docker ~ 15:58:08]# docker exec -it busybox1 sh
/ # ping -c 3 172.22.16.2
PING 172.22.16.2 (172.22.16.2): 56 data bytes
64 bytes from 172.22.16.2: seq=0 ttl=64 time=0.167 ms
64 bytes from 172.22.16.2: seq=1 ttl=64 time=0.108 ms
64 bytes from 172.22.16.2: seq=2 ttl=64 time=0.109 ms

--- 172.22.16.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.108/0.128/0.167 ms
/ # ping -c 3 172.22.16.8
PING 172.22.16.8 (172.22.16.8): 56 data bytes
64 bytes from 172.22.16.8: seq=0 ttl=64 time=0.120 ms
64 bytes from 172.22.16.8: seq=1 ttl=64 time=0.097 ms
64 bytes from 172.22.16.8: seq=2 ttl=64 time=0.173 ms

--- 172.22.16.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.097/0.130/0.173 ms
/ # exit

035 容器通信的三种方式

容器之间可通过 IP,Docker DNS Server 或 joined 容器三种方式通信。

IP 通信

从上一节的例子可以得出这样一个结论:两个容器要能通信,必须要有属于同一个网络的网卡。

满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网络,或者通过 docker network connect 将现有容器加入到指定网络。可参考上一节 busybox 的例子,这里不再赘述。

Docker DNS Server

通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。

从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过"容器名"通信。方法很简单,只要在启动时用 --name 为容器命名就可以了。

下面启动两个容器 bbox1 和 bbox2:

bash 复制代码
/ #[root@docker ~ 16:18:51]#docker run -it --network my_net2 --name bbox1 busybox
/ # 
ctrl_p,ctrl_q退出容器
[root@docker ~ 16:19:24]# docker run -it --network my_net2 --name bbox2 busybox
/ #
ctrl_p,ctrl_q退出容器

然后,bbox2 就可以直接 ping 到 bbox1 了:

bash 复制代码
[root@docker ~ 16:19:24]# docker run -it --network my_net2 --name bbox2 busybox
/ #ping -c 3 bbox1
PING bbox1 (172.22.16.5): 56 data bytes
64 bytes from 172.22.16.5: seq=0 ttl=64 time=0.112 ms
64 bytes from 172.22.16.5: seq=1 ttl=64 time=0.098 ms
64 bytes from 172.22.16.5: seq=2 ttl=64 time=0.101 ms

--- bbox1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.098/0.103/0.112 ms
/ #

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。下面验证一下:

创建 bbox3 和 bbox4,均连接到 bridge 网络。

bash 复制代码
[root@docker ~ 16:19:54]# docker run -it --name bbox3 busybox
/ # 
ctrl_p,ctrl_q退出容器

[root@docker ~ 16:20:20]# docker run -it --name bbox4 busybox
/ #
ctrl_p,ctrl_q退出容器

bbox4 无法 ping 到 bbox3。

bash 复制代码
[root@docker ~ 16:20:20]# docker run -it --name bbox4 busybox
/ #ping -c 3 bbox3
ping: bad address 'bbox3'
/ # 
joined 容器

joined 容器是另一种实现容器间通信的方式。

joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。请看下面的例子:

先创建一个 httpd 容器,名字为 web1。

bash 复制代码
[root@docker ~ 16:20:55]# docker run -d -it --name web1 httpd
a2fb924c78f06d0994a61be9a33d835bd52c94e7c78cc857300643ab0fdfc031

下面我们查看一下 web1 的网络:

bash 复制代码
[root@docker ~ 16:21:11]# docker exec -it web1 bash
root@a2fb924c78f0:/usr/local/apache2# apt-get update
Get:1 http://deb.debian.org/debian trixie InRelease [140 kB]
... ...
root@a2fb924c78f0:/usr/local/apache2# apt-get install iproute2  -y
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be ... ...
root@a2fb924c78f0:/usr/local/apache2# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever
root@a2fb924c78f0:/usr/local/apache2# read escape sequence       
# 也可以直接通过hostname -I观察IP地址       

然后创建 busybox 容器并通过 --network=container:web1 指定 jointed 容器为 web1:

请注意 busybox 容器中的网络配置信息

bash 复制代码
[root@docker ~ 16:34:20]# docker run -it --network container:web1 busybox
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
    link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.5/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

看!busybox 和 web1 的网卡 mac 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 web1 的 http 服务。

bash 复制代码
/ # wget 127.0.0.1
Connecting to 127.0.0.1 (127.0.0.1:80)
saving to 'index.html'
index.html           100% |***************************************|    45  0:00:00 ETA
'index.html' saved
/ # cat index.html
<html><body><h1>It works!</h1></body></html>
/ #
bash 复制代码
[root@docker ~ 16:35:17]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:8c:97:27 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 192.168.108.30/24 brd 192.168.108.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe8c:9727/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:7a:2e:86:c5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:7aff:fe2e:86c5/64 scope link
       valid_lft forever preferred_lft forever
23: vethf70190a@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 26:3f:4b:23:5a:95 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::243f:4bff:fe23:5a95/64 scope link
       valid_lft forever preferred_lft forever
24: br-6e74ad3cfe88: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:0d:5c:ea:18 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-6e74ad3cfe88
       valid_lft forever preferred_lft forever
25: br-270cb2fd71d9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:f1:d6:9d:6a brd ff:ff:ff:ff:ff:ff
    inet 172.22.16.1/24 brd 172.22.16.255 scope global br-270cb2fd71d9
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f1ff:fed6:9d6a/64 scope link
       valid_lft forever preferred_lft forever
27: veth5f0502d@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 56:50:78:b6:73:29 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::5450:78ff:feb6:7329/64 scope link
       valid_lft forever preferred_lft forever
29: vethd8e4684@if28: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 8a:ce:de:05:4b:8d brd ff:ff:ff:ff:ff:ff link-netnsid 2
    inet6 fe80::88ce:deff:fe05:4b8d/64 scope link
       valid_lft forever preferred_lft forever
31: vethc8f8ea2@if30: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether ce:69:6e:c8:04:62 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::cc69:6eff:fec8:462/64 scope link
       valid_lft forever preferred_lft forever
33: veth8dc2dd1@if32: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 3e:1e:b8:7a:f7:e1 brd ff:ff:ff:ff:ff:ff link-netnsid 3
    inet6 fe80::3c1e:b8ff:fe7a:f7e1/64 scope link
       valid_lft forever preferred_lft forever
35: veth311828f@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 4a:75:07:9d:21:53 brd ff:ff:ff:ff:ff:ff link-netnsid 4
    inet6 fe80::4875:7ff:fe9d:2153/64 scope link
       valid_lft forever preferred_lft forever
37: veth216692e@if36: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-270cb2fd71d9 state UP group default
    link/ether 6a:87:48:da:99:4f brd ff:ff:ff:ff:ff:ff link-netnsid 5
    inet6 fe80::6887:48ff:feda:994f/64 scope link
       valid_lft forever preferred_lft forever
39: vethc80f5b9@if38: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 8e:a6:61:04:85:69 brd ff:ff:ff:ff:ff:ff link-netnsid 6
    inet6 fe80::8ca6:61ff:fe04:8569/64 scope link
       valid_lft forever preferred_lft forever
41: veth8ed40f4@if40: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether 0e:dd:2c:1e:7d:83 brd ff:ff:ff:ff:ff:ff link-netnsid 7
    inet6 fe80::cdd:2cff:fe1e:7d83/64 scope link
       valid_lft forever preferred_lft forever
43: veth88d4b58@if42: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
    link/ether a6:51:b3:68:d7:2c brd ff:ff:ff:ff:ff:ff link-netnsid 8
    inet6 fe80::a451:b3ff:fe68:d72c/64 scope link
       valid_lft forever preferred_lft forever
[root@docker ~ 16:35:21]# brctl show
bridge name     bridge id               STP enabled     interfaces
br-270cb2fd71d9         8000.0242f1d69d6a       no              veth216692e
                                                        veth311828f
                                                        veth5f0502d
                                                        veth8dc2dd1
                                                        vethc8f8ea2
                                                        vethd8e4684
br-6e74ad3cfe88         8000.02420d5cea18       no
docker0         8000.02427a2e86c5       no              veth88d4b58
                                                        veth8ed40f4
                                                        vethc80f5b9
                                                        vethf70190a

joined 容器非常适合以下场景:

  1. 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
  2. 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。

容器之间的通信我们已经搞清楚了,接下来要考虑的是容器如何与外部世界通信?这将是下一节的主题。

036 容器如何访问外部世界

前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:

  1. 容器访问外部世界
  2. 外部世界访问容器

容器访问外部世界

在我们当前的实验环境下,docker host 是可以访问外网的。

bash 复制代码
[root@docker ~ 09:43:27]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED        STATUS                       PORTS     NAMES
d6c1c5b906a5   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              stoic_goldberg
a2fb924c78f0   httpd     "httpd-foreground"   41 hours ago   Exited (0) 41 hours ago                web1
decaabe8c7d9   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox4
579f23b868bf   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox3
16ede510e333   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox2
d097de27bd74   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox1
715038fb75f2   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox
c64cc2467714   busybox   "sh"                 43 hours ago   Exited (137) 41 hours ago              busybox3
d992df414dac   busybox   "sh"                 43 hours ago   Exited (137) 41 hours ago              busybox2
31b695b4580f   busybox   "sh"                 44 hours ago   Exited (137) 41 hours ago              busybox1

[root@docker ~ 09:43:53]# docker start busybox1
busybox1
bash 复制代码
[root@docker ~ 09:44:00]# ping -c 3 baidu.com
PING baidu.com (124.237.177.164) 56(84) bytes of data.
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=1 ttl=128 time=69.4 ms
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=2 ttl=128 time=44.5 ms
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=3 ttl=128 time=43.8 ms

--- baidu.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 43.846/52.591/69.378/11.873 ms

我们看一下容器是否也能访问外网呢?

bash 复制代码
[root@docker ~ 09:44:43]# docker run -it --name test1 busybox
/ # ping -c 3 www.baidu.com
PING www.baidu.com (180.101.51.73): 56 data bytes
64 bytes from 180.101.51.73: seq=0 ttl=127 time=86.101 ms
64 bytes from 180.101.51.73: seq=1 ttl=127 time=80.574 ms
64 bytes from 180.101.51.73: seq=2 ttl=127 time=8.051 ms

--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 8.051/58.242/86.101 ms
/ # exit

可见,容器默认就能访问外网

请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。

现象很简单,但更重要的:我们应该理解现象下的本质。

在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 www.baidu.com 的呢?

这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:

bash 复制代码
[root@docker ~ 09:45:30]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P POSTROUTING ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-6e74ad3cfe88 -j MASQUERADE
-A POSTROUTING -s 172.22.16.0/24 ! -o br-270cb2fd71d9 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
-A DOCKER -i br-6e74ad3cfe88 -j RETURN
-A DOCKER -i br-270cb2fd71d9 -j RETURN

在 NAT 表中,有这么一条规则:

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:如果网桥 docker0 收到来自 172.17.0.0/16 网段的外出包,把它交给 MASQUERADE 处理。而 MASQUERADE 的处理方式是将包的源地址替换成 host 的地址发送出去,即做了一次网络地址转换(NAT)

下面我们通过 tcpdump 查看地址是如何转换的。先查看 docker host 的路由表:

bash 复制代码
[root@docker ~ 09:45:45]# ip r
default via 192.168.108.2 dev ens160 proto static metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
172.18.0.0/16 dev br-6e74ad3cfe88 proto kernel scope link src 172.18.0.1 linkdown
172.22.16.0/24 dev br-270cb2fd71d9 proto kernel scope link src 172.22.16.1
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100

默认路由通过 ens160 发出去,所以我们要同时监控 ens160 和 docker0 上的 icmp(ping)数据包。

使用tcpdump观察现象

bash 复制代码
[root@docker ~ 09:45:51]# yum install -y tcpdump
Last metadata expiration check: 1 day, 18:30:31 ago on Tue 18 Nov 2025 03:15:40 PM CST.
... ...

Complete!

#再开一个窗口
[root@docker ~ 09:46:14]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes


[root@docker ~ 09:46:50]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes

再开一个窗口, busybox ping www.baidu.com

bash 复制代码
[root@docker ~ 09:47:21]# docker run -it busybox
/ # ping -c 3 www.qq.com
PING www.qq.com (101.91.42.232): 56 data bytes
64 bytes from 101.91.42.232: seq=0 ttl=127 time=74.171 ms
64 bytes from 101.91.42.232: seq=1 ttl=127 time=67.845 ms
64 bytes from 101.91.42.232: seq=2 ttl=127 time=124.780 ms

--- www.qq.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 67.845/88.932/124.780 ms

tcpdump 输出如下:

bash 复制代码
[root@docker ~ 09:46:14]# tcpdump -i docker0 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
09:47:47.217518 IP 172.17.0.3 > 101.91.42.232: ICMP echo request, id 6, seq 0, length 6 4
09:47:47.291555 IP 101.91.42.232 > 172.17.0.3: ICMP echo reply, id 6, seq 0, length 64
09:47:48.218207 IP 172.17.0.3 > 101.91.42.232: ICMP echo request, id 6, seq 1, length 6 4
09:47:48.285912 IP 101.91.42.232 > 172.17.0.3: ICMP echo reply, id 6, seq 1, length 64
09:47:49.219332 IP 172.17.0.3 > 101.91.42.232: ICMP echo request, id 6, seq 2, length 6 4
09:47:49.343978 IP 101.91.42.232 > 172.17.0.3: ICMP echo reply, id 6, seq 2, length 64

docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,这没问题,交给 MASQUERADE 处理。这时,在 ens160 上我们看到了变化:

bash 复制代码
[root@docker ~ 09:46:50]# tcpdump -i ens160 -n icmp
dropped privs to tcpdump
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens160, link-type EN10MB (Ethernet), capture size 262144 bytes
09:47:47.217549 IP 192.168.108.30 > 101.91.42.232: ICMP echo request, id 6, seq 0, leng th 64
                                                      vethc80f5b9
                                                        vethf70190a

joined 容器非常适合以下场景:

  1. 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。
  2. 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。

容器之间的通信我们已经搞清楚了,接下来要考虑的是容器如何与外部世界通信?这将是下一节的主题。

036 容器如何访问外部世界

前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:

  1. 容器访问外部世界
  2. 外部世界访问容器

容器访问外部世界

在我们当前的实验环境下,docker host 是可以访问外网的。

bash 复制代码
[root@docker ~ 09:43:27]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED        STATUS                       PORTS     NAMES
d6c1c5b906a5   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              stoic_goldberg
a2fb924c78f0   httpd     "httpd-foreground"   41 hours ago   Exited (0) 41 hours ago                web1
decaabe8c7d9   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox4
579f23b868bf   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox3
16ede510e333   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox2
d097de27bd74   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox1
715038fb75f2   busybox   "sh"                 41 hours ago   Exited (137) 41 hours ago              bbox
c64cc2467714   busybox   "sh"                 43 hours ago   Exited (137) 41 hours ago              busybox3
d992df414dac   busybox   "sh"                 43 hours ago   Exited (137) 41 hours ago              busybox2
31b695b4580f   busybox   "sh"                 44 hours ago   Exited (137) 41 hours ago              busybox1

[root@docker ~ 09:43:53]# docker start busybox1
busybox1
bash 复制代码
[root@docker ~ 09:44:00]# ping -c 3 baidu.com
PING baidu.com (124.237.177.164) 56(84) bytes of data.
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=1 ttl=128 time=69.4 ms
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=2 ttl=128 time=44.5 ms
64 bytes from 124.237.177.164 (124.237.177.164): icmp_seq=3 ttl=128 time=43.8 ms

--- baidu.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 43.846/52.591/69.378/11.873 ms

我们看一下容器是否也能访问外网呢?

bash 复制代码
[root@docker ~ 09:44:43]# docker run -it --name test1 busybox
/ # ping -c 3 www.baidu.com
PING www.baidu.com (180.101.51.73): 56 data bytes
64 bytes from 180.101.51.73: seq=0 ttl=127 time=86.101 ms
64 bytes from 180.101.51.73: seq=1 ttl=127 time=80.574 ms
64 bytes from 180.101.51.73: seq=2 ttl=127 time=8.051 ms

--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 8.051/58.242/86.101 ms
/ # exit

可见,容器默认就能访问外网

请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。

现象很简单,但更重要的:我们应该理解现象下的本质。

在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从容器向外 ping 时,数据包是怎样到达 www.baidu.com 的呢?

这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:

bash 复制代码
[root@docker ~ 09:45:30]# iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P POSTROUTING ACCEPT
-P OUTPUT ACCEPT
-N DOCKER
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-6e74ad3cfe88 -j MASQUERADE
-A POSTROUTING -s 172.22.16.0/24 ! -o br-270cb2fd71d9 -j MASQUERADE
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A DOCKER -i docker0 -j RETURN
-A DOCKER -i br-6e74ad3cfe88 -j RETURN
-A DOCKER -i br-270cb2fd71d9 -j RETURN

在 NAT 表中,有这么一条规则:

-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

其含义是:如果网桥 `docker0

相关推荐
Lynnxiaowen1 小时前
今天我们开始学习Docker概述与安装
linux·学习·docker·容器·云计算
Lee-Aiya1 小时前
MacBook M4芯片 Arm64架构 基于docker安装Oracle 19c
macos·docker·oracle·arm
曦云沐1 小时前
Docker双模式实战:从零到精通,Dockerfile与Docker Compose全方位构建部署
docker
风逸柏1 小时前
dockerfile说明
docker·容器
p***43481 小时前
后端在消息系统中的顺序保证
数据库·数据仓库·docker
翼龙云_cloud1 小时前
阿里云渠道商:阿里云网站访问速度慢有哪些加速方案?
运维·服务器·阿里云·云计算·php
wanhengidc1 小时前
云手机面向的用户群体都有哪些?
运维·服务器·科技·智能手机·云计算
你好龙卷风!!!1 小时前
mac上安x86minio的docker版的
运维·docker·容器
wanhengidc4 小时前
云计算时代 云手机与云服务器的不同
服务器·智能手机·云计算