docker详解:安装docker,docker架构,docker镜像、容器、网络、存储,容器监控,容器日志,综合实验

Docker

1. 容器生态系统

1.1 为什么要学习容器技术?

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

容器技术是继大数据和云计算之后又一炙手可热的技术,未来相当长一段时间内都会非常流行。掌握容器技术是市场的需要,也是提升自我价值的重要途径。

对于像容器这类平台级别的技术,通常涉及的知识范围会很广,相关的软件、解决方案也会很多,初学者往往容易迷失。我们可以先从整体鸟瞰容器生态系统,了解不同层次的技术及其关系,分清核心技术与辅助技术,做到心中有数。

1.2 容器生态系统概览

容器生态系统包含核心技术平台技术支持技术三个方面。

1.2.1 容器核心技术

容器核心技术是指能够让 Container 在 Host OS 上运行起来的那些技术。

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

别介绍。

  • 容器规范:保证容器生态的健康发展,保证不同容器之间能够兼容。OCI(Open Container Initiative)组织制定了开放的容器规范,目前发布了 runtime spec 和 image format spec。

  • 容器 runtime:容器真正运行的地方,需与操作系统 kernel 紧密协作。为容器提供运行环境,容器只有在 runtime 中才能运行。

    • lxc:Linux 上老牌的容器 runtime。

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

    • rkt:CoreOS 开发的容器 runtime,符合 OCI 规范。

  • 容器管理工具:对内与 runtime 交互,对外为用户提供 interface。

    • lxd:对应 lxc 的管理工具。

    • docker engine:对应 runc 的管理工具,包含后台 daemon 和 CLI。

    • rkt cli:对应 rkt 的管理工具。

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

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

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

  • Registry:统一存放 image 的仓库。

    • 公共:Docker Hub、Quay.io

    • 私有:企业自建的私有 Registry。

  • 容器 OS:专门运行容器的操作系统,与常规 OS 相比,容器 OS 通常体积更小,启动更快。因为是为容

    器定制的 OS,通常它们运行容器的效率会更高。

    • 代表:CoreOS、Atomic、Ubuntu Core。
1.2.2 容器平台技术

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

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

  • 容器管理平台:架构在编排引擎之上,支持多种编排引擎,抽象底层细节,提供 application catalog、一键部署等方便功能。

    • RancherContainerShip
  • 基于容器的 PaaS:为开发人员和公司提供开发、部署和管理应用的平台。

    • DeisFlynnDokku
  • 容器编排引擎:高效管理容器集群,包括容器管理、调度、集群定义和服务发现等。

    Docker Swarm

    Kubernetes

    Mesos + Marathon

1.2.3 容器支持技术

用于支持基于容器的基础设施。

  • 网络:管理容器与容器,容器与其他实体之间的连通性和隔离性。

    • docker networkflannelweavecalico
  • 服务发现:在动态环境下让 client 知道如何访问容器提供的服务。

    • etcdconsulzookeeper
  • 监控:收集和展示容器状态信息。

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

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

    • docker logslogspout
  • 安全性:扫描镜像漏洞。

    • OpenSCAP

2. 安装Docker

2.1 环境选择

  • 管理工具:Docker Engine

  • runtime:runc

  • 操作系统:CentOS Stream 8

安装操作系统

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

静态IP 192.168.108.30,主机名改为docker

复制代码
[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

2.2 安装步骤

复制代码
# 卸载旧版本(可选)
[root@docker ~ 20:43:57]# yum remove  docker-ce
No match for argument: docker-ce
No packages marked for removal.
Dependencies resolved.
Nothing to do.
Complete!

 
# 安装必要工具
# devicemapper 存储驱动已经在 docker 18.09 版本中被废弃,所以在后续的安Docker装中无需安装devicemapper支持。
[root@docker ~ 20:39:04]# yum install -y yum-utils device-mapper-persistent-data lvm2 vim

 
# 添加阿里云Docker CE仓库
#yum-config-manager --add-repo 地址
#作用:下载 URL 上对应的 repo 文件,自动保存到 /etc/yum.repos.d/
#下载下来的文件名默认:docker-ce.repo(由远端文件名决定)
[root@docker ~ 20:39:48]# 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 ~ 20:40:24]# cd /etc/yum.repos.d/

[root@docker yum.repos.d 20:40:38]# ls
docker-ce.repo  os.repo
[root@docker yum.repos.d 20:40:39]# cat docker-ce.repo 
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-stable-debuginfo]
name=Docker CE Stable - Debuginfo $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/debug-$basearch/stable
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-stable-source]
name=Docker CE Stable - Sources
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/source/stable
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-test]
name=Docker CE Test - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/$basearch/test
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-test-debuginfo]
name=Docker CE Test - Debuginfo $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/debug-$basearch/test
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-test-source]
name=Docker CE Test - Sources
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/source/test
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-nightly]
name=Docker CE Nightly - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/$basearch/nightly
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-nightly-debuginfo]
name=Docker CE Nightly - Debuginfo $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/debug-$basearch/nightly
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-nightly-source]
name=Docker CE Nightly - Sources
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/source/nightly
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg



# 更新缓存
[root@docker ~ 20:44:05]# yum makecache 
Docker CE Stable - x86_64                                    109 kB/s |  66 kB     00:00    
CentOS Stream 8 - BaseOS                                     9.3 kB/s | 3.9 kB     00:00    
CentOS Stream 8 - AppStream                                   11 kB/s | 4.4 kB     00:00    
Metadata cache created.

 
# 安装docker-ce
[root@docker ~ 20:44:25]# yum install  -y docker-ce

 
# 启动并设置开机自启
[root@docker ~ 20:45:35]# systemctl enable docker.service --now
Created symlink /etc/systemd/system/multi-user.target.wants/docker.service → /usr/lib/systemd/system/docker.service.

 
# 验证安装,查看docker版本
[root@docker ~ 20:46:29]# docker --version 
Docker version 26.1.3, build b72abbb

#验证docker状态
[root@docker ~ 20:47:01]# systemctl  status  docker.service 
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2026-07-26 20:46:29 CST; 34s ago
     Docs: https://docs.docker.com
 Main PID: 3305 (dockerd)
    Tasks: 9
   Memory: 31.5M
   CGroup: /system.slice/docker.service
           └─3305 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Jul 26 20:46:28 docker systemd[1]: Starting Docker Application Container Engine...
Jul 26 20:46:28 docker dockerd[3305]: time="2026-07-26T20:46:28.707398173+08:00" level=info >
Jul 26 20:46:28 docker dockerd[3305]: time="2026-07-26T20:46:28.735652944+08:00" level=info >
Jul 26 20:46:29 docker dockerd[3305]: time="2026-07-26T20:46:29.543989007+08:00" level=info >
Jul 26 20:46:29 docker dockerd[3305]: time="2026-07-26T20:46:29.642846367+08:00" level=info >
Jul 26 20:46:29 docker dockerd[3305]: time="2026-07-26T20:46:29.656042854+08:00" level=info >
Jul 26 20:46:29 docker dockerd[3305]: time="2026-07-26T20:46:29.656145937+08:00" level=info >
Jul 26 20:46:29 docker dockerd[3305]: time="2026-07-26T20:46:29.682909684+08:00" level=info >
Jul 26 20:46:29 docker systemd[1]: Started Docker Application Container Engine.

2.3 配置镜像加速器(华为云)

复制代码
[root@docker ~ 20:47:31]# vi /etc/docker/daemon.json
{
    "registry-mirrors": [
        "https://054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com"
    ]
}
# 按“Esc”,输入:wq保存并退出。

#重启容器引擎
[root@docker ~ 20:49:39]# systemctl  restart docker.service 

#确认配置结果
[root@docker ~ 20:50:05]# 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: 3.549GiB
 Name: docker
 ID: ac1f79c7-26ef-432d-a9b4-31d69e3e299a
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:    #看这里确认配置成功:
  https://054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com/
 Live Restore Enabled: false

2.4 运行第一个容器

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

复制代码
[root@docker ~ 20:50:23]# docker run hello-world

运行流程为:

  1. 先是从本地查找 hello-wrold 镜像,若找到则直接启动容器;若没找到

  2. 从 Docker Hub 下载 hello-world 镜像。

  3. 启动 hello-world 容器。

清空刚才的实验环境:

复制代码
[root@docker ~ 20:51:51]# docker rm -f $(docker ps -aq)   #删除所有容器
28f60c406954 
[root@docker ~ 20:52:04]# docker rmi -f hello-world:latest    #删除镜像hello-world

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

运行第二个容器

复制代码
[root@docker ~ 20:53:29]# docker run -d -p 80:80 httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
062e450697fa: Pull complete 
39d55e966aeb: Pull complete 
4f4fb700ef54: Pull complete 
69b5b831a361: Pull complete 
ebd81352b960: Pull complete 
010f7d7e175f: Pull complete 
Digest: sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
Status: Downloaded newer image for httpd:latest
c5c0e69cce16003b1a3148b06268a7fb1c40b46b53c091c50da75f8fe18580fc

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

  1. 从 Docker Hub 下载 httpd 镜像。镜像中已经安装好了 Apache HTTP Server。

  2. 启动 httpd 容器,并将容器的 80 端口映射到 host 的 80 端口。

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

复制代码
[root@docker ~ 20:53:56]# curl 192.168.108.30
<html><body><h1>It works!</h1></body></html>

2.5 Docker C/S分离部署

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

Docker 采用的是 Client/Server 架构,可以将客户端和服务端部署在不同的主机上。

Server端配置:

配置 ip 192.168.108.30

配置 hostname docker_server

复制代码
[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

#安装软件
[root@docker_server ~]# yum install -y yum-utils device-mapper-persistent-data
lvm2 vim
[root@localhost ~]# 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/dockerce.repo
[root@docker_server ~]# yum makecache
[root@docker_server ~]# yum install -y docker-ce
[root@docker_server ~]# systemctl enable docker.service --now
[root@docker_server ~]# vi /etc/docker/daemon.json
{
"registry-mirrors": [
"https://054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com" ]
}
# 按“Esc”,输入:wq保存并退出。

#重启容器引擎
[root@docker_server ~]# systemctl restart docker

[root@docker_server ~]# 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 ~]# systemctl daemon-reload
[root@docker_server ~]# systemctl restart docker.service
[root@docker_server ~]# systemctl stop firewalld

配置效果如下:

验证:

复制代码
#lsof:用于查看端口、进程占用情况的工具
[root@docker_server ~]# yum install lsof
[root@docker_server ~]# lsof -i :2375
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
dockerd 5440 root 3u IPv6 44991 0t0 TCP *:docker (LISTEN)

lsof:list open files,查看系统打开的文件
/ 端口 / 进程
-i:指定查看网络连接
:2375:指定要检查的端口号
作用:验证 Docker 是否在监听 2375 远程端口

Client端配置:

配置ip 192.168.108.31

复制代码
[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客户端
[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/dockerce.repo
[root@docker-client ~]# yum makecache
[root@docker-client ~]# yum install -y docker-ce-cli

#验证:
[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端。

复制代码
实验想要证明:Docker 典型 C/S(客户端 / 服务端)架构
1. Docker 分为两部分
- 客户端 docker(docker-ce-cli):只是命令工具,不负责运行容器、保存镜像
- 服务端 dockerd(docker-ce):容器引擎,真正拉镜像、创建容器、管理所有资源

2. 资源存放位置
镜像、容器、卷、网络全部存在 docker_server(运行 dockerd 的机器)
docker_client 仅仅是发送指令、接收返回结果,本机不会产生任何镜像和容器。

3. Docker架构

3.1 容器发展史

  • 1979年:Unix V7 引入 Chroot Jail,被认为是最早的容器化技术之一。

  • 2000年:FreeBSD 引入 Jail,为简单的 Chroot 隔离带来更多安全性。

  • 2004年:Solaris Containers 问世,使用系统资源控制和区域实现边界分离。

  • 2006年:Google 推出 Process Containers(后命名为 cgroups),限制、计算和隔离进程资源使用。

  • 2008年:LXC(LinuX Containers)发布,第一个使用 cgroups 和 namespace 的 Linux 容器管理器。

  • 2013年:Docker 推出首个版本,在 LXC 基础上优化了容器使用体验,后用 libcontainer 替换。

  • 2014年:Google 推出 LMCTFY;CoreOS 发布并支持 rkt。

3.2 容器 What, Why, How

What-什么是容器:

容器是一种轻量级、可移植、自包含的软件打包技术,使应用程序可以在几乎任何地方以相同的方式运

行。开发人员在自己笔记本上创建并测试好的容器,无需任何修改就能够在生产系统的虚拟机、物理服

务器或公有云主机上运行。

容器与虚拟机

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

容器由两部分组成:

  1. 应用程序本身

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

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

传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应

用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。

下图展示了二者的区别。

复制代码
一、左边:传统虚拟机(VM)架构
1.分层结构(自下而上)
Infrastructure(基础设施):物理硬件(CPU、内存、磁盘等)。
Hypervisor(虚拟机监控程序):如 VMware、KVM、Hyper-V,负责在物理硬件上虚拟化出多个独立虚拟机,并分配硬件资源。
Guest Operating System(客户操作系统):每个虚拟机都需要运行一套完整的独立操作系统(如 CentOS、Ubuntu),这是 VM资源开销的主要来源。
应用层:每个应用(App A/B/C)运行在独立的客户操作系统之上。

2.核心特点
系统级强隔离:每个 VM 拥有独立内核与完整 OS,隔离性极强,但资源开销巨大。
低运行密度:单台物理机通常只能部署几十台 VM,硬件资源利用率低。
启动缓慢:需加载完整操作系统,启动时间通常为分钟级。


二、右边:Docker 容器架构
1.分层结构(自下而上)
Infrastructure(基础设施):物理硬件,与 VM 架构一致。
Host Operating System(宿主机操作系统):所有容器共享宿主机的操作系统内核(如 CentOS、Ubuntu)。
Docker(容器引擎):负责容器的创建、隔离与管理,通过 Linux 内核的Namespace(资源隔离)和Cgroups(资源限制)技术
实现进程级隔离。
容器化应用层:多个容器(App A/B/C/D/E/F)仅包含应用程序及其依赖,无需独立操作系统,共享宿主机内核。

2.核心特点
进程级轻量隔离:容器共享宿主机内核,仅对进程、网络、文件系统做隔离,性能损耗极低(接近原生)。
高运行密度:单台物理机可部署上百甚至上千个容器,硬件资源利用率大幅提升。
秒级启动:仅需启动应用进程,无需加载完整 OS,启动时间通常为秒级 / 毫秒级。

如图所示,由于所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。另外,启动

容器不需要启动整个操作系统,所以容器部署和启动速度更快,开销更小,也更容易迁移。

Why-为什么需要容器:

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

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

容器解决的问题

Docker 的特性

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

容器的优势

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

容器意味着环境隔离和可重复性。开发人员只需为应用创建一次运行环境,然后打包成容器便可在其他

机器上运行。另外,容器环境与所在的 Host 环境是隔离的,就像虚拟机一样,但更快更简单。

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

只需要配置好标准的 runtime 环境,服务器就可以运行任何容器。这使得运维人员的工作变得更高效,

一致和可重复。容器消除了开发、测试、生产环境的不一致性。

How-容器如何工作的:

容器与虚拟机比较

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

容器由两部分组成:

  1. 应用程序本身

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

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

传统的虚拟化技术,比如 VMWare, KVM, Xen,目标是创建完整的虚拟机。为了运行应用,除了部署应

用本身及其依赖(通常几十 MB),还得安装整个操作系统(几十 GB)。

下图展示了二者的区别:

如图所示:

所有的容器共享同一个 Host OS,这使得容器在体积上要比虚拟机小很多。

另外启动容器不需要启动整个操作系统,容器部署和启动速度更快,开销更小,也更容易迁移。

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

级模拟,也不需要即时编译。

3.3 Docker介绍

Docker是基于Go语言实现的开源容器项目,诞生于2013年年初,最初发起者是dotCloud公司。Docker

自开源后受到广泛的关注和讨论,逐渐形成了围绕Docker容器的生态体系。由于Docker在业界造成的影

响力实在太大,dotCloud公司后来也直接改名为Docker Inc,并专注于 Docker相关技术和产品的开发。

Docker提供了各种容器管理工具(如分发、版本、移植等),让用户无需关注底层的操作,可以更简单

明了地管理和使用容器;其次,Docker引入分层文件系统构建和高效的镜像机制**降低了迁移难度,极大

地提升了用户体验。用户操作Docker容器就像操作应用自身一样简单。

简单地讲,可以将Docker容器理解为一种轻量级的沙盒 (sandbox)。每个容器内运行着一个应用,不

**同的容器相互隔离,容器之间也可以通过网络互相通信。**容器的创建和停止都十分快速,几乎跟创建和

终止原生应用一致;另外,容器自身对系统资源的额外需求也十分有限,远远低于传统虚拟机。很多时

候,甚至直接把容器当作应用本身也没有任何问题。

3.4 Docker核心概念

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

(1)Docker镜像

Docker镜像类似于虚拟机镜像,可以将它理解为一个只读的模板。例如,一个镜像可以包含一个基本的

操作系统环境,里面仅安装了Apache应用程序(或用户需要的其他软件)。可以把它称为一个Apache镜

像。镜像是创建Docker容器的基础。通过版本管理和增量的文件系统,Docker提供了一套十分简单的机

制创建和更新现有的镜像,用户甚至可以从网上下载一个已经做好的应用镜像,并直接使用。

(2)Docker容器

Docker容器类似于一个轻量级的沙箱,Docker利用容器来运行和隔离应用。容器是从镜像创建的应用运

行实例。可以将其启动、开始、停止、删除,而这些容器都是彼此相互隔离的、互不可见的。可以把容

器看做是一个简易版的Linux系统环境(包括root用户权限、进程空间、用户空间和网络空间等)以及运

行在其中的应用程序打包而成的盒子。

(3)Docker仓库

Docker仓库类似于代码仓库,它是Docker集中存放镜像文件的场所。仓库注册服务器存放着很多类镜

像,每类镜像包括多个镜像文件,通过不同的标签(tag)来进行区分。例如存放Ubuntu操作系统镜像

的分类中可能包括1804、1604、14.04、12.04等不同版本的镜像。

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

公开仓库(Public)

私有仓库(Private)

目前,最大的公开仓库是官方提供的Docker Hub,其中存放了数量庞大的镜像供用户下载。国内不少云

服务提供商(如时速云、阿里云等)也提供了仓库的本地源,可以提供稳定的国内访问。

当然,用户如果不希望公开分享自己的镜像文件,Docker也支持用户在本地网络内创建一个只能自己访

问的私有仓库。当用户创建了自己的镜像之后就可以使用push命令将它上传到指定的公有或者私有仓

库。这样用户下次在另外一台机器上使用该镜像时,只需要将其从仓库上pull下来就可以了。


3.5 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服务看起来是一个单独的应用程序;

3.6 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的请求。

  1. Docker Engine执行Docker内部的一系列工作,每一项工作都是以一个Job的形式的存在。

  2. Job的运行过程中,当需要容器镜像时,则从DockerRegistry中下载镜像,并通过镜像管理驱动

Graph driver将下载镜像以Graph的形式存储。

  1. 当需要为Docker创建网络环境时,通过网络管理驱动Networkdriver创建并配置Docker容器网络环

境。

  1. 当需要限制Docker容器运行资源或执行用户指令等操作时,则通过Exec driver来完成。

  2. Libcontainer是一项独立的容器管理包,Networkdriver以及Execdriver都是通过Libcontainer来实

现具体对容器进行的操作。

Docker 客户端

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

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

复制代码
[root@docker ~ 14:50:06]# 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 后台服务的方式运行。

复制代码
[root@docker ~ 15:13:22]# systemctl  status docker.service 
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disab>
   Active: active (running) since Mon 2026-07-27 14:49:12 CST; 25min ago
     Docs: https://docs.docker.com
 Main PID: 1220 (dockerd)
    Tasks: 10
   Memory: 118.4M
   CGroup: /system.slice/docker.service
           └─1220 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Jul 27 14:49:11 docker dockerd[1220]: time="2026-07-27T14:49:11.829661053+08:00" level=i>
Jul 27 14:49:11 docker dockerd[1220]: time="2026-07-27T14:49:11.883592914+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.260592073+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.488139725+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.774020707+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.886760384+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.904405964+08:00" level=i>
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.904549804+08:00" level=i>
Jul 27 14:49:12 docker systemd[1]: Started Docker Application Container Engine.
Jul 27 14:49:12 docker dockerd[1220]: time="2026-07-27T14:49:12.939291056+08:00" level=i

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 命令则是先下载镜像(如果本地没有),然后再启动容器。

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

3.7 Docker组件如何协作?

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

容器启动过程如下:

  1. Docker 客户端执行 docker run 命令。

  2. Docker daemon 发现本地没有 httpd 镜像。

  3. daemon 从 Docker Hub 下载镜像。

  4. 下载完成,镜像 httpd 被保存到本地。

  5. Docker daemon 启动容器。

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

复制代码
[root@docker ~ 15:14:41]# docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
httpd        latest    a06426a45b48   13 days ago   117MB

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 人员的幸福指数。

4.镜像

复制代码
[root@docker _data 18:02:30]# docker images
REPOSITORY               TAG       IMAGE ID       CREATED        SIZE
wordpress                latest    c7464401a233   2 days ago     762MB
<none>                   <none>    08b2b4bfdc71   8 days ago     115MB
mysql                    latest    d236310860c6   10 days ago    945MB
ubuntu-with-stress       latest    9d72c80dd63b   10 days ago    142MB
httpd                    latest    a06426a45b48   3 weeks ago    117MB
portainer/portainer-ce   latest    be26dc26896a   3 weeks ago    145MB
ubuntu                   latest    de7345b16e94   3 weeks ago    100MB
tomcat                   latest    a0a96d0e5ff5   4 weeks ago    422MB
busybox                  latest    c6348fa86ba0   2 months ago   4.45MB
registry                 2         26b2eb03618e   2 years ago    25.4MB
joinsunsoft/docker.ui    latest    a56148454e56   3 years ago    69MB

这个none是虚悬镜像吗

是的,REPOSITORYTAG 都为 <none> 的镜像,正是所谓的"虚悬镜像(Dangling Image)"。


🧠 什么是虚悬镜像?

虚悬镜像是指没有仓库名和标签的镜像。它们通常是 Docker 在构建或更新镜像时产生的中间残留层,或者是因为标签被覆盖/删除后留下的孤立镜像。

类比:就像电脑里下载了一半被中断的安装包,或者被删除了引用但文件本身还没被清理的"僵尸文件"。


🔍 虚悬镜像是怎么产生的?

场景 说明
镜像构建时中间层 docker build 构建时,有些临时层如果未被最终镜像引用,就会变成虚悬镜像。
重新构建同名镜像 当你用相同的 REPOSITORY:TAG 重新构建一个新镜像时,旧镜像的标签会被移除,变成虚悬镜像。
强制删除标签 执行 docker rmidocker tag 时,如果旧标签被移除,原镜像也可能变为虚悬镜像。

你的环境中很可能是因为你反复构建 wordpressmysql 镜像,旧版本没了标签,变成了 <none>


🧹 如何清理虚悬镜像?

复制代码
# 查看当前所有虚悬镜像
docker images -f "dangling=true"

# 删除所有虚悬镜像(释放磁盘空间)
docker image prune

# 强制删除所有未使用的镜像(包括虚悬镜像和未被容器引用的镜像)
docker image prune -a

⚠️ 注意

  • 虚悬镜像通常不会占用特别大的空间,也不会影响正常容器的运行。

  • 如果你的磁盘空间紧张,可以定期执行 docker image prune 清理。

  • 确认不需要再删除,因为这些镜像无法通过标签恢复,只能通过镜像 ID 重新打标签(如果还有缓存层)。


📌 一句话总结

<none>:<none> 镜像就是"无主孤儿",不影响使用,但建议定期清理,尤其是生产环境。 🚀

4.1 最小的镜像:

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

镜像的内部结构

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

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

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

了。

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

hello-world - 最小的镜像

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

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

复制代码
[root@docker ~ 15:15:14]# docker pull hello-world
Using default tag: latest
latest: Pulling from library/hello-world
4f55086f7dd0: Pull complete 
Digest: sha256:c3cbe1cc1aa588a64951ac6286e0df7b27fe2e6324b1001c619bb358770c0178
Status: Downloaded newer image for hello-world:latest
docker.io/library/hello-world:latest

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

复制代码
[root@docker ~ 15:19:47]# docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
httpd         latest    a06426a45b48   13 days ago    117MB
hello-world   latest    e2ac70e7319a   4 months ago   10.1kB

hello-world 镜像竟然还不到 14KB!

通过 docker run 运行。

复制代码
[root@docker ~ 15:19:54]# 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 内容如下:

只有短短三条指令。

  1. FROM scratch

此镜像是从白手起家,从 0 开始构建。

  1. COPY hello /

将文件"hello"复制到镜像的根目录。

  1. CMD "/hello"

容器启动时,执行 /hello

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

息。

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

hello-world 虽然是一个完整的镜像,但它并没有什么实际用途。通常来说,我们希望镜像能提供一个基

本的操作系统环境,用户可以根据需要安装和配置软件。这样的镜像我们称作 base 镜像。

我们下一节讨论 base 镜像。

4.2 base镜像:

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

base 镜像有两层含义:

  1. 不依赖其他镜像,从 scratch 构建。

  2. 其他镜像可以之为基础进行扩展。

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

等。

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

下载镜像:

复制代码
[root@docker ~ 15:20:15]# docker pull centos:7
7: Pulling from library/centos
2d473b07cdd5: Pull complete 
Digest: sha256:be65f488b7764ad3638f236b7b515b3678369a5124c47b8d32916d6487418ea4
Status: Downloaded newer image for centos:7
docker.io/library/centos:7

查看镜像信息:

复制代码
[root@docker ~ 15:24:53]# docker images 
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
httpd         latest    a06426a45b48   13 days ago    117MB
ubuntu        latest    de7345b16e94   13 days ago    100MB
hello-world   latest    e2ac70e7319a   4 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 的内容:(对应了base 镜像的特点:不依赖其他镜像,从 scratch 构建。)

第二行 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。

复制代码
[root@docker ~ 21:28:44]# uname -r
4.18.0-553.6.1.el8.x86_64

#Host OS kernel 为 4.18.0

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

复制代码
[root@docker ~ 15:21:43]# docker run -it ubuntu
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
ed819469700f: Pull complete 
a3679419df18: Pull complete 
Digest: sha256:3131b4cc82a783df6c9df078f86e01819a13594b865c2cad47bd1bca2b7063bb
Status: Downloaded newer image for ubuntu:latest
root@b7ebf699c7bc:/# uname -r
4.18.0-553.6.1.el8.x86_64     		#容器ubuntu用的内核就是docker host内核
root@b7ebf699c7bc:/# 

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

复制代码
[root@docker ~ 15:25:38]#  docker run -it centos:7
[root@48c9a28262f5 /]# uname -r
4.18.0-553.6.1.el8.x86_64
[root@48c9a28262f5 /]# 
  1. 容器只能使用 Host 的 kernel,并且不能修改。

所有容器都共用 host 的 kernel,在容器中没办法对 kernel 升级。如果容器对 kernel 版本有要求

(比如应用只能在某个 kernel 版本下运行),则不建议用容器,这种场景虚拟机可能更合适。

复制代码
通俗易懂理解:
核心一句话
容器没有独立内核!所有容器共享宿主机(Docker Host)的 Linux 内核。容器内部看到的发行版系统只是「用户空间文件」。
1. 两个概念分清
    (1). Linux Kernel(内核)
    操作系统底层核心,管理 CPU、内存、进程、网络、Namespace、Cgroup。
    内核属于宿主机,全局唯一。
    (2). Linux 发行版(Ubuntu / CentOS7 / Rocky)
    只是一套:命令行工具、软件包、库文件、配置(用户空间)。
    也就是我们常说的 Ubuntu 系统、CentOS 系统。

2.容器根本不带内核!
ubuntu 镜像、centos7 镜像,仅仅提供:yum/apt、命令、lib 库、系统文件(用户空间)
启动后直接调用宿主机内核运行。

3. 关键结论
(1). 所有容器共用宿主机同一个内核,容器无法更换、升级内核
你在容器里执行 yum update / apt upgrade,只能更新软件,更新不了内核。内核由宿主机控制。
(2). uname -r 查询到的永远是宿主机内核版本。
镜像只是 “一套工具文件”,不包含内核。

4. 容器 vs 虚拟机 最大区别(考点)
- 虚拟机(KVM/VMware)
虚拟机拥有完整独立内核;宿主机内核和虚拟机内核互不干扰。
你可以宿主机 CentOS8,虚拟机里面跑 CentOS7(内核 3.10)。
- Docker 容器
共享宿主机内核,只有独立的文件系统、网络、进程隔离;没有独立内核。

4.3 镜像的分层结构

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。在容器

层中,用户看到的是一个叠加之后的文件系统。

只有当需要修改时才复制一份数据,这种特性被称作 Copy-on-Write。可见,容器层保存的是镜像变化

的部分,不会对镜像本身进行任何修改。

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

改,所以镜像可以被多个容器共享

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

4.4 构建镜像

对于 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. 第一步, 运行容器
复制代码
[root@docker ~ 15:29:16]# docker run -it ubuntu
root@f683fb6b4cb5:/# 

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

  1. 安装 vim

确认 vim 没有安装。

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

安装 vim

复制代码
root@8dbdff6d3d88:/# apt-get update
root@8dbdff6d3d8:/# apt-get install -y vim
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

Geographic area: 5

Please select the city or region corresponding to your time zone.

  1. Aden        23. Dubai        45. Kuala_Lumpur  67. Seoul
  2. Almaty      24. Dushanbe     46. Kuching       68. Shanghai
  3. Amman       25. Famagusta    47. Kuwait        69. Singapore
  4. Anadyr      26. Gaza         48. Macau         70. Srednekolymsk
  5. Aqtau       27. Harbin       49. Magadan       71. Taipei
  6. Aqtobe      28. Hebron       50. Makassar      72. Tashkent
  7. Ashgabat    29. Ho_Chi_Minh  51. Manila        73. Tbilisi
  8. Atyrau      30. Hong_Kong    52. Muscat        74. Tehran
  9. Baghdad     31. Hovd         53. Nicosia       75. Tel_Aviv
  10. Bahrain    32. Irkutsk      54. Novokuznetsk  76. Thimphu
  11. Baku       33. Istanbul     55. Novosibirsk   77. Tokyo
  12. Bangkok    34. Jakarta      56. Omsk          78. Tomsk
  13. Barnaul    35. Jayapura     57. Oral          79. Ulaanbaatar
  14. Beirut     36. Jerusalem    58. Phnom_Penh    80. Urumqi
  15. Bishkek    37. Kabul        59. Pontianak     81. Ust-Nera
  16. Brunei     38. Kamchatka    60. Pyongyang     82. Vientiane
  17. Chita      39. Karachi      61. Qatar         83. Vladivostok
  18. Chongqing  40. Kashgar      62. Qostanay      84. Yakutsk
  19. Colombo    41. Kathmandu    63. Qyzylorda     85. Yangon
  20. Damascus   42. Khandyga     64. Riyadh        86. Yekaterinburg
  21. Dhaka      43. Kolkata      65. Sakhalin      87. Yerevan
  22. Dili       44. Krasnoyarsk  66. Samarkand
Time zone: 68
  1. 保存为新镜像

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

复制代码
[root@docker ~ 15:30:55]# docker ps
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS              PORTS     NAMES
f683fb6b4cb5   ubuntu    "/bin/bash"   About a minute ago   Up About a minute             crazy_poitras

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

f683fb6b4cb5 是新创建容器的ID

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

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

复制代码
[root@docker ~ 15:30:57]# docker commit crazy_poitras ubuntu-with-vim		 #cool_darwin是容器名,ubuntu-with-vim是新建的镜像名
sha256:7f73c9261bf9fac716d87635be669d1fa4c33d01a76a76699db9b0ec0b1a95e1

新镜像命名为 ubuntu-with-vim 。

查看新镜像的属性。

复制代码
[root@docker ~ 15:32:57]# docker images
REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
ubuntu-with-vim   latest    7f73c9261bf9   49 seconds ago   125MB
ubuntu            latest    de7345b16e94   13 days ago      100MB

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

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

复制代码
[root@docker ~ 15:33:02]# docker run -it ubuntu-with-vim #ubuntu-with-vim是新创建的镜像名
root@4d071cf3014f:/# which vim
/usr/bin/vim
root@4d071cf3014f:/# vim file1

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

原因如下:

  1. 这是一种手工创建镜像的方式,容易出错,效率低且可重复性弱。比如要在 debian base 镜像中也加入 vim,还得重复前面的所有步骤。

  2. 更重要的:使用者并不知道镜像是如何创建出来的,里面是否有恶意程序。也就是说无法对镜像进行审计,存在安全隐患。

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

原因是:即便是用 Dockerfile(推荐方法)构建镜像,底层也 docker commit 一层一层构建新镜像的。

学习 docker commit 能够帮助我们更加深入地理解构建过程和镜像的分层结构。

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

4.5 Dockerfile构建镜像

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

Dockerfile内容基础知识:

  1. 每条保留字指令都必须为大写字母且后面要跟随至少一个参数

  2. 指令按照从上到下,顺序执行

  3. #表示注释

  4. 每条指令都会创建一个新的镜像层并对镜像进行提交

常用参数:

复制代码
docker build -f [Dockerfile路径] [构建上下文路径]

第一个 Dockerfile

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

下面我们运行 docker build 命令构建镜像并详细分析每个细节。

复制代码
[root@docker ~ 15:33:02]# cd /root/		#生产环境一般新建一个目录,里面写Dockerfile,一个项目
[root@docker ~ 15:35:18]# vim Dockerfile
FROM ubuntu
RUN apt-get update && apt-get install -y vim

[root@docker ~ 15:35:40]# docker build  -t ubuntu-with-vim-dockerfile .

① 当前目录为 /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 查看镜像信息。

复制代码
[root@docker ~ 15:39:26]# docker images 
REPOSITORY                   TAG       IMAGE ID       CREATED         SIZE
ubuntu-with-vim-dockerfile   latest    970639bb09f8   8 seconds ago   216MB
ubuntu-with-vim              latest    7f73c9261bf9   9 minutes ago   125MB
ubuntu                       latest    de7345b16e94   13 days ago     100MB

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

在上面的构建过程中,我们要特别注意指令 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 下载的镜像会有这个问题。

4.6 镜像的缓存特性

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

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

建。

举例说明。

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

复制代码
[root@docker ~ 15:41:13]# pwd
/root
[root@docker ~ 15:47:20]# ls
anaconda-ks.cfg  Dockerfile
[root@docker ~ 15:47:23]# touch testfile
[root@docker ~ 15:47:57]# ls
anaconda-ks.cfg  Dockerfile  testfile
[root@docker ~ 15:47:58]# vim Dockerfile 
[root@docker ~ 15:48:11]# 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:5e510ea875dcc5182684d86a7353833dc0d07fdf4020f85540143fae320  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 的顺序:

虽然在逻辑上这种改动对镜像的内容没有影响,但由于分层的结构特性,Docker 必须重建受影响的镜像层。

复制代码
[root@docker ~ 15:48:46]#  vim Dockerfile
FROM ubuntu
COPY testfile /
RUN apt-get update && apt-get install -y vim
[root@docker ~ 15:48:49]# docker build -t ubuntu-with-vim-dockerfile-3 .

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

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

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

因为我的PC上有一个容器是基于httpd创建的

4.7 调试dockerfile

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

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

  1. 从 base 镜像运行一个容器。

  2. 执行一条指令,对容器做修改。

  3. 执行类似 docker commit 的操作,生成一个新的镜像层。

  4. Docker 再基于刚刚提交的镜像运行一个新容器。

  5. 重复 2-4 步,直到 Dockerfile 中的所有指令执行完毕。

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

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

执行 docker build :

复制代码
[root@docker ~ 15:48:49]# ls 		#查看下有没有Dockerfile和testfile
Dockerfile testfile
[root@docker ~ 15:50:24]# vim Dockerfile		#编辑Dockerfile,写入上图的内容
FROM busybox
RUN touch tmpfile
RUN /bin/bash -c "echo continue to build..."
COPY testfile /
[root@docker ~]# docker build -t image-debug . #基于刚才写的Dockerfile构建镜像
image-debug

Dockerfile 在执行第三步 RUN 指令时失败。我们可以利用busybox的镜像进行调试,方式是通过

docker run -it 启动镜像的一个容器。

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

复制代码
# 找出错误原因,修改错误
[root@docker ~ 15:41:05]#  vim Dockerfile
FROM busybox
RUN touch tmpfile
RUN /bin/sh -c "echo continue to builld..." #将错误的/bin/bash修改为正确的/bin/sh
COPY testfile /

[root@docker ~ 15:51:45]# docker build -t image-debug . #基于刚才写的Dockerfile构建镜像
image-debug

成功了!

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

4.7 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

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

复制代码
...
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:

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

完整的操作步骤如下:

复制代码
[root@docker ~ 15:51:49]# pwd      	#确定Dockerfile工作目录
/root
[root@docker ~ 15:59:13]# ls         	#当前/root目录下空的
[root@docker ~ 15:59:13]# touch tmpfile2     	#创建空文档tmpfile2
[root@docker ~ 15:59:52]# cp /etc/passwd .    	#将/etc/passwd文件拷贝到/root
[root@docker ~ 16:00:20]tar -cvzf passwd.tar.gz passwd 	#将passwd文件做出归档文件
passwd.tar.gz
passwd
[root@docker ~ 16:01:03]# rm passwd           #删除passwd文件
rm: remove regular file 'passwd'? y
[root@docker ~ 16:01:03]# 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 ~ 16:01:45]# ls	#最后目录中有三个文件
Dockerfile passwd.tar.gz tmpfile2

[root@docker ~ 16:02:08]# docker build -t my-image . 		#构建新镜像my-image
[+] Building 0.7s (10/10) FINISHED                                             docker:default
 => [internal] load build definition from Dockerfile                                     0.0s
 => => transferring dockerfile: 225B                                                     0.0s
 => [internal] load metadata for docker.io/library/busybox:latest                        0.5s
 => [internal] load .dockerignore                                                        0.0s
 => => transferring context: 2B                                                          0.0s
 => [1/5] FROM docker.io/library/busybox:latest@sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90  0.0s
 => [internal] load build context                                                        0.0s
 => => transferring context: 615B                                                        0.0s
 => CACHED [2/5] WORKDIR /testdir                                                        0.0s
 => CACHED [3/5] RUN touch tmpfile1                                                      0.0s
 => [4/5] COPY [tmpfile2,.]                                                              0.0s
 => [5/5] ADD [passwd.tar.gz,.]                                                          0.0s
 => exporting to image                                                                   0.0s
 => => exporting layers                                                                  0.0s
 => => writing image sha256:e2bd7edc6ce00d69aa90969d8c8939059e247d831c14ae2d80fb3775618  0.0s
 => => naming to docker.io/library/my-image                                              0.0s
		

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

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

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

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

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

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

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

文件 tmpfile1:由 RUN 指令创建。

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

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

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

4.8 RUN vs CMD vs ENTRYPOINT

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

讨论它们的区别。

简单的说:

  1. RUN 执行命令并创建新的镜像层,RUN 经常用于安装软件包。

  2. CMD 设置容器启动后默认执行的命令及其参数,但 CMD 能够被 docker run 后面跟的命令行参数替换。

  3. ENTRYPOINT 配置容器启动时运行的命令。

4.9 Dockerfile案例:配置SSH镜像

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

centos镜像

创建dockerfile

复制代码
[root@docker ~ 16:19:57]#vim centos.ssh.dockerfile
# 基础镜像:使用 centos 8.4.2105 官方镜像
FROM centos:8.4.2105

# 镜像作者信息(已过时指令,新标准建议使用 LABEL maintainer=xxx)
MAINTAINER gaoqiaodong

# 【构建阶段执行】替换yum源为阿里云centos-vault归档源
# minorver=8.4.2105 定义版本变量
# sed批量修改所有CentOS repo文件:
# 1. 注释 mirrorlist 网络镜像列表
# 2. 启用 baseurl,替换为阿里云 vault地址
# -i.bak 修改文件同时自动生成 .bak 备份文件
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

# 【构建阶段】安装openssh服务端
RUN yum install -y openssh-server

# 【构建阶段】生成ssh rsa主机密钥(ssh服务启动必需)
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key

# 【构建阶段】生成ssh ecdsa主机密钥
RUN ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key

# 【构建阶段】设置root密码为 huawei
# echo "用户名:密码" | chpasswd 批量设置系统用户密码
RUN echo "root:huawei" | chpasswd

# 声明容器对外暴露22端口(仅文档作用,不会自动映射端口)
EXPOSE 22

# 容器启动默认执行命令
# /usr/sbin/sshd -D:以前台方式运行sshd服务
# -D 代表前台运行;不加-D sshd后台启动,容器会立刻退出
CMD ["/usr/sbin/sshd","-D"]

构建镜像

复制代码
[root@docker ~ 16:20:25]# docker build -t centos:ssh -f centos.ssh.dockerfile  .
[+] Building 84.7s (10/10) FINISHED                                            docker:default
 => [internal] load build definition from centos.ssh.dockerfile                          0.0s
 => => transferring dockerfile: 1.52kB                                                   0.0s
 => [internal] load metadata for docker.io/library/centos:8.4.2105                       6.6s
 => [internal] load .dockerignore                                                        0.0s
 => => transferring context: 2B                                                          0.0s
 => [1/6] FROM docker.io/library/centos:8.4.2105@sha256:a27fd8080b517143cbbbab9dfb7c85  52.6s
 => => resolve docker.io/library/centos:8.4.2105@sha256:a27fd8080b517143cbbbab9dfb7c857  0.0s
 => => sha256:a1801b843b1bfaf77c501e7a6d3f709401a1e0c83863037fa3aab063a7fdb 529B / 529B  0.0s
 => => sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e 2.14kB / 2.14kB  0.0s
 => => sha256:a1d0c75327776413fa0db9ed3adcdbadedc95a662eb1d360dad82b 83.52MB / 83.52MB  46.7s
 => => sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b 762B / 762B  0.0s
 => => extracting sha256:a1d0c75327776413fa0db9ed3adcdbadedc95a662eb1d360dad82bb913f8a1  5.8s
 => [2/6] RUN minorver=8.4.2105 && sed -e "s|^mirrorlist=|#mirrorlist=|g" -e "s|^#baseu  0.9s
 => [3/6] RUN yum install -y openssh-server                                             23.4s
 => [4/6] RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key                             0.4s 
 => [5/6] RUN ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key                         0.3s 
 => [6/6] RUN echo "root:huawei" | chpasswd                                              0.3s 
 => exporting to image                                                                   0.1s 
 => => exporting layers                                                                  0.1s 
 => => writing image sha256:a25fb558ec7e0fe8e26a6465db9c9ef21e08690017373b86b5ed90dc7e0  0.0s 
 => => naming to docker.io/library/centos:ssh                                            0.0s

查看现象

复制代码
[root@docker ~ 16:21:55]# docker history  centos:ssh 
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
a25fb558ec7e   16 seconds ago   CMD ["/usr/sbin/sshd" "-D"]                     0B        buildkit.dockerfile.v0
<missing>      16 seconds ago   EXPOSE map[22/tcp:{}]                           0B        buildkit.dockerfile.v0
<missing>      16 seconds ago   RUN /bin/sh -c echo "root:huawei" | chpasswd…   1.77kB    buildkit.dockerfile.v0
<missing>      16 seconds ago   RUN /bin/sh -c ssh-keygen -t ecdsa -f /etc/s…   695B      buildkit.dockerfile.v0
<missing>      17 seconds ago   RUN /bin/sh -c ssh-keygen -t rsa -f /etc/ssh…   3.18kB    buildkit.dockerfile.v0
<missing>      17 seconds ago   RUN /bin/sh -c yum install -y openssh-server…   51.9MB    buildkit.dockerfile.v0
<missing>      40 seconds ago   RUN /bin/sh -c minorver=8.4.2105 && sed -e "…   17.6kB    buildkit.dockerfile.v0
<missing>      40 seconds ago   MAINTAINER gaoqiaodong                          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     

测试

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


#创建出来的容器
[root@docker ~ 16:22:48]# docker ps
CONTAINER ID   IMAGE        COMMAND               CREATED          STATUS         PORTS                                   NAMES
d993af1e6c31   centos:ssh   "/usr/sbin/sshd -D"   10 seconds ago   Up 9 seconds   0.0.0.0:2022->22/tcp, :::2022->22/tcp   sshtest


#ssh登录容器测试ssh,能够成功登录
[root@docker ~ 16:22:58]# ssh root@localhost -p 2022
The authenticity of host '[localhost]:2022 ([::1]:2022)' can't be established.
ECDSA key fingerprint is SHA256:rF0+riyj59aNd0if/ZjzYJk62q0+kftgj2uqn5fVgps.
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:    		#密码:huawei
"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: Mon Jul 27 08:23:42 UTC 2026 from 172.17.0.1 on ssh:notty
There was 1 failed login attempt since the last successful login.
[root@d993af1e6c31 ~]# 

4.10 Dockerfile案例:自定义httpd镜像

创建dockerfile

复制代码
[root@docker ~ 16:24:45]# vim httpd.dockerfile
# 指定基础镜像,基于 CentOS 8.4.2105
FROM centos:8.4.2105

# 标注镜像维护者(旧指令,推荐使用 LABEL maintainer="gaoqiaodong")
MAINTAINER gaoqiaodong

# 构建阶段执行sed命令替换yum源
# minorver=8.4.2105 定义版本变量
# -e "s|^mirrorlist=|#mirrorlist=|g":注释掉默认mirrorlist
# -e "s|xxx|xxx|g":替换baseurl为阿里云centos-vault归档源
# -i.bak:修改文件并自动生成后缀.bak备份文件
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

# 构建阶段安装httpd(Apache网页服务)
# yum clean all && rm -rf /var/cache/yum:清理yum缓存,减小镜像体积
RUN yum install -y httpd && yum clean all && rm -rf /var/cache/yum

# 将宿主机当前目录下 index.html 文件复制到容器网页根目录
COPY index.html /var/www/html/

# 声明容器打算开放80端口;仅作为文档说明,不会自动发布端口
EXPOSE 80

# 容器启动默认执行命令
# -DFOREGROUND:让httpd以前台方式运行
# Docker容器依赖前台进程维持存活,如果后台启动httpd,容器会立刻退出
# 使用CMD:docker run 镜像 后跟其他命令,可以直接覆盖本条指令
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]

[root@docker ~ 16:25:51]# echo Hello world > index.html

构建镜像

复制代码
[root@docker ~ 16:26:21]# docker build -t httpd:centos -f httpd.dockerfile .
[+] Building 25.4s (9/9) FINISHED                                              docker:default
 => [internal] load build definition from httpd.dockerfile                               0.0s
 => => transferring dockerfile: 1.43kB                                                   0.0s
 => [internal] load metadata for docker.io/library/centos:8.4.2105                       1.8s
 => [internal] load .dockerignore                                                        0.0s
 => => transferring context: 2B                                                          0.0s
 => [1/4] FROM docker.io/library/centos:8.4.2105@sha256:a27fd8080b517143cbbbab9dfb7c857  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            23.4s
 => [4/4] COPY index.html /var/www/html/                                                 0.0s
 => exporting to image                                                                   0.1s 
 => => exporting layers                                                                  0.1s 
 => => writing image sha256:0b3ffedc2da7e0f55ce189145da8f8bd441418ae8492c59b414f7f66612  0.0s 
 => => naming to docker.io/library/httpd:centos                                          0.0s 

查看现象

复制代码
[root@docker ~ 16:27:16]# docker history  httpd:centos                                        
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
0b3ffedc2da7   14 seconds ago   CMD ["/usr/sbin/httpd" "-DFOREGROUND"]          0B        buildkit.dockerfile.v0
<missing>      14 seconds ago   EXPOSE map[80/tcp:{}]                           0B        buildkit.dockerfile.v0
<missing>      14 seconds ago   COPY index.html /var/www/html/ # buildkit       12B       buildkit.dockerfile.v0
<missing>      14 seconds ago   RUN /bin/sh -c yum install -y httpd && yum c…   21.6MB    buildkit.dockerfile.v0
<missing>      5 minutes ago    RUN /bin/sh -c minorver=8.4.2105 && sed -e "…   17.6kB    buildkit.dockerfile.v0
<missing>      5 minutes ago    MAINTAINER gaoqiaodong                          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     

测试

复制代码
#基于刚才dockerfile创建的镜像httpd:centos创建容器myweb
root@docker ~ 16:27:30]# docker run -d -p 80:80 --name myweb httpd:centos 
4b5dffb378fb4762d2f79f0f5069bb8d87b36b194f4d15e423b70b39c0f8dc49

#创建出来的容器
[root@docker ~ 16:28:20]# docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                               NAMES
4b5dffb378fb   httpd:centos   "/usr/sbin/httpd -DF…"   24 seconds ago   Up 23 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   myweb


#访问测试
[root@docker ~ 16:28:22]# curl localhost
Hello world

4.11 Dockerfile案例:自定义mycentosjava8

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

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

编写Dockerfile文件

复制代码
[root@docker ~ 16:28:38]# mkdir myfile ; cd myfile
[root@docker myfile 16:29:46]# vim Dockerfile

# 基础镜像 centos8.4
FROM centos:8.4.2105

# 镜像维护者信息
MAINTAINER gaoqiaodong<6946630@qq.com>

# 定义环境变量MYPATH
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编辑器
RUN yum -y install vim

#安装ifconfig工具(net-tools)
RUN yum -y install net-tools

#安装32位兼容库
RUN yum -y install glibc.i686

# 创建java存放目录
RUN mkdir /usr/local/java

# ADD:将当前目录下jdk压缩包解压到容器/usr/local/java
# ADD特性:识别tar压缩包自动解压;源文件必须和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不能直接回车换行,变量赋值写在同一行
ENV CLASSPATH=$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib:$CLASSPATH
ENV PATH $JAVA_HOME/bin:$PATH

# 声明开放80端口(仅文档标识,不会自动端口映射)
EXPOSE 80

# ⚠️ Dockerfile多个CMD仅最后一条生效
# CMD echo $MYPATH
# CMD echo "success--------------ok"
CMD /bin/bash

# 将jdk-8u461-linux-x64.tar.gz与Dockerfile放到同一目录
[root@docker myfile 16:33:49]# ls
Dockerfile  jdk-8u162-linux-x64.tar.gz

# 构建镜像为centosjava8:461
[root@docker myfile 16:37:27]#  docker build -t centosjava8:461 .

查看构建的镜像

复制代码
[root@docker myfile 16:38:09]# docker images 
REPOSITORY                     TAG       IMAGE ID       CREATED             SIZE
centosjava                     461       a7f946045fd0   30 seconds ago      739MB
                

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

复制代码
[root@docker myfile 16:38:39]# docker run -it centosjava8:461 /bin/bash
[root@6b2767bcdf24 local]# pwd 		#测试了ENV和WORKDIR
/usr/local

[root@docker myfile 16:40:02]# java -version 		#测试java是否安装
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@docker myfile 16:40:05]# ifconfig 		#测试net-tools是否安装
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.3 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:ac:11:00:03 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@docker myfile 16:40:07]# vim file1 		#测试vim是否安装


docker run [选项] 镜像名[:标签] [启动命令] [参数]
-i:保持标准输入-t:分配伪终端
-d:后台运行-p:端口映射-v:数据卷挂载

4.11 镜像命名的最佳实践

我们已经学会构建自己的镜像了。接下来的问题是如何在多个 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:tag 即

054b8ac70e8010d90f2ac00ef29e6580.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。

4.12 使用公共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 上登录。

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

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

username/xxx:tag

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

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

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

Docker 会上传镜像的每一层。因为 gaoqd/httpd:v1 这个镜像实际上跟官方的 httpd 镜像一模一

样,Docker Hub 上已经有了全部的镜像层,所以真正上传的数据很少。同样的,如果我们的镜像

是基于 base 镜像的,也只有新增加的镜像层会被上传。如果想上传同一 repository 中所有镜像,

省略 tag 部分就可以了,例如:

docker push gaoqd/httpd

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

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

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

4.13 用公共Registry-华为云

登录华为云

选择区域,选择离自己近的数据中心,创建组织

接下来登录华为云,上传自己的镜像

复制登录指令

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

复制代码
[root@docker ~ 16:48:43]# docker login -u cn-east-3@HST3WCDKLC588QPATW0W -p efab95292dec87f88617304e6edb30b4b735b7e49f2f70eaef4b03126c02d05f 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

上传镜像

#命令格式

sudo docker tag {镜像名称}:{版本名称} swr.cn-east-3.myhuaweicloud.com/{组织名称}/{镜像名称}:{版本名称}

sudo docker push swr.cn-east-3.myhuaweicloud.com/{组织名称}/{镜像名称}:{版本名称}

复制代码
#刚才Dockerfile制作的httpd:centos镜像 用来上传
[root@docker ~ 18:11:06]# docker images  httpd:centos 
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
httpd        centos    0b3ffedc2da7   2 hours ago   253MB


#修改镜像名
[root@docker ~ 18:15:11]# docker tag httpd:centos swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1


[root@docker ~]# docker images
REPOSITORY TAG IMAGE ID
CREATED SIZE
httpd centos
973cac3a687f 4 hours ago 288MB
swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd v1
973cac3a687f 4 hours ago 288MB

#上传镜像
[root@docker ~ 18:16:55]# docker push swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1
The push refers to repository [swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd]
17d115a5cc74: Pushed 
ac73fb47255a: Pushed 
d0e3bb901750: Pushed 
74ddd0ec08fa: Layer already exists 
v1: digest: sha256:6cae8bed58c6f84a206104e28202e74af5140b9ae4a289aa688e75c9ad880765 size: 1155

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

下载自己上传的镜像

本地先把镜像删除再下载

复制代码
[root@docker ~ 18:17:43]# docker rmi swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1 

复制pull命令

复制代码
[root@docker ~ 18:20:34]# docker pull swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1
v1: Pulling from zhangyi/centos_httpd
a1d0c7532777: Already exists 
ea064a8bee05: Already exists 
867e1edde362: Already exists 
b0a315ce92ba: Already exists 
Digest: sha256:6cae8bed58c6f84a206104e28202e74af5140b9ae4a289aa688e75c9ad880765
Status: Downloaded newer image for swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1
swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd:v1

[root@docker ~ 18:20:53]# docker images 
REPOSITORY                                             TAG       IMAGE ID       CREATED        SIZE
swr.cn-east-3.myhuaweicloud.com/zhangyi/centos_httpd   v1        0b3ffedc2da7   2 
53MB

设置为公开

点击右上角**...**

4.14 搭建本地Registry

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

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

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

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

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

Registry

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

Docker 中运行自己的 registry。

  1. 启动 registry 容器。
复制代码
[root@docker ~ 18:24:00]# docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry  registry:2
Unable 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
1668dd2d22235fe27d1bbe9eb915d66eec9182bfd28435c83ff9fa261c200692

我们使用的镜像是 registry:2

-d 是后台启动容器。

-p 将容器的 5000 端口映射到 Host 的 5000 端口。5000 是 registry 服务端口。端口映射我们会

在容器网络章节详细讨论。

-v 将容器 /var/lib/registry 目录映射到 Host 的 /myregistry,用于存放镜像数据。 -v 的使用我们

会在容器存储章节详细讨论。

  1. 通过 docker tag 重命名镜像,使之与 registry 匹配。
复制代码
[root@docker ~ 18:24:15]# docker images
httpd                                                  latest    a06426a45b48   13 days ago    117MB

[root@docker ~ 18:24:32]# docker tag httpd:latest localhost:5000/httpd:v1
[root@docker ~ 18:25:55]# docker images
httpd                                                  latest    a06426a45b48   13 days ago    117MB
localhost:5000/httpd                                   v1        a06426a45b48   13 days ago    117MB

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

前面已经讨论了镜像名称由 repository 和 tag 两部分组成。而 repository 的完整格式为**:**

registry-host:port/username/xxx

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

  1. 通过 docker push 上传镜像
复制代码
[root@docker ~ 18:25:58]# docker push localhost:5000/httpd:v1
The push refers to repository [localhost:5000/httpd]
88b7a0afb755: Pushed 
fc946299f148: Pushed 
a06505022ffe: Pushed 
5f70bf18a086: Pushed 
b0343dd98f7c: Pushed 
f2ec4de84f55: Pushed 
v1: digest: sha256:bf1c467aceaf5892d068089dba79e2b871a117b7ce292560a50713d61264da23 size: 1572

[root@docker ~ 18:26:44]# curl http://localhost:5000/v2/_catalog
{"repositories":["httpd"]}
  1. 现在已经可通过 docker pull 从本地 registry 下载镜像了
复制代码
#下载之前先删除本地的镜像
[root@docker ~ 18:26:50]# docker rmi httpd:latest
Untagged: httpd:latest
Untagged: httpd@sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
[root@docker ~ 18:27:07]# docker rmi localhost:5000/httpd:v1
Untagged: localhost:5000/httpd:v1
Untagged: localhost:5000/httpd@sha256:bf1c467aceaf5892d068089dba79e2b871a117b7ce292560a50713d61264da23
Deleted: sha256:a06426a45b48be8136f40a688cd945dc13cb5854bb813488fc6f2b3aa6d64662
Deleted: sha256:0b8f21d9a53bafdbc99407bad15ef5568654aa7705f9b3cc6a3b162f3a7e9443
Deleted: sha256:bbe45e758a4c484664dc3bbebd5f9fb8611bc48c726ff765a42cb03ad21df469
Deleted: sha256:e5f04345ac56e341dabc2ef2784888f929bea74be38782b538c1d4e8f69edba4
Deleted: sha256:addb8b53bc50d5ab5fe1e862865c13500226f41a874eaa913ba27d21cfb47fd1
Deleted: sha256:3d622057b3031dc79cb3eb3449b1a39dc3210abe32d82b1bd1c57024eb7f89fe
Deleted: sha256:f2ec4de84f559f5c7be4233b589cdbdbb5507807e05621b77320edd55a1f2a0f


#从自建仓库下载镜像
[root@docker ~ 18:27:11]# docker pull localhost:5000/httpd:v1
v1: Pulling from httpd
f96fde5c7579: Pull complete 
39d55e966aeb: Pull complete 
4f4fb700ef54: Pull complete 
69b5b831a361: Pull complete 
ebd81352b960: Pull complete 
010f7d7e175f: Pull complete 
Digest: sha256:bf1c467aceaf5892d068089dba79e2b871a117b7ce292560a50713d61264da23
Status: Downloaded newer image for localhost:5000/httpd:v1
localhost:5000/httpd:v1


#验证从自建仓库下载的镜像
[root@docker ~ 18:27:26]# docker images localhost:5000/httpd
REPOSITORY             TAG       IMAGE ID       CREATED       SIZE
localhost:5000/httpd   v1        a06426a45b48   13 days ago   117MB

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

以上是搭建本地 registry 的简要步骤。当然 registry 也支持认证,https 安全传输等特性,具体可以参考

官方文档 Deprecated and retired Docker products and features | Docker Docs

4.15 企业级私有仓库Harbor(!!!)

我们如果需要部署一个私有镜像仓库来使用,最简单的就是 registry ,一行命令就可以运行在 Docker

中,但功能也比较弱,如果想要私有镜像仓库功能更丰富些,可以使用 Harbor。

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

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

ons)仓库。

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

安装

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

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

复制代码
[root@docker ~ 18:27:40]# ls
harbor-offline-installer-v2.9.1.tgz

#执行下面命令进行解压
[root@docker ~ 18:29:48]# 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 ~ 18:30:06]# mkdir /opt/harbor
[root@docker ~ 18:30:32]# mv harbor/* /opt/harbor/


#导入Harbor镜像
[root@docker ~]# cd /opt/harbor/
[root@docker harbor 18:30:57]# docker load -i harbor.v2.9.1.tar.gz 


#修改 Harbor 配置文件
[root@docker harbor 18:32:12]# cd /opt/harbor/
[root@docker harbor 18:32:45]# cp -ar harbor.yml.tmpl harbor.yml
[root@docker harbor 18:32:58]# ls
common.sh  harbor.v2.9.1.tar.gz  harbor.yml  harbor.yml.tmpl  install.sh  LICENSE  prepare
[root@docker harbor 18:32:58]# vim harbor.yml

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

https: #注释掉

harbor_admin_password: harbor登录密码

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

复制代码
[root@docker harbor 18:34:09]# ./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
[root@docker harbor 18:34:28]# ./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.3s 
 ✔ Container harbor-log         Started                                               0.5s 
 ✔ Container redis              Started                                               2.6s 
 ✔ Container registry           Started                                               2.4s 
 ✔ Container registryctl        Started                                               2.7s 
 ✔ Container harbor-portal      Started                                               2.7s 
 ✔ Container harbor-db          Started                                               2.5s 
 ✔ Container harbor-core        Started                                               3.1s 
 ✔ Container nginx              Started                                               4.1s 
 ✔ Container harbor-jobservice  Started                                               4.1s 
✔ ----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 文件中,执行下面命令进行配置

复制代码
[root@docker ~ 18:35:42]# vim /etc/docker/daemon.json
{
"insecure-registries": ["192.168.108.30"],
"registry-mirrors": [
"https://054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com" ]
}

[root@docker ~ 18:35:43]#  systemctl restart docker
#重新执行安装命令
[root@docker ~ 18:35:47]# ./install.sh

#登录服务器
[root@docker ~ 18:36:45]# docker login 192.168.108.30
Username: images_admin
Password:Cloud12#$
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文件夹
[root@docker ~ 18:36:45]# cd ~/.docker/
[root@docker ~ 18:36:58]# pwd
/root/.docker
[root@docker .docker]# cat config.json
{
"auths": {
"192.168.108.30": {
"auth": "aW1hZ2VzX2FkbWluOkNsb3VkMTIjJA=="
},
"swr.cn-north-4.myhuaweicloud.com": {
"auth":
"Y24tbm9ydGgtNEBMUVhBVjU4MENES1A3SUoxTDREMzpmM2NlMTJlODY4ODVkN2JkZWZlYmFiMWI1N2Rm
ZDBiYjJiM2MxZTEyNjUxYWQ1ZTk1ODY1Nzg4MTczYzI4OTJj"
}
}
}

上传镜像

复制代码
[root@docker ~ 18:37:02]# docker tag nginx:latest 192.168.108.30/cloud/nginx:latest
[root@docker ~ 18:37:02]# docker push 192.168.108.30/cloud/nginx:latest
The push refers to repository [192.168.108.30/cloud/nginx]
11de3d47036d: Pushed
16907864a2d0: Pushed
2bdf51597158: Pushed
0fc6bb94eec5: Pushed
eda13eb24d4c: Pushed
67796e30ff04: Pushed
8e2ab394fabf: Pushed
latest: digest:
sha256:596c783ac62b9a43c60edb876fe807376cd5022a4e25e89b9a9ae06c374299d4 size:
177

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

下载镜像

复制代码
[root@docker ~ 18:37:03]# docker pull 192.168.108.30/cloud/nginx
Using default tag: latest
latest: Pulling from cloud/nginx
a2318d6c47ec: Pull complete
095d327c79ae: Pull complete
bbfaa25db775: Pull complete
7bb6fb0cfb2b: Pull complete
0723edc10c17: Pull complete
24b3fdc4d1e3: Pull complete
3122471704d5: Pull complete
Digest: sha256:596c783ac62b9a43c60edb876fe807376cd5022a4e25e89b9a9ae06c374299d4
Status: Downloaded newer image for 192.168.108.30/cloud/nginx:latest
192.168.108.30/cloud/nginx:latest

卸载harbor

清理容器

复制代码
[root@docker ~ 18:37:40]# cd /opt/harbor/
[root@docker harbor 18:37:52]# 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 

清理镜像

复制代码
[root@docker harbor 18:38:24]# docker images |grep harbor|awk '{print $1":"$2}' | xargs docker rmi

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

复制代码
[root@docker harbor]# rm -rf /data

删除软件包

复制代码
[root@docker harbor 18:38:48]#  cd
[root@docker ~ 18:38:55]# ls

anaconda-ks.cfg harbor harbor-offline-installer-v2.9.1.tgz
[root@docker ~ 18:38:55]# rm -f harbor-offline-installer-v2.9.1.tgz
[root@docker ~ 18:39:00]# rm -rf /opt/harbor/

4.16 Docker镜像小结

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

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

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

images 显示镜像列表

history 显示镜像构建历史

commit 从容器创建新镜像

build 从 Dockerfile 构建镜像

tag 给镜像打 tag

pull 从 registry 下载镜像

push 将 镜像 上传到 registry

rmi 删除 Docker host 中的镜像

search 搜索 Docker Hub 中的镜像

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

rmi

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

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

busybox镜像有三个 tag:

复制代码
[root@docker ~ 18:39:03]#  docker images busybox
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 6fd955f66c23 16 months ago 4.26MB
[root@docker ~ 18:39:39]# docker tag busybox:latest busybox:v1
[root@docker ~ 18:39:41]# docker tag busybox:latest busybox:v2
[root@docker ~ 18:39:42]# docker images busybox
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest 6fd955f66c23 16 months ago 4.26MB
busybox v1 6fd955f66c23 16 months ago 4.26MB
busybox v2 6fd955f66c23 16 months ago 4.26MB

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

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

search

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

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

保存本地镜像为文件-save

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

复制代码
[root@docker ~ 18:39:42]# docker info | grep 'Storage Driver'
 Storage Driver: overlay2

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

复制代码
[root@docker ~ 18:40:42]# 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



#示例:
[root@docker ~ 18:41:54]# docker save  httpd -o httpd.tar    		 #httpd是镜像名 httpd.tar是要保存的文件名
[root@docker ~ 18:42:20]# ls
    httpd.tar       

root@docker ~ 18:43:31]# docker save httpd hello-world -o images.tar 	#将httpd和hello-world镜像一起保存为images.tar

[root@docker ~ 18:43:40]# ls

将本地镜像文件导入本地-load

复制代码
[root@docker ~ 18:43:42]# 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


#示例:删除本地镜像,并导入本地镜像文件
[root@docker ~ 18:44:04]# docker images
REPOSITORY                                             TAG       IMAGE ID       CREATED        SIZE
httpd                                                  latest    a06426a45b48   13 days ago    117MB
hello-world                                            latest    e2ac70e7319a   4 months ago   10.1kB
[root@docker ~ 18:44:29]# docker rmi httpd:latest hello-world:latest
Untagged: httpd:latest
Untagged: httpd@sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
Untagged: hello-world:latest
Untagged: hello-world@sha256:c3cbe1cc1aa588a64951ac6286e0df7b27fe2e6324b1001c619bb358770c0178
Deleted: sha256:e2ac70e7319a02c5a477f5825259bd118b94e8b02c279c67afa63adab6d8685b
Deleted: sha256:897b3f2a7c1bc2f3d02432f7892fe31c6272c521ad4d70257df624504a3238b4
[root@docker ~ 18:45:25]# docker images 		 #本地没有镜像
REPOSITORY TAG IMAGE ID CREATED SIZE

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

导入镜像

复制代码
[root@docker ~ 18:45:58]# docker load -i images.tar
Loaded image: httpd:latest
897b3f2a7c1b: Loading layer  11.78kB/11.78kB
Loaded image: hello-world:latest

[root@docker ~ 18:46:10]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
httpd latest 90f191b9781e 11 days ago 148MB
hello-world latest 74cc54e27dc4 6 months ago 10.1kB

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

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

5. 容器

5.1 如何运行容器?

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

运行容器

docker run=docker create + docker start

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

执行的命令:

  1. CMD 指令。

  2. ENTRYPOINT 指令。

  3. 在 docker run 命令行中指定。

例如下面的例子:

复制代码
[root@docker ~ 18:46:44]# docker create ubuntu 		#使用ubuntu镜像创建容器
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
ed819469700f: Already exists 
a3679419df18: Already exists 
Digest: sha256:3131b4cc82a783df6c9df078f86e01819a13594b865c2cad47bd1bca2b7063bb
Status: Downloaded newer image for ubuntu:latest
175ab2d5ebc953d19f5c2bc8392a8f360324dab065806ca22c0f127462967443		#新创建的容器长ID


[root@docker ~ 15:51:58]# docker ps -a 		#create的容器状态时Created
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS    PORTS     NAMES
175ab2d5ebc9   ubuntu    "/bin/bash"   8 seconds ago   Created             suspicious_chaplygin

quizzical_goldwasser
[root@docker ~ 18:47:50]# docker start 175ab2d5		#启动容器,刚创建的容器ID
175ab2d5
 		
		
[root@docker ~ 18:49:08]# docker ps -a 			#查看容器状态,启动了又退出了
CONTAINER ID   IMAGE     COMMAND       CREATED              STATUS                      PORTS     NAMES
175ab2d5ebc9   ubuntu    "/bin/bash"   About a minute ago   Exited (0) 18 seconds ago             suspicious_chaplygin

#使用ubuntu镜像创建容器并执行pwd命令
[root@docker ~ 18:49:27]# docker run ubuntu pwd 
/
[root@docker ~]#

容器启动时执行 pwd ,返回的 / 是容器中的当前目录。 执行 docker ps 或 docker container ls

可以查看 Docker host 中当前运行的容器:

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

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

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

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

让容器长期运行

如何让容器保存运行呢?

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

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

令:

复制代码
[root@docker ~ 18:50:24]# docker run ubuntu /bin/bash -c "while true ; do sleep 1 ; echo hahaha; done"

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

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

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

复制代码
[root@docker ~]# docker run -d ubuntu /bin/bash -c "while true ; do sleep 1 ;
echo hahaha; done"
2a0bfa267fe146753b4fc8b23d55b08fbe3a5f9b5e093de6885133f6bbd20c56
[root@docker ~]#

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

docker ps 查看容器:

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

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

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

docker 会自动为容器分配名字。

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

复制代码
[root@docker ~ 15:51:58]# docker ps -a 
CONTAINER ID   IMAGE     COMMAND       CREATED         STATUS    PORTS     NAMES
175ab2d5ebc9   ubuntu    "/bin/bash"   8 seconds ago   Created             suspicious_chaplygin
[root@docker ~ 18:47:58]# docker stop 175ab
175ab

这里我们就是通过 "短ID" 指定了要停止的容器。

通过 while 启动的容器虽然能够保持运行,但实际上没有干什么有意义的事情。容器常见的用途是运行

后台服务,例如前面我们已经看到的 http server:

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

docker history 可知这个命令是通过 CMD 指定的。

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

容器内部。

5.2 两种进入容器的方法

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

器:attach 和 exec。

1.docker attach

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

复制代码
[root@docker ~ 18:51:27]# docker run -d ubuntu /bin/bash -c "while true ; do sleep 1 ; echo I_am_in_container ; done"
f68c12f90957617d8389766dc64ca6fb54a6d721de3334a891c4e28512cdfbf4

[root@docker ~ 18:52:10]# docker attach  f68c12f90957617d8389766dc64ca6fb54a6d721de3334a891c4e28512cdfbf4
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 终端。

2.docker exec

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

说明如下:

① -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 命令:

复制代码
[root@docker ~ 18:53:59]# docker logs -f dc508b94447f
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 类似,能够持续打印输出。

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

5.3 运行容器的最佳实践

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

  1. 服务类容器以 daemon 的形式运行,对外提供服务。比如 web server,数据库等。通过 -d 以后

台方式启动这类容器是非常合适的。如果要排查问题,可以通过 exec -it 进入容器。

  1. 工具类容器通常给能我们提供一个临时的工作环境,通常以 run -it 方式运行,比如:
复制代码
[root@docker ~ 18:54:22]# docker run -it busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
b05093807bb0: Already exists 
Digest: sha256:fd8d9aa63ba2f0982b5304e1ee8d3b90a210bc1ffb5314d980eb6962f1a9715d
Status: Downloaded newer image for busybox:latest
/ # wget www.baidu.com
Connecting to www.baidu.com (180.101.51.73:80)
saving to 'index.html'
index.html           100% |*****************************************|  2381  0:00:00 ETA
'index.html' saved
/ # exit
[root@docker ~ 18:54:58]# 

运行 busybox, run -it 的作用是在容器启动后就直接进入。我们这里通过 wget 验证了在容器中访问

internet 的能力。执行 exit 退出终端,同时容器停止。

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

容器运行小结

容器运行相关的知识点:

  1. 当 CMD 或 Entrypoint 或 docker run 命令行指定的命令运行结束时,容器停止。

  2. 通过 -d 参数在后台启动容器。

  3. 通过 exec -it 可进入容器并执行命令。

指定容器的三种方法:

  1. 短ID。

  2. 长ID。

  3. 容器名称。 可通过 --name 为容器命名。重命名容器可执行 docker rename 。

容器按用途可分为两类:

  1. 服务类的容器。

  2. 工具类的容器。

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

5.4 容器常用操作

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

stop/start/restart 容器

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

容器在 docker host 中实际上是一个进程, docker stop 命令本质上是向该进程发送一个 SIGTERM 信

号。如果想快速停止容器,可使用 docker kill 命令,其作用是向容器进程发送 SIGKILL 信号。

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

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

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

容器可能会因某种错误而停止运行。对于服务类容器,我们通常希望在这种情况下容器能够自动重启。

启动容器时设置 --restart 就可以达到这个效果。

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

程)会重启。

**--restart=always 意味着无论容器因何种原因退出(包括正常退出),就立即重启。**该参数的形式还

可以是 --restart=on-failure:3 ,意思是如果启动进程退出代码非0,则重启容器,最多重启3次

pause/unpause 容器

有时我们只是希望暂时让容器暂停工作一段时间,比如要对容器的文件系统打个快照,或者 dcoker host

需要使用 CPU,这时可以执行 docker pause 。

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

删除容器

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

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

除。

复制代码
[root@docker ~]# docker rm 35af7150bd17 9769bb915803
35af7150bd17
9769bb915803

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

docker rm -f $(docker ps -aq -f status=exited)

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

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

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

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

了容器各种状态之间是如何转换的。

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

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

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

② docker start 将以后台方式启动容器。 docker run 命令实际上是 docker create 和

docker start 的组合。

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

退出包括正常退出或者非正常退出。这里举了两个例子:启动进程正常退出或发生 OOM,此时

docker 会根据 --restart 的策略判断是否需要重启容器。但如果容器是因为执行 docker stop

或 docker kill 退出,则不会自动重启。

复制代码
#基于httpd镜像创建容器httpd11,重启策略配置为always
[root@docker ~ 18:54:58]# docker run -d --restart=always --name httpd11 httpd
Unable to find image 'httpd:latest' locally
latest: Pulling from library/httpd
062e450697fa: Pull complete 
39d55e966aeb: Pull complete 
4f4fb700ef54: Pull complete 
69b5b831a361: Pull complete 
ebd81352b960: Pull complete 
010f7d7e175f: Pull complete 
Digest: sha256:305fd8326a27a137fedb8900f26375699ab233cc37793f35cf5d7c65671fb252
Status: Downloaded newer image for httpd:latest
4499d8d36ba19f4c5f223be117742af22cd657c03e46af56ff573f61c213c8e5

#查看发现多了个httpd11容器
[root@docker ~ 18:58:12]# docker ps 
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS          PORTS     NAMES
4499d8d36ba1   httpd     "httpd-foreground"   33 seconds ago   Up 32 seconds   80/tcp    httpd11


#登陆到容器httpd11,然后执行ctrl_c退出容器(会触发--restart=always参数)
[root@docker ~ 18:59:02]# docker attach httpd11
^C[Mon Jul 27 10:59:40.968680 2026] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down

#发现httpd11重启了
[root@docker ~ 18:59:47]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS          PORTS     NAMES
4499d8d36ba1   httpd     "httpd-foreground"   2 minutes ago   Up 23 seconds   80/tcp    httpd11


#管理员停止httpd11(不会触发重启)
[root@docker ~ 19:00:04]# docker stop httpd11 
httpd11


#发现httpd11没有重启
[root@docker ~ 19:00:20]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS                      PORTS     NAMES
4499d8d36ba1   httpd     "httpd-foreground"   2 minutes ago   Exited (0) 13 seconds ago             httpd11

5.6 限制容器对内存的使用

1.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

复制代码
[root@docker ~ 19:00: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
[root@docker ~ 19:06:25]# docker build -t ubuntu-with-stress .

一个 docker host 上会运行若干容器,每个容器都需要 CPU、内存和 IO 资源。对于 KVM,VMware 等虚拟化技术,用户可以控制分配多少 CPU、内存资源给每个虚拟机。对于容器,Docker 也提供了类似的机制避免某个容器因占用太多资源而影响其他容器乃至整个 host 的性能。

2.内存限额

与操作系统类似,容器可使用的内存包括两部分:物理内存和 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镜像来学习如何为容器分配内存。该镜像可用于对容器执行压力测试。执行如下命令:

复制代码
[root@docker ~ 19:08:48]#  docker run -it -m 200M --memory-swap=300M ubuntu-with-stress --vm 1 --vm-bytes 280M -v


--vm 1 :启动 1 个内存工作线程。
--vm-bytes 280M :每个线程分配 280M 内存。

运行结果如下:

因为 280M 在可分配的范围(300M)内,所以工作线程能够正常工作,其过程是:

  1. 分配 280M 内存。

  2. 释放 280M 内存。

  3. 再分配 280M 内存。

  4. 再释放 280M 内存。

  5. 一直循环......

如果让工作线程分配的内存超过 300M,结果如下:

分配的内存超过限额,stress 线程报错,容器退出。

如果在启动容器时只指定 -m 而不指定 --memory-swap ,那么 --memory-swap 默认为 -m 的两倍,

比如:docker run -it -m 200M ubuntu-with-stress

容器最多使用 200M 物理内存和 200M swap。

5.7 限制容器对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:
复制代码
[root@docker ~ 19:08:53]#  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 的数量。

  1. 再开一个窗口,启动 container_B,cpu share 为 512:
复制代码
[root@docker ~ 19:09:21]# docker run --name "container_B" -it -c 512 ubuntu-withstress --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 ~ 19:09:23]#  cat /sys/fs/cgroup/cpu/docker/<容器长ID>/cpu.shares
512
  1. 在 host 中执行 top ,查看容器对 CPU 的使用情况:

因为我们host是4核所以开启了4个进程,为的就是充分让系统资源变得紧张,只有这样竞争资源,

我们设定的资源比例才可以显现出来,如果只运行一个进程,他们会自动分配到空闲的CPU,这样

比例就无法看出来。目前可以看到总比例是2:1。

container_A 消耗的 CPU 是 container_B 的两倍。

再开一个窗口

复制代码
[root@docker ~ 19:09:30]#  docker stats
  1. 现在暂停 container_A:
复制代码
[root@docker ~ 19:09:46]# docker pause container_A
container_A
  1. top 显示 container_B 在 container_A 空闲的情况下能够用满整颗 CPU:

5.8 export和import容器

export-容器导出

将容器导出为一个tar包

复制代码
[root@docker ~ 19:17:36]# 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

不管此时这个容器是否处于运行状态,都可以导出为文件。

示例:

复制代码
# 创建容器httpd1用于测试
[root@docker ~]# docker run -d --name httpd1 httpd
e4f0a329c4df50ef0afb0bf21e22edc20e5a24c03ae64c6340aa2992d6e32525
[root@docker ~ 19:19:04]# docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS          PORTS     NAMES
5a4d6f0a363e   httpd     "httpd-foreground"   49 seconds ago   Up 48 seconds   80/tcp    httpd1

[root@docker ~ 19:19:18]# docker export httpd1 -o myhttpd.tar
[root@docker ~ 19:19:59]# ls
anaconda-ks.cfg        harbor            images.tar  myhttpd.tar    tmpfile2

import-容器tar包导入

import将export导出的tar包导入成为镜像

复制代码
[root@docker ~ 19:20:00]# 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



#示例:
[root@docker ~ 19:20:42]# docker import  myhttpd.tar 
sha256:08b2b4bfdc71a1fe6829b6fa452380b51fa24bda81a253d1d742df9884a29053
[root@docker ~ 19:21:06]# docker import myhttpd.tar myweb:v1
sha256:2d575a2e5f5384182ed6c1a54a9e4923a3997efbcbc2801515b7ce20fb5fa36b
[root@docker ~ 19:21:32]# docker images 
REPOSITORY           TAG       IMAGE ID       CREATED          SIZE
myweb                v1        2d575a2e5f53   8 seconds ago    115MB
<none>               <none>    08b2b4bfdc71   34 seconds ago   115MB
ubuntu-with-stress   latest    9d72c80dd63b   2 days ago       142MB
httpd                latest    a06426a45b48   2 weeks ago      117MB
ubuntu               latest    de7345b16e94   2 weeks ago      100MB
busybox              latest    c6348fa86ba0   2 months ago     4.45MB
registry             2         26b2eb03618e   2 years ago      25.4MB

docker save 和 docker export 对比:

docker save :将镜像保存为文件,save会保存该镜像的所有元数据和历史记录。

docker export :将容器导出为文件,文件会丢失所有元数据和历史记录,仅保存容器当时的状

态,再次导入会当作全新的镜像。

思考: docker export 导出的文件是否可以使用 docker load 导入?

复制代码
[root@docker ~ 19:21:40]# docker load  -i myhttpd.tar 
open /var/lib/docker/tmp/docker-import-1442784263/boot/json: no such file or directory

思考:docker save导出的文件是否可以使用docker import导入?

提示:我们可以将 docker save 保存出来的文件解压后分析文件结构。

5.9 实现容器的底层技术

为了更好地理解容器的特性,本节我们将讨论容器的底层实现技术。

cgroup 和 namespace 是最重要的两种技术。cgroup 实现资源限额namespace 实现资源隔离

1.cgroup

cgroup 全称 Control Group。Linux 操作系统通过 cgroup 可以设置进程使用 CPU、内存 和 IO 资源的限额。相信你已经猜到了:前面我们看到的 --cpu-shares 、 -m 、 --device-write-bps 实际上就是

在配置 cgroup。

cgroup 到底长什么样子呢?我们可以在 /sys/fs/cgroup 中找到它。还是用例子来说明,启动一个容器,设置 --cpu-shares=512 :

2.namespace

在每个容器中,我们都可以看到文件系统,网卡等资源,这些资源看上去是容器自己的。拿网卡来说,

每个容器都会认为自己有一块独立的网卡,即使 host 上只有一块物理网卡。这种方式非常好,它使得容器更像一个独立的计算机。

Linux 实现这种方式的技术是 namespace。namespace 管理着 host 中全局唯一的资源,并可以让每个容器都觉得只有自己在使用它。换句话说,namespace 实现了容器间资源的隔离

Linux 使用了六种 namespace,分别对应六种资源:Mount、UTS、IPC、PID、Network 和 User,下面我们分别讨论。

(1)Mount namespace

Mount namespace 让容器看上去拥有整个文件系统。

容器有自己的 / 目录,可以执行 mount 和 umount 命令。当然我们知道这些操作只在当前容器中生

效,不会影响到 host 和其他容器。

(2)UTS namespace

简单的说,UTS namespace 让容器有自己的 hostname。 默认情况下,容器的 hostname 是它的短

ID,可以通过 -h 或 --hostname 参数设置。

(3)IPC namespace

IPC namespace 让容器拥有自己的共享内存和信号量(semaphore)来实现进程间通信,而不会与

host 和其他容器的 IPC 混在一起。

(4)PID namespace

我们前面提到过,容器在 host 中以进程的形式运行。例如当前 host 中运行了两个容器:

复制代码
[root@docker ~ 19:24:16]# 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 就只能看到自己的进程了:

复制代码
[root@docker ~ 19:24:03]# 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 提供的功能。

(5)Network namespace

Network namespace 让容器拥有自己独立的网卡、IP、路由等资源。我们会在后面网络章节详细讨论。

(6)User namespace

User namespace 让容器能够管理自己的用户,host 不能看到容器中创建的用户

复制代码
[root@docker ~ 19:23:53]# docker exec -it a80f1f8b692c bash
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# useradd gaoqd #容器中创建用户gaoqd
root@a80f1f8b692c:/#
root@a80f1f8b692c:/# exit
exit

[root@docker ~ 19:25:37]#  su - gaoqd #宿主机中并没有用户gaoqd
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 :从磁盘中删除容器

6.网络

6.1 none和host网络的适用场景

本章开始讨论 Docker 网络。

我们会首先学习 Docker 提供的几种原生网络,以及如何创建自定义网络。然后探讨容器之间如何通信,以及容器与外界如何交互。

Docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络,本章重点讨论前一种。对于更为复杂的多 host 容器网络,我们会在后面进阶技术章节单独讨论。

Docker 安装时会自动在 host 上创建三个网络,我们可用 docker network ls 命令查看:

复制代码
[root@docker ~ 18:39:45]# docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
95d565974ef0   bridge    bridge    local
091f3b4addce   host      host      local
235a95facc89   none      null      local

下面我们分别讨论它们。

1.none 网络

none网络的driver类型是null,IPAM字段为空。挂在none网络上的容器只有lo,无法与外界通信。

故名思议,none 网络就是什么都没有的网络。挂在这个网络下的容器除了 lo,没有其他任何网卡。容器创建时,可以通过 --network=none 指定使用 none 网络。

复制代码
[root@docker ~ 18:41:02]# 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 
f88c7e792cbe
/ # 

我们不禁会问,这样一个封闭的网络有什么用呢?

其实还真有应用场景。封闭意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用 none 网络。比如某个容器的唯一用途是生成随机密码,就可以放到 none 网络中避免密码被窃取。

当然大部分容器是需要网络的,我们接着看 host 网络。

2.host 网络

挂在host网络上的容器共享宿主机的network namespace。即容器的网络配置与host网络配置完全一

样。

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 完全一样。可以通过 --network=host 指定使用 host 网络。

复制代码
[root@docker ~ 18:44:58]# 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:20:8c:f5 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 10.1.8.25/24 brd 10.1.8.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe20:8cf5/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:0a:e5:d4:6d 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


[root@docker ~ 18:45:01]# docker run -it --network=host 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
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
    link/ether 00:0c:29:20:8c:f5 brd ff:ff:ff:ff:ff:ff
    inet 10.1.8.25/24 brd 10.1.8.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe20:8cf5/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue 
    link/ether 02:42:0a:e5:d4:6d 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
/ # hostname 				#容器主机与宿主机相同
docker
/ # 

在容器中可以看到 host 的所有网卡,并且连 hostname 也是 host 的。host 网络的使用场景又是什么呢?

直接使用 Docker host 的网络最大的好处就是性能,如果容器对网络传输效率有较高要求,则可以选择host 网络。当然不便之处就是牺牲一些灵活性,比如要考虑端口冲突问题,Docker host 上已经使用的端口就不能再用了。

Docker host 的另一个用途是让容器可以直接配置 host 网路。比如某些跨 host 的网络解决方案,其本身也是以容器方式运行的,这些方案需要对网络进行配置,比如管理 iptables。

6.2学容器必须懂brige网络

上一节我们讨论了 none 和 host 类型的容器网络,本节学习应用最广泛也是默认的 bridge 网络。

Docker 安装时会创建一个 命名为 docker0 的 linux bridge,实际上它是 Linux 的一个 bridge (网桥),可以理解为一个软件交换机,它会在挂载到它的网口之间进行转发。如果不指定 --network ,创建的容器默认都会挂到docker0 上。

Docker 就创建了在主机和所有容器之间一个虚拟共享网络 当创建一个 Docker 容器的时候,同时会创建了一对 veth pair 接口(当数据包发送到一个接口时,另外一个接口也可以收到相同的数据包), 这对接口一端在容器内即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethAQI2QT)

通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。

下面演示:

先配置yum源用于安装软件

复制代码
[root@docker ~ 18:47:48]# cd /etc/yum.repos.d/
[root@docker yum.repos.d 18:48:07]# vim cloud.repo
[centos-openstack-victoria]
name=CentOS 8 - OpenStack victoria
baseurl=https://mirrors.aliyun.com/centos-vault/8/cloud/x86_64/openstack-victoria/
enabled=1
gpgcheck=0

[root@docker yum.repos.d 18:59:20]# yum install -y bridge-utils
[root@docker yum.repos.d 18:59:37]# brctl  show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02420ae5d46d	no		

当前 docker0 上没有任何其他网络设备,我们创建一个容器看看有什么变化

复制代码
[root@docker ~ 19:00:19]# docker run -it -d --name busybox1 busybox
f7b04be17e4733df097faeed625d8cfa53cd663140f3f9db8756f4aa6073c590
[root@docker ~ 19:01:02]# brctl  show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.02420ae5d46d	no		veth4b3e2fc

一个新的网络接口 veth4b3e2fc被挂到了 docker0 上, veth4b3e2fc 就是新创建容器的虚拟网卡。

下面看一下容器的网络配置。

复制代码
[root@docker ~ 19:01:07]# 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
# 容器里的网卡是4号网卡名字叫eth0,对面是5号网卡
4: eth0@if5: <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
/ # 


[root@docker ~ 19:05:02]# 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:20:8c:f5 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 10.1.8.25/24 brd 10.1.8.255 scope global noprefixroute ens160
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe20:8cf5/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:0a:e5:d4:6d 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:aff:fee5:d46d/64 scope link 
       valid_lft forever preferred_lft forever
5: veth4b3e2fc@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether 0a:3d:6b:cf:d2:c7 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::83d:6bff:fecf:d2c7/64 scope link 
       valid_lft forever preferred_lft forever

容器有一个网卡 eth0@if5 。大家可能会问了,为什么不是 veth4b3e2fc呢?

实际上 eth0@if5 和 veth4b3e2fc是一对 veth pair。veth pair 是一种成对出现的特殊网络设备,可以把它们想象成由一根虚拟网线连接起来的一对网卡,网卡的一头( eth0@if5 )在容器中,另一头( veth4b3e2fc)挂在网桥 docker0 上,其效果就是将 eth0@if5 也挂在了 docker0 上。

在宿主机上查看IP地址,可以证明Docker0上的 veth4b3e2fc和容器中的 eth0@if5 是一对;在宿主机看到有块网卡:5: veth4b3e2fc@if4:;宿主机的5号网卡: 5: veth4b3e2fc@if4:含义就是宿主机5号网卡对面连接了一块4号网卡,5号网卡的名字叫veth4b3e2fc

证明了容器busybox1里的 eth0 连接到了docker0网桥的 veth4b3e2fc

我们还看到 eth0@if5 已经配置了 IP 172.17.0.2 ,为什么是这个网段呢?让我们通过 docker network inspect bridge 看一下 bridge 网络的配置信息

复制代码
[root@docker ~ 19:05:06]# docker network inspect  bridge 
[
    {
        "Name": "bridge",
        "Id": "95d565974ef010545461640c62a67235c406a8747d809c75e972e6fc95e1d62a",
        "Created": "2026-07-30T18:39:27.210472587+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",		`#这是bridge网络分配的网段`
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "f7b04be17e4733df097faeed625d8cfa53cd663140f3f9db8756f4aa6073c590": {
                "Name": "busybox1",			`#容器busybox1`
                "EndpointID": "d4f392b780f8ba1731ff65ee49d4174f33e37966a5c4019936076ad752616624",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",		`分配给busybox1的
                "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。

复制代码
[root@docker ~ 19:09:26]# ip a | grep docker0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: veth4b3e2fc@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 

当前容器网络拓扑结构如图所示:

容器创建时,docker 会自动从 172.17.0.0/16 中分配一个 IP,这里 16 位的掩码保证有足够多的 IP 可以供容器使用。

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络,下一节我们将详细讨论。

6.3 如何自定义容器网络?

除了 none, host, bridge 这三个自动创建的网络,用户也可以根据业务需要创建 user-defined 网络。

Docker 提供三种 user-defined 网络驱动:bridge, overlay 和 macvlan。overlay 和 macvlan 用于创建跨主机的网络。

我们可通过 bridge 驱动创建类似前面默认的 bridge 网络,例如:

复制代码
[root@docker ~ 19:09:33]# docker network create  --driver bridge zy_net
dab376c4e0140ee7284864d1503d82f55b2852233d03a0275dfc663926487b30

查看一下当前 host 的网络结构变化:

复制代码
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no 		#zy_net,看bridgename和刚才创建的zy_net id一样
docker0 8000.02420be905a5 no vethddb2744

[root@docker ~ 19:11:38]# brctl  show
bridge name	bridge id		STP enabled	interfaces
br-dab376c4e014		8000.0242fa341cee	no			#zy_net,看bridgename和刚才创建的zy_net id一样
docker0		8000.02420ae5d46d	no		veth4b3e2fc

新增了一个网桥 br-dab376c4e014 ,这里 br-dab376c4e014正好是新建 bridge 网络 zy_net 的短id。执行docker network inspect 查看一下 zy_net 的配置信息:

复制代码
[root@docker ~]# docker network ls
[root@docker ~]# docker network inspect my_net
[
{
"Name": "my_net",
"Id": "89f7bc11b602e84452ae01786113ac196a535f7296b9989ad941b3a48a5e6d04",
"Created": "2025-07-31T15:57:17.487644996+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16", `#这里 172.18.0.0/16 是 Docker自动分配的 IP 网段。``
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]



[root@docker ~ 19:11:46]# docker network  ls
NETWORK ID     NAME      DRIVER    SCOPE
95d565974ef0   bridge    bridge    local
091f3b4addce   host      host      local
235a95facc89   none      null      local
dab376c4e014   zy_net    bridge    local
[root@docker ~ 19:13:39]# docker network inspect  zy_net 
[
    {
        "Name": "zy_net",
        "Id": "dab376c4e0140ee7284864d1503d82f55b2852233d03a0275dfc663926487b30",
        "Created": "2026-07-30T19:11:37.915703555+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 参数:

复制代码
[root@docker ~]# docker network create --driver bridge --subnet 172.22.16.0/24 --
gateway 172.22.16.1 my_net2
ec761bc51778f67c2245af72fd969f00cd517ee617a10a70dc01904f9c10279d
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no #my_net2
docker0 8000.02420be905a5 no vethddb2744
[root@docker ~]# docker network inspect my_net2
[
{
"Name": "my_net2",
"Id": "ec761bc51778f67c2245af72fd969f00cd517ee617a10a70dc01904f9c10279d",
"Created": "2025-07-31T16:00:06.021244096+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-ec761bc51778 上:

复制代码
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no #my_net2
docker0 8000.02420be905a5 no vethddb2744
#同时宿主机上出现了与网桥my_net,my_net2同名的网卡
#查看my_net2网卡
[root@docker ~]# ip a | grep br-ec761bc51778
27: br-ec761bc51778: <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-ec761bc51778

容器要使用新的网络,需要在启动时通过 --network 指定:

复制代码
[root@docker ~]# 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
28: eth0@if29: <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退出容器
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
br-ec761bc51778 8000.02427fa54b88 no veth02dd704 #my_net2
上新增的接口veth02dd704连接容器busybox2
docker0 8000.02420be905a5 no vethddb2744

容器分配到的 IP 为 172.22.16.2。

到目前为止,容器的 IP 都是 docker 自动从 subnet 中分配,我们能否指定一个静态 IP 呢?

答案是:可以,通过 --ip 指定。

复制代码
[root@docker ~]# 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
30: eth0@if31: <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 ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no #my_net
br-ec761bc51778 8000.02427fa54b88 no veth02dd704
#my_net2
veth5fbb7f6
#veth5fbb7f6是连接busybox3
docker0 8000.02420be905a5 no vethddb2744

注:只有使用 --subnet 创建的网络才能指定静态 IP

my_net 创建时没有指定 --subnet ,如果指定静态 IP 报错如下:

复制代码
[root@docker ~]# docker run -it --network=my_net --ip 172.18.0.8 busybox
docker: Error response from daemon: invalid config for network my_net: invalid
endpoint settings:
user specified IP address is supported only when connecting to networks with user
configured subnets.
See 'docker run --help'.

好了,我们来看看当前 docker host 的网络拓扑结构。

6.4 理解容器之间的连通性

通过前面小节的实践,当前 docker host 的网络拓扑结构如下图所示,今天我们将讨论这几个容器之间的连通性。

busybox2、busybox3 容器都挂在 my_net2 上,应该能够互通,我们验证一下:

复制代码
# 登陆busybox2 ping busybox3
[root@docker ~]# docker exec -it busybox2 sh
/ # ifconfig eth0
eth0 Link encap:Ethernet HWaddr 02:42:AC:16:10:02
inet addr:172.22.16.2 Bcast:172.22.16.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:18 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1436 (1.4 KiB) TX bytes:0 (0.0 B)
/ # 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.197 ms
64 bytes from 172.22.16.8: seq=1 ttl=64 time=0.137 ms
64 bytes from 172.22.16.8: seq=2 ttl=64 time=0.158 ms
--- 172.22.16.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.137/0.164/0.197 ms
/ #
# ping my_net2网关地址
/ # 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.149 ms
64 bytes from 172.22.16.1: seq=1 ttl=64 time=0.116 ms
64 bytes from 172.22.16.1: seq=2 ttl=64 time=0.182 ms
--- 172.22.16.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.116/0.149/0.182 ms

可见同一网络中的容器、网关之间都是可以通信的。

my_net2 与默认 bridge 网络(docker0)能通信吗?

从拓扑图可知,两个网络属于不同的网桥,应该不能通信,我们通过实验验证一下,让 busybox2 容器ping buxybox1 容器:

复制代码
/ # ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss
/ #

确实 ping 不通,符合预期。

"等等!不同的网络如果加上路由应该就可以通信了吧?"

这是一个非常非常好的想法。

确实,如果 host 上对每个网络的都有一条路由,同时操作系统上打开了 ip forwarding,host 就成了一个路由器,挂接在不同网桥上的网络就能够相互通信。下面我们来看看 docker host 满不满足这些条件呢?

ip r 查看 host 上的路由表:

复制代码
[root@docker ~]# 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-a6eff42dc3c6 proto kernel scope link src 172.18.0.1 linkdown
172.22.16.0/24 dev br-c0fc7bdf8143 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
[root@docker ~]#

172.17.0.0/16 和 172.22.16.0/24 两个网络的路由都定义好了。再看看 ip forwarding:

复制代码
[root@docker ~]# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

ip forwarding 也已经启用了。

条件都满足,为什么不能通行呢?

我们还得看看 iptables:

复制代码
[root@docker ~]# iptables-save
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*filter
:INPUT ACCEPT [69184:9702316]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [113709:49882234]
: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-ec761bc51778 -m conntrack --ctstate RELATED,ESTABLISHED -j
ACCEPT
-A FORWARD -o br-ec761bc51778 -j DOCKER
-A FORWARD -i br-ec761bc51778 ! -o br-ec761bc51778 -j ACCEPT
-A FORWARD -i br-ec761bc51778 -o br-ec761bc51778 -j ACCEPT
-A FORWARD -o br-89f7bc11b602 -m conntrack --ctstate RELATED,ESTABLISHED -j
ACCEPT
-A FORWARD -o br-89f7bc11b602 -j DOCKER
-A FORWARD -i br-89f7bc11b602 ! -o br-89f7bc11b602 -j ACCEPT
-A FORWARD -i br-89f7bc11b602 -o br-89f7bc11b602 -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-ec761bc51778 ! -o br-ec761bc51778 -j DOCKERISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-89f7bc11b602 ! -o br-89f7bc11b602 -j DOCKERISOLATION-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-ec761bc51778 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-89f7bc11b602 -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 Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*security
:INPUT ACCEPT [69166:9698194]
:FORWARD ACCEPT [34:2856]
:OUTPUT ACCEPT [113709:49882234]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*raw
:PREROUTING ACCEPT [69554:9733396]
:OUTPUT ACCEPT [113709:49882234]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*mangle
:PREROUTING ACCEPT [69554:9733396]
:INPUT ACCEPT [69184:9702316]
:FORWARD ACCEPT [370:31080]
:OUTPUT ACCEPT [113709:49882234]
:POSTROUTING ACCEPT [113743:49885090]
COMMIT
# Completed on Thu Jul 31 17:09:34 2025
# Generated by iptables-save v1.8.5 on Thu Jul 31 17:09:34 2025
*nat
:PREROUTING ACCEPT [367:33182]
:INPUT ACCEPT [9:500]
:POSTROUTING ACCEPT [294:23341]
:OUTPUT ACCEPT [291:23089]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.22.16.0/24 ! -o br-ec761bc51778 -j MASQUERADE
-A POSTROUTING -s 172.18.0.0/16 ! -o br-89f7bc11b602 -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-ec761bc51778 -j RETURN
-A DOCKER -i br-89f7bc11b602 -j RETURN
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Thu Jul 31 17:09:34 2025

原因就在这里了:iptables DROP 掉了网桥 docker0 br-ec761bc51778(my_net2) 之间双向的流

复制代码
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
br-ec761bc51778(mynet2) 8000.02427fa54b88 no veth02dd704
veth5fbb7f6
docker0 8000.02420be905a5 no vethddb2744

从规则的命名 DOCKER-ISOLATION 可知 docker 在设计上就是要隔离不同的 netwrok。

那么接下来的问题是:怎样才能让 busybox1与busybox2 通信呢?

答案是:为 busybox1 容器添加一块 my_net2的网卡。这个可以通过 docker network connect 命令

实现。

复制代码
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
NAMES
0899976a1c59 busybox "sh" About an hour ago Up About an hour
busybox3
e9da18f962d6 busybox "sh" About an hour ago Up About an hour
busybox2
5225d246f751 busybox "sh" About an hour ago Up About an hour
busybox1
[root@docker ~]# docker network connect my_net2 busybox1

我们在 httpd 容器中查看一下网络配置:

复制代码
[root@docker ~]# 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
24: eth0@if25: <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
32: eth1@if33: <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
[root@docker ~]# brctl show
bridge name bridge id STP enabled interfaces
br-89f7bc11b602 8000.0242194d039e no
br-ec761bc51778 8000.02427fa54b88 no veth02dd704
#busybox2
veth5fbb7f6
#busybox3
veth6642cc9
#busybox1
docker0 8000.02420be905a5 no vethddb2744

容器中增加了一个网卡 eth1,分配了 my_net2 的 IP 172.22.16.3。现在 busybox2 应该能够访问

busybox1 了,验证一下:

复制代码
[root@docker ~]# docker exec -it busybox2 sh
/ # ping -c 3 172.22.16.3
PING 172.22.16.3 (172.22.16.3): 56 data bytes
64 bytes from 172.22.16.3: seq=0 ttl=64 time=0.199 ms
64 bytes from 172.22.16.3: seq=1 ttl=64 time=0.134 ms
64 bytes from 172.22.16.3: seq=2 ttl=64 time=0.141 ms
--- 172.22.16.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.134/0.158/0.199 ms
/ #

busybox 能够 ping 到 busybox2。当前网络结构如图所示:

6.5 容器通信的三种方式

容器之间可通过 IP,Docker DNS Server 或 joined 容器三种方式通信。

(1)IP 通信

从上一节的例子可以得出这样一个结论:两个容器要能通信,必须要有属于同一个网络的网卡。

满足这个条件后,容器就可以通过 IP 交互了。具体做法是在容器创建时通过 --network 指定相应的网

络,或者通过 docker network connect 将现有容器加入到指定网络。可参考上一节 busybox 的例

子,这里不再赘述。

(2)Docker DNS Server

通过 IP 访问容器虽然满足了通信的需求,但还是不够灵活。因为我们在部署应用之前可能无法确定 IP,部署之后再指定要访问的 IP 会比较麻烦。对于这个问题,可以通过 docker 自带的 DNS 服务解决。

从 Docker 1.10 版本开始,docker daemon 实现了一个内嵌的 DNS server,使容器可以直接通过"容器名"通信。方法很简单,只要在启动时用 --name 为容器命名就可以了。

下面启动两个容器 bbox1 和 bbox2:

复制代码
[root@docker ~]# docker run -it --network my_net2 --name bbox1 busybox
/ #
ctrl_p,ctrl_q退出容器
[root@docker ~]# docker run -it --network my_net2 --name bbox2 busybox
/ #
ctrl_p,ctrl_q退出容器

#然后,bbox2 就可以直接 ping 到 bbox1 了:
[root@docker ~]# docker exec -it bbox2 sh
/ #
/ # ping -c 3 bbox1
PING bbox1 (172.22.16.2): 56 data bytes
64 bytes from 172.22.16.2: seq=0 ttl=64 time=0.177 ms
64 bytes from 172.22.16.2: seq=1 ttl=64 time=0.161 ms
64 bytes from 172.22.16.2: seq=2 ttl=64 time=0.187 ms
--- bbox1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.161/0.175/0.187 ms

使用 docker DNS 有个限制:只能在 user-defined 网络中使用。也就是说,默认的 bridge 网络是无法使用 DNS 的。下面验证一下:

创建 bbox3 和 bbox4,均连接到 bridge 网络。

复制代码
[root@docker ~]# docker run -it --name bbox3 busybox
/ #
ctrl_p,ctrl_q退出容器
[root@docker ~]# docker run -it --name bbox4 busybox
/ #
ctrl_p,ctrl_q退出容器

#bbox4 无法 ping 到 bbox3。
[root@docker ~]# docker exec -it bbox4 sh
/ # ping -c 3 bbox3
ping: bad address 'bbox3'
/ #
(3)joined 容器

joined 容器是另一种实现容器间通信的方式。

joined 容器非常特别,它可以使两个或多个容器共享一个网络栈,共享网卡和配置信息,joined 容器之间可以通过 127.0.0.1 直接通信。请看下面的例子:

先创建一个 httpd 容器,名字为 web1。

复制代码
[root@docker ~]# docker run -d -it --name web1 httpd
f7641c43eb7021724e869f77bd4541e909ebf0968fc35e6a43e8e674d046ef3f

#下面我们查看一下 web1 的网络:
[root@docker ~]# docker exec -it web1 bash
root@1d5181ce7a85:/usr/local/apache2# hostname -I
172.17.0.4
# 也可以直接通过hostname -I观察IP地址

然后创建 busybox 容器并通过 --network=container:web1 指定 jointed 容器为 web1:

请注意 busybox 容器中的网络配置信息

复制代码
[root@docker ~]# 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
18: eth0@if19: <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
/ #

看!busybox 和 web1 的网卡 mac 地址与 IP 完全一样,它们共享了相同的网络栈。busybox 可以直接用 127.0.0.1 访问 web1 的 http 服务。

复制代码
/ # 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>
/ #

joined 容器非常适合以下场景:

  1. 不同容器中的程序希望通过 loopback 高效快速地通信,比如 web server 与 app server。

  2. 希望监控其他容器的网络流量,比如运行在独立容器中的网络监控程序。

容器之间的通信我们已经搞清楚了,接下来要考虑的是容器如何与外部世界通信?这将是下一节的主

题。

6.6 容器如何访问外部世界

前面我们已经解决了容器间通信的问题,接下来讨论容器如何与外部世界通信。这里涉及两个方向:

  1. 容器访问外部世界

  2. 外部世界访问容器

容器访问外部世界

在我们当前的实验环境下,docker host 是可以访问外网的。

复制代码
[root@docker ~]# ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.6) 56(84) bytes of data.
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=1 ttl=128 time=67.8 ms
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=2 ttl=128 time=13.0 ms
64 bytes from 223.109.82.6 (223.109.82.6): icmp_seq=3 ttl=128 time=12.5 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 12.454/31.074/67.754/25.937 ms

我们看一下容器是否也能访问外网呢?

复制代码
[root@docker ~]# docker run -it --name test1 busybox
/ #
/ # ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.6): 56 data bytes
64 bytes from 223.109.82.6: seq=0 ttl=127 time=21.240 ms
64 bytes from 223.109.82.6: seq=1 ttl=127 time=14.987 ms
64 bytes from 223.109.82.6: seq=2 ttl=127 time=14.651 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 14.651/16.959/21.240 ms

可见,容器默认就能访问外网

请注意:这里外网指的是容器网络以外的网络环境,并非特指 internet。

现象很简单,但更重要的:我们应该理解现象下的本质。

在上面的例子中,busybox 位于 docker0 这个私有 bridge 网络中(172.17.0.0/16),当 busybox 从

容器向外 ping 时,数据包是怎样到达 <www.baidu.com> 的呢?

这里的关键就是 NAT。我们查看一下 docker host 上的 iptables 规则:

复制代码
[root@docker ~]# 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.19.0.0/16 ! -o br-6936dc39839e -j MASQUERADE
-A POSTROUTING -s 172.22.16.0/24 ! -o br-c0fc7bdf8143 -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-6936dc39839e -j RETURN
-A DOCKER -i br-c0fc7bdf8143 -j RETURN
[root@docker ~]#

在 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 的路由表:

复制代码
[root@docker ~]# 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.19.0.0/16 dev br-6936dc39839e proto kernel scope link src 172.19.0.1 linkdown
172.22.16.0/24 dev br-c0fc7bdf8143 proto kernel scope link src 172.22.16.1
linkdown
192.168.108.0/24 dev ens160 proto kernel scope link src 192.168.108.30 metric 100

默认路由通过 ens160 发出去,所以我们要同时监控 ens160 和 docker0 上的 icmp(ping)数据包。

使用tcpdump观察现象

复制代码
[root@docker ~]# yum install -y tcpdump
[root@docker ~]# 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 ~]# 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>

复制代码
[root@docker ~]# docker run -it busybox
/ #
/ # ping -c 3 www.baidu.com
PING www.baidu.com (223.109.82.41): 56 data bytes
64 bytes from 223.109.82.41: seq=0 ttl=127 time=12.503 ms
64 bytes from 223.109.82.41: seq=1 ttl=127 time=13.380 ms
64 bytes from 223.109.82.41: seq=2 ttl=127 time=11.507 ms
--- www.baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 11.507/12.463/13.380 ms
/ #

tcpdump 输出如下:

查看docker0抓包窗口

复制代码
[root@docker ~]# 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
15:23:42.854951 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 0,
length 64
15:23:42.870473 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 0,
length 64
15:23:43.857551 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 1,
length 64
15:23:43.880065 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 1,
length 64
15:23:44.869116 IP 172.17.0.2 > 223.109.82.41: ICMP echo request, id 7, seq 2,
length 64
15:23:44.883777 IP 223.109.82.41 > 172.17.0.2: ICMP echo reply, id 7, seq 2,
length 64

docker0 收到 busybox 的 ping 包,源地址为容器 IP 172.17.0.2,这没问题,交给 MASQUERADE 处

理。这时,在 ens160 上我们看到了变化:

复制代码
[root@docker ~]# 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
15:25:41.886472 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
0, length 64
15:25:41.906738 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 0,
length 64
15:25:42.887752 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
1, length 64
15:25:42.899613 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 1,
length 64
15:25:43.889467 IP 192.168.108.30 > 223.109.82.41: ICMP echo request, id 8, seq
2, length 64
15:25:43.904277 IP 223.109.82.41 > 192.168.108.30: ICMP echo reply, id 8, seq 2,
length 64

ping 包的源地址变成了 ens160 IP 192.168.108.130

这就是 iptable NAT 规则处理的结果,从而保证数据包能够到达外网。下面用一张图来说明这个过程:

  1. busybox 发送 ping 包:172.17.0.2 > <www.baidu.com>。

  2. docker0 收到包,发现是发送到外网的,交给 NAT 处理。

  3. NAT 将源地址换成 ens160的 IP:192.168.108.30 > <www.baidu.com>。

  4. ping 包从 ens160 发送出去,到达 <www.baidu.com>。

通过 NAT,docker 实现了容器对外网的访问。

下一节我们讨论另一个方向的流量:外部世界如何访问容器。

6.7 外部世界如何访问容器

外部网络如何访问到容器?

答案是:端口映射

docker 可将容器对外提供服务的端口映射到 host 的某个端口,外网通过该端口访问容器。容器启动时通过 -p 参数映射端口:

容器启动后,可通过 docker ps 或者 docker port 查看到 host 映射的端口。在上面的例子中,httpd

容器的 80 端口被映射到 host 32768 上,这样就可以通过 <host ip>:<32768> 访问容器的 web 服务了。

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

除了映射动态端口,也可在 -p 中指定映射到 host 某个特定端口,例如可将 80 端口映射到 host 的

8080 端口:

复制代码
[root@docker ~]# docker run -d -p 8080:80 httpd
25b04cb7f6d6c012c609251a425475fcf7da03c93feb20c1d951eb45cb1ef79b
[root@docker ~]#
[root@docker ~]# curl 192.168.108.30:8080
<html><body><h1>It works!</h1></body></html>

每一个映射的端口,host 都会启动一个 docker-proxy 进程来处理访问容器的流量:

以 0.0.0.0:8080->80/tcp 为例分析整个过程:

  1. docker-proxy 监听 host 的 8080 端口。

  2. 当 curl 访问 192.168.108.30:8080时,docker-proxy 转发给容器 172.17.0.3:80。

  3. httpd 容器响应请求并返回结果。

6.8 实战:安装tomcat

复制代码
#docker hub上面查找tomcat镜像
[root@docker ~]# docker search tomcat
NAME DESCRIPTION STARS
OFFICIAL
tomcat Apache Tomcat is an open source implementati… 3751
[OK]
bitnami/tomcat Bitnami container image for Tomcat 52
bitnamicharts/tomcat Bitnami Helm chart for Apache Tomcat 0
rootpublic/tomcat 0
chainguard/tomcat Build, ship and run secure software with Cha… 0
islandora/tomcat Base tomcat image used for java services. 0
vulhub/tomcat 0
openeuler/tomcat 0
tomcat/tomcat01 0
jelastic/tomcat An image of the Tomcat Java application serv… 4
rightctrl/tomcat CentOS , Oracle Java, tomcat application ssl… 7
amd64/tomcat Apache Tomcat is an open source implementati… 10
softwareplant/tomcat Tomcat images for jira-cloud testing 0
arm64v8/tomcat Apache Tomcat is an open source implementati… 13
appsvc/tomcat 1
s390x/tomcat Apache Tomcat is an open source implementati… 0
ppc64le/tomcat Apache Tomcat is an open source implementati… 1
arm32v7/tomcat Apache Tomcat is an open source implementati… 12
awscory/tomcat tomcat 0
devbeta/tomcat 0
tutum/tomcat Base docker image to run a Tomcat applicatio… 11
qasymphony/tomcat Tomcat images 1
hegand/tomcat docker-tomcat 0
techangels/tomcat 0
cloudesire/tomcat Tomcat server, 6/7/8 14

#从docker hub上拉取tomcat镜像到本地
[root@docker ~ 19:46:51]# docker pull tomcat


#docker images查看是否有拉取到的tomcat
[root@docker ~ 19:47:50]# docker images 
REPOSITORY           TAG       IMAGE ID       CREATED        SIZE
myweb                v1        2d575a2e5f53   24 hours ago   115MB
<none>               <none>    08b2b4bfdc71   24 hours ago   115MB
ubuntu-with-stress   latest    9d72c80dd63b   3 days ago     142MB
httpd                latest    a06426a45b48   2 weeks ago    117MB
ubuntu               latest    de7345b16e94   2 weeks ago    100MB
tomcat               latest    a0a96d0e5ff5   3 weeks ago    422MB
busybox              latest    c6348fa86ba0   2 months ago   4.45MB
registry             2         26b2eb03618e   2 years ago    25.4MB


#使用tomcat镜像创建容器实例(也叫运行镜像)
[root@docker ~ 19:47:59]# docker run -d -it -p 8080:8080 tomcat
d78ef856705e165a07e54fe77a224296e99cadef815983f914a1a05257f475db

访问tomcat首页

把webapps.dist目录换成webapps

复制代码
#查看刚才创建的tomcat容器ID
[root@docker ~ 19:48:23]# docker ps
CONTAINER ID   IMAGE     COMMAND             CREATED          STATUS          PORTS                                       NAMES
d78ef856705e   tomcat    "catalina.sh run"   43 seconds ago   Up 42 seconds   0.0.0.0:8080->8080/tcp, :::8080->8080/tcp   mystifying_noether

#进入tomcat容器
[root@docker ~ 19:49:06]# docker exec  -it  d78ef856705e16 bash

#查看webapps文件夹为空
root@d78ef856705e:/usr/local/tomcat# ls webapps

#文件在webapps.dist
root@d78ef856705e:/usr/local/tomcat# ls webapps.dist/
docs  examples  host-manager  manager  ROOT

#用webapps.dist替换webapps
root@d78ef856705e:/usr/local/tomcat# mv webapps.dist/* webapps

再次访问tomcat

7.存储

7.1 Docker的两类存储资源

Docker 为容器提供了两种存放数据的资源:

  1. 由 storage driver 管理的镜像层和容器层。

  2. Data Volume。

我们会详细讨论它们的原理和特性。

(1)storage driver

在前面镜像章节我们学习到 Docker 镜像的分层结构,简单回顾一下。

容器由最上面一个可写的容器层,以及若干只读的镜像层组成,容器的数据就存放在这些层中。这样的

分层结构最大的特性是 Copy-on-Write:

  1. 新数据会直接存放在最上面的容器层。

  2. 修改现有数据会先从镜像层将数据复制到容器层,修改后的数据直接保存在容器层中,镜像层保持

不变。

  1. 如果多个层中有命名相同的文件,用户只能看到最上面那层中的文件。
复制代码
两件核心工作
1.管镜像多层(只读)
镜像不是一整个文件,是一层一层叠起来的,比如底层 ubuntu、再装 httpd、
再装配置。
驱动负责把这些分层存好,多个容器共用底层只读层,省硬盘空间。

2.给每个容器单独一层可写层
镜像全部不能改。启动容器时,驱动会在所有只读层最上面,给这个容器单独加
一层“草稿层”。
容器里改文件、新建文件,只存在这层草稿里;
删掉容器,只删这层草稿,底层镜像完好无损;
多个同镜像容器,底层共用,草稿互相独立。

分层结构使镜像和容器的创建、共享以及分发变得非常高效,而这些都要归功于 Docker storage driver。正是 storage driver 实现了多层数据的堆叠并为用户提供一个单一的合并之后的统一视图。Docker 支持多种 storage driver,有 AUFS、Device Mapper、Btrfs、OverlayFS、VFS 和 ZFS。它们都能实现分层的架构,同时又有各自的特性。对于 Docker 用户来说,具体选择使用哪个 storage driver 是一个难题,因为:

  1. 没有哪个 driver 能够适应所有的场景。

  2. driver 本身在快速发展和迭代。

不过 Docker 官方给出了一个简单的答案:

优先使用 Linux 发行版默认的 storage driver

Docker 安装时会根据当前系统的配置选择默认的 driver。默认 driver 具有最好的稳定性,因为默认driver 在发行版上经过了严格的测试。

运行 docker info 查看CentOS的默认 driver:

复制代码
[root@docker ~ 11:38:41]# 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: 4
  Running: 4
  Paused: 0
  Stopped: 0
 Images: 8
 Server Version: 26.1.3
 Storage Driver: overlay2  			#CentOS Stream 8 用的overlay2
  Backing Filesystem: xfs			#底层文件系统是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: 1.706GiB
 Name: docker
 ID: ac1f79c7-26ef-432d-a9b4-31d69e3e299a
 Docker Root Dir: /var/lib/docker			#各层数据存放在 /var/lib/docker。
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  127.0.0.0/8
 Registry Mirrors:
  https://054b8ac70e8010d90f2ac00ef29e6580.mirror.swr.myhuaweicloud.com/
 Live Restore Enabled: false

CentOS Stream 8 用的overlay2,底层文件系统是xfs,各层数据存放在 /var/lib/docker。

对于某些容器,直接将数据放在由 storage driver 维护的层中是很好的选择,比如那些无状态的应用。

无状态意味着容器没有需要持久化的数据,随时可以从镜像直接创建。

比如 busybox,它是一个工具箱,我们启动 busybox 是为了执行诸如 wget,ping 之类的命令,不需要

保存数据供以后使用,使用完直接退出,容器删除时存放在容器层中的工作数据也一起被删除,这没问题,下次再启动新容器即可。

但对于另一类应用这种方式就不合适了,它们有持久化数据的需求,容器启动时需要加载已有的数据,

容器销毁时希望保留产生的新数据,也就是说,这类容器是有状态的。

这就要用到 Docker 的另一种存储机制:Data Volume,下一节我们讨论。

7.2 Data Volume详解

storage driver 和 data volume 是容器存放数据的两种方式,上一节我们学习了 storage driver,本节开始讨论 Data Volume。

Data Volume 本质上是 Docker Host 文件系统中的目录或文件,能够直接被 mount 到容器的文件系统中。Data Volume 有以下特点:

  1. Data Volume 是目录或文件,而非没有格式化的磁盘(块设备)。

  2. 容器可以读写 volume 中的数据。

  3. volume 数据可以被永久的保存,即使使用它的容器已经销毁。

好,现在我们有数据层(镜像层和容器层)和 volume 都可以用来存放数据,具体使用的时候要怎样选择呢?考虑下面几个场景:

  1. Database 软件 vs Database 数据

  2. Web 应用 vs 应用产生的日志

  3. 数据分析软件 vs input/output 数据

  4. Apache Server vs 静态 HTML 文件

相信大家会做出这样的选择:

  1. 前者放在数据层中。因为这部分内容是无状态的,应该作为镜像的一部分。

  2. 后者放在 Data Volume 中。这是需要持久化的数据,并且应该与镜像分开存放。

还有个大家可能会关心的问题:如何设置 voluem 的容量?

因为 volume 实际上是 docker host 文件系统的一部分,所以 volume 的容量取决于文件系统当前未使用的空间,目前还没有方法设置 volume 的容量。在具体的使用上,docker 提供了两种类型的 volume:bind mount 和 docker managed volume。

(1)bind mount

bind mount 是将 host 上已存在的目录或文件 mount 到容器。

例如 docker host 上有目录 $HOME/htdocs:

复制代码
[root@docker ~ 11:38:56]# pwd
/root
[root@docker ~ 11:45:06]# mkdir htdocs
[root@docker ~ 11:45:15]# cd htdocs/
[root@docker htdocs 11:45:18]# touch index.html
[root@docker htdocs 11:45:24]# vim index.html 
<html><body><h1>This is a file in host file system !</h1></body></html>

[root@docker htdocs 11:45:38]# cd ..
[root@docker ~ 11:45:40]# cat htdocs/index.html 
<html><body><h1>This is a file in host file system !</h1></body></html>
[root@docker ~ 11:45:50]# 

通过 -v 将其 mount 到 httpd 容器:

复制代码
#先了解一下使用httpd镜像运行一个容器,这个
[root@docker ~ 12:06:52]# docker run -d -p 80:80 httpd
\3a14d31ccf277c419728d2e09d8a7c724ef33dadcfe4ba1921609c00b4030f3e
[root@docker ~ 12:06:57]# docker exec  -it 3a14 bash
root@3a14d31ccf27:/usr/local/apache2# ls
bin  build  cgi-bin  conf  error  htdocs  icons  include  logs	modules
root@3a14d31ccf27:/usr/local/apache2# cd htdocs/
root@3a14d31ccf27:/usr/local/apache2/htdocs# ls
index.html
root@3a14d31ccf27:/usr/local/apache2/htdocs# cat index.html 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
root@3a14d31ccf27:/usr/local/apache2/htdocs# 

1. 为什么进入容器后,默认在 /usr/local/apache2 目录?
这个目录是 Apache HTTP Server(httpd) 在 Linux 系统中的标准安装路径。你用的镜像是官方的 httpd 镜像(即 Apache)。镜像的制作者(Docker 官方)在构建时,把 Apache 安装到了 /usr/local/apache2 这个目录下。这相当于 Apache 的家目录,里面包含了:bin/ → 启动脚本(如 apachectl)conf/ → 配置文件(如 httpd.conf)htdocs/ → 默认的网站根目录(网页存放位置)logs/ → 访问日志和错误日志所以当你用 docker exec -it ... bash 进入容器时,默认的当前工作目录(WORKDIR)就是 /usr/local/apache2,这是镜像作者设定的。


2.httpd 不是网页,它是一个 Web 服务器软件(Apache),作用是接收 HTTP 请求并返回网页内容。默认网页内容就在你刚才找到的地方:/usr/local/apache2/htdocs/index.html。你看到的这个 index.html 就是 Apache 安装时自带的默认测试页,内容就是:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
这就是你通过浏览器访问 http://你的服务器IP:80 时看到的 "It works!" 页面的来源。



[root@docker ~ 11:45:50]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs httpd
43d04246efc69cc2a13b91f380359334f890bb71496bc4e2b2ebba6ace55cd9a

-v 的格式为 <host path>:<container path> 。

/usr/local/apache2/htdocs 就是 apache server 存放静态文件的地方。由于 /usr/local/apache2/htdocs 已经存在,原有数据会被隐藏起来,取而代之的是host $HOME/htdocs/ 中的数据,这与 linux mount 命令的行为是一致的。

复制代码
#意思就是虽然这个容器里面有这个/usr/local/apache2/htdocs的目录下有index.html但是我们用~/usr/绑定到这个容器中的目录后,容器htdocs的目录就会变成宿主机的htdocs目录下面的内容。
#并且我随便在容器中修改或者在宿主机上面修改内容,两者的内容都会同步
[root@docker ~ 12:13:28]# docker exec -it pensive_bhabha  bash
root@ebf8d5db3264:/usr/local/apache2# ls
bin  build  cgi-bin  conf  error  htdocs  icons  include  logs	modules
root@ebf8d5db3264:/usr/local/apache2# cat htdocs/
cat: htdocs/: Is a directory
root@ebf8d5db3264:/usr/local/apache2# ls
bin  build  cgi-bin  conf  error  htdocs  icons  include  logs	modules
root@ebf8d5db3264:/usr/local/apache2# cd htdocs/           
root@ebf8d5db3264:/usr/local/apache2/htdocs# ls
index.html
root@ebf8d5db3264:/usr/local/apache2/htdocs# cat index.html 
updatte index.html
root@ebf8d5db3264:/usr/local/apache2/htdocs# vim index.html 
bash: vim: command not found
root@ebf8d5db3264:/usr/local/apache2/htdocs# yum install -y vim
bash: yum: command not found
root@ebf8d5db3264:/usr/local/apache2/htdocs# echo "zhwhddwhdhwd" > index.html 
root@ebf8d5db3264:/usr/local/apache2/htdocs# ls
index.html
root@ebf8d5db3264:/usr/local/apache2/htdocs# cat index.html 
zhwhddwhdhwd
root@ebf8d5db3264:/usr/local/apache2/htdocs# exit
exit
[root@docker ~ 12:20:18]# cd htdocs/
[root@docker htdocs 12:20:22]# ls
index.html
[root@docker htdocs 12:20:23]# cat index.html 
zhwhddwhdhwd
[root@docker htdocs 12:20:25]# 

curl 显示当前主页确实是 $HOME/htdocs/index.html 中的内容。更新一下,看是否能生效:

复制代码
[root@docker ~ 11:47:33]# curl 127.0.0.1:80
<html><body><h1>This is a file in host file system !</h1></body></html>
复制代码
[root@docker ~ 11:54:00]# echo "updatte index.html" > ~/htdocs/index.html 
[root@docker ~ 11:54:30]# curl 127.0.0.1:80
updatte index.html

host 中的修改确实生效了,bind mount 可以让 host 与容器共享数据。这在管理上是非常方便的。

下面我们将容器销毁,看看对 bind mount 有什么影响:

复制代码
[root@docker ~ 11:54:33]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS         PORTS                               NAMES
43d04246efc6   httpd     "httpd-foreground"   7 minutes ago   Up 7 minutes   0.0.0.0:80->80/tcp, :::80->80/tcp   goofy_moser
[root@docker ~ 11:54:52]# docker stop goofy_moser 
goofy_moser
[root@docker ~ 11:55:06]# docker rm goofy_moser 
goofy_moser

[root@docker ~ 11:55:33]# cat ~/htdocs/index.html 
updatte index.html

可见,即使容器没有了,bind mount 也还在。这也合理,bind mount 是 host 文件系统中的数据,只是借给容器用用,哪能随便就删了啊。

另外,bind mount 时还可以指定数据的读写权限,默认是可读可写,可指定为只读:

复制代码
[root@docker ~ 11:55:41]# docker run -d -p 80:80 -v ~/htdocs:/usr/local/apache2/htdocs:ro  httpd
14db2162a6ef2331d98464edd32a4f69ed69943e0ae6e11df6fe2fa4e4e417f8

[root@docker ~ 12:00:17]# docker exec -it 14db bash
root@14db2162a6ef:/usr/local/apache2# echo "do some changes" > htdocs/index.html
bash: htdocs/index.html: Read-only file system
root@14db2162a6ef:/usr/local/apache2# 

ro 设置了只读权限,在容器中是无法对 bind mount 数据进行修改的。只有 host 有权修改数据,提高了安全性。

除了 bind mount 目录,还可以单独指定一个文件:

复制代码
# 删除上一个容器不然80端口冲突
[root@docker ~]# docker rm - f bf1f526ddde7

#运行新容器用单独文件绑定
[root@docker ~ 12:01:52]# docker run -d -p 80:80 -v ~/htdocs/index.html:/usr/local/apache2/htdocs/new_index.html httpd
ef082d0f309a368170f9874097fe23bbf9fe3783d45fd3e89c57c1268f65fc52
[root@docker ~ 12:03:08]# curl 127.0.0.1:80
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
[root@docker ~ 12:03:12]# curl 127.0.0.1:80/new_index.html
updatte index.html
[root@docker ~ 12:03:19]# 

使用 bind mount 单个文件的场景是:只需要向容器添加文件,不希望覆盖整个目录。在上面的例子

中,我们将 html 文件加到 apache 中,同时也保留了容器原有的数据。

使用单一文件有一点要注意:host 中的源文件必须要存在,不然会当作一个新目录 bind mount 给容

器。

mount point 有很多应用场景,比如我们可以将源代码目录 mount 到容器中,在 host 中修改代码就能

看到应用的实时效果。再比如将 mysql 容器的数据放在 bind mount 里,这样 host 可以方便地备份和迁

移数据。

bind mount 的使用直观高效,易于理解,但它也有不足的地方:bind mount 需要指定 host 文件系统

的特定路径,这就限制了容器的可移植性,当需要将容器迁移到其他 host,而该 host 没有要 mount 的

数据或者数据不在相同的路径时,操作会失败。

移植性更好的方式是 docker managed volume,下一节我们讨论。

(2) docker volume

docker managed volume 与 bind mount 在使用上的最大区别是不需要指定 mount 源,指明 mount

point (就是要挂载到容器的哪个目录)就行了。还是以 httpd 容器为例:

复制代码
[root@docker ~ 12:45:27]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd
adfe0329007c37c6d4bbc1bf158ce28ed6ccdc1f010ceaff2ec392444c713f72

我们通过 -v 告诉 docker 需要一个 data volume,并将其 mount 到 /usr/local/apache2/htdocs。那么

这个 data volume 具体在哪儿呢?

这个答案可以在容器的配置信息中找到,执行 docker inspect 命令:

复制代码
[root@docker ~ 12:45:29]# docker inspect adfe0329007c37c6d4bbc1bf158ce28ed6ccdc1f010ceaff2ec392444c713f72
 #docker inspect后面跟的是容器ID
...
"Mounts": [
            {
                "Type": "volume",
                "Name": "cd8ef8c31944901835076183f38613650552386fbbd6bc09a70942e4fc62df9b",
                "Source": "/var/lib/docker/volumes/cd8ef8c31944901835076183f38613650552386fbbd6bc09a70942e4fc62df9b/_data",
                "Destination": "/usr/local/apache2/htdocs",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],

...

docker inspect 的输出很多,我们感兴趣的是 Mounts 这部分,这里会显示容器当前使用的所有 datavolume,包括 bind mount 和 docker managed volume。Source 就是该 volume 在 host 上的目录。

原来,每当容器申请 mount docker manged volume 时,docker 都会在 /var/lib/docker/volumes下生成一个目录(例子中是"/var/lib/docker/volumes/cd8ef8c31944901835076183f38613650552386fbbd6bc09a70942e4fc62df9b/_data ),这个目录就是 mount 源。

下面继续研究这个 volume,看看里面有些什么东西:

复制代码
[root@docker ~ 12:49:13]# ll /var/lib/docker/volumes/cd8ef8c31944901835076183f38613650552386fbbd6bc09a70942e4fc62df9b/_data
total 4
-rw-r--r-- 1 501 ftp 191 Nov  7  2025 index.html


[root@docker ~ 12:49:25]# curl 127.0.0.1:80
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>

volume 的内容跟容器原有 /usr/local/apache2/htdocs 完全一样,这是怎么回事呢?

这是因为:如果 mount point 指向的是已有目录,原有数据会被复制到 volume 中。

但要明确一点:此时的 /usr/local/apache2/htdocs 已经不再是由 storage driver 管理的层数据了,它已

经是一个 data volume。我们可以像 bind mount 一样对数据进行操作,例如更新数据:

复制代码
[root@docker ~]# echo "update volume from host !" >
/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635
e569d16/_data/index.html
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
update volume from host !

简单回顾一下 docker managed volume 的创建过程:

  1. 容器启动时,简单的告诉 docker "我需要一个 volume 存放数据,帮我 mount 到目录 /abc"。

  2. docker 在 /var/lib/docker/volumes 中生成一个随机目录作为 mount 源。

  3. 如果 /abc 已经存在,则将数据复制到 mount 源,

  4. 将 volume mount 到 /abc

除了通过 docker inspect 查看 volume,我们也可以用 docker volume 命令:

复制代码
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16
[root@docker ~]#
[root@docker ~]# docker volume inspect
1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16
[
{
"CreatedAt": "2024-09-17T21:04:33+08:00",
"Driver": "local",
"Labels": {
"com.docker.volume.anonymous": ""
},
"Mountpoint":
"/var/lib/docker/volumes/1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a63
5e569d16/_da
"Name":
"1cc915096121dea197bbbb93fc90e6491d530e0c9e2ace1de07e2a635e569d16",
"Options": null,
"Scope": "local"
}
]

目前, docker volume 只能查看 docker managed volume,还看不到 bind mount;同时也无法知道

volume 对应的容器,这些信息还得靠 docker inspect 。

我们已经学习了两种 data volume 的原理和基本使用方法,下面做个对比:

  1. 相同点:两者都是 host 文件系统中的某个路径。

  2. 不同点:

7.3 如何共享数据

数据共享是 volume 的关键特性,本节我们详细讨论通过 volume 如何在容器与 host 之间,容器与容器

之间共享数据。

(1)容器与 host 共享数据

我们有两种类型的 data volume,它们均可实现在容器与 host 之间共享数据,但方式有所区别。

对于 bind mount 是非常明确的:直接将要共享的目录 mount 到容器。具体请参考前面 httpd 的例子,

不再赘述。

docker managed volume 就要麻烦点。由于 volume 位于 host 中的目录,是在容器启动时才生成,所

以需要将共享数据拷贝到 volume 中。请看下面的例子:

复制代码
[root@docker ~]# docker run -d -p 80:80 -v /usr/local/apache2/htdocs httpd
889b9d5309bd12499f8e4ee78a491d9ba7acef5660be00f30cda6569c9974c6f
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
<html><body><h1>It works!</h1></body></html>
[root@docker ~]#
[root@docker ~]# docker cp ~/htdocs/index.html  889b9d5309bd:/usr/local/apache2/htdocs #将host os的/root/htdocs/index.html拷贝到容器中的/usr/local/apache2/htdocs目录下
Successfully copied 2.05kB to 889b9d5309bd:/usr/local/apache2/htdocs
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
updated index page!
[root@docker ~]#

docker cp 可以在容器和 host 之间拷贝数据,当然我们也可以直接通过 Linux 的 cp 命令复制到/var/lib/docker/volumes/xxx。

(2)容器之间共享数据

第一种方法是将共享数据放在 bind mount 中,然后将其 mount 到多个容器。还是以 httpd 为例,不过

这次的场景复杂些,我们要创建由三个 httpd 容器组成的 web server 集群,它们使用相同的 html 文

件,操作如下:

  1. 将 $HOME/htdocs mount 到三个 httpd 容器。
复制代码
[root@docker ~]# docker run --name web1 -d -p 80 -v ~/htdocs:/usr/local/apache2/htdocs httpd
994e75ef90a80365d7ea7733c727c3b1d24e5423d59a74185304462b7137d774
[root@docker ~]#
[root@docker ~]# docker run --name web2 -d -p 80 -v  ~/htdocs:/usr/local/apache2/htdocs httpd
28ff7c72d18b30123ff7e383db211890a0007e48817cdc73e7f50e9f2233eee6
[root@docker ~]# docker run --name web3 -d -p 80 -v ~/htdocs:/usr/local/apache2/htdocs httpd
160d823d985bad0ce7c27a0d301956f3c7ab4e9ca34c5be2601b3374fa30f6f1
  1. 查看当前主页内容。
复制代码
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
160d823d985b httpd "httpd-foreground" 13 seconds ago Up 12 seconds
0.0.0.0:32770->80/tcp, :::32770->80/tcp web3
28ff7c72d18b httpd "httpd-foreground" 19 seconds ago Up 18 seconds
0.0.0.0:32769->80/tcp, :::32769->80/tcp web2
994e75ef90a8 httpd "httpd-foreground" 29 seconds ago Up 27 seconds
0.0.0.0:32768->80/tcp, :::32768->80/tcp web1

[root@docker ~]# curl 127.0.0.1:32770 		#这个32770端口是host os映射到web3的端口
updated index page!
[root@docker ~]# curl 127.0.0.1:32769
updated index page!
[root@docker ~]# curl 127.0.0.1:32768
updated index page!
  1. 修改 volume 中的主页文件,再次查看并确认所有容器都使用了新的主页。
复制代码
[root@docker ~]# echo "This is a new index page for web cluster" >  ~/htdocs/index.html
[root@docker ~]# curl 127.0.0.1:32770
This is a new index page for web cluster
[root@docker ~]# curl 127.0.0.1:32769
This is a new index page for web cluster
[root@docker ~]# curl 127.0.0.1:32768
This is a new index page for web cluster

7.4 volume container共享数据

volume container 是专门为其他容器提供 volume 的容器它提供的卷可以是 bind mount,也可以是

docker managed volume。下面我们创建一个 volume container:

复制代码
[root@docker ~]# docker create --name vc_data    -v ~/htdocs/:/usr/local/apache2/htdocs    -v /other/userful/tools   busybox
bb4a33bf34aa7817ea752db83079aff15a5dfd77c23363f2e8dd3096d1cb910f

我们将容器命名为 vc_data (vc 是 volume container 的缩写)。注意这里执行的是 docker create命令,这是因为 volume container 的作用只是提供数据,它本身不需要处于运行状态。容器 mount 了两个 volume:

  1. bind mount,存放 web server 的静态文件。

  2. docker managed volume,存放一些实用工具(当然现在是空的,这里只是做个示例)。

通过 docker inspect 可以查看到这两个 volume。

复制代码
[root@docker ~]# docker inspect vc_data
...
"Mounts": [
{
"Type": "volume",
"Name":
"4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a7a9557e0",
"Source":
"/var/lib/docker/volumes/4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a
7a9557e0/_data",
"Destination": "/other/userful/tools",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
},
{
"Type": "bind",
"Source": "/root/htdocs",
"Destination": "/usr/local/apache2/htdocs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],
...

其他容器可以通过 --volumes-from 使用 vc_data 这个 volume container:

复制代码
[root@docker ~]# docker run --name web1 -d -p 80 --volumes-from vc_data httpd
362f039b562e4bf0020c38839a69bfd62733f27fda5481eb5b58178f41318ffe
[root@docker ~]#
[root@docker ~]# docker run --name web2 -d -p 80 --volumes-from vc_data httpd
217c1a243bb9a1f32ccae9ae2792e3720263d36402bbac58f43cfb068bc21e66
[root@docker ~]#
[root@docker ~]# docker run --name web3 -d -p 80 --volumes-from vc_data httpd
ce00b5fa458fc1af01e82ce8b43e8c072c858a669c1298add8053cbc7ac9f49b
[root@docker ~]#

三个 httpd 容器都使用了 vc_data,看看它们现在都有哪些 volume,以 web1 为例:

复制代码
[root@docker ~]# docker inspect web1
...
"Mounts": [
{
"Type": "bind",
"Source": "/root/htdocs",
"Destination": "/usr/local/apache2/htdocs",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "volume",
"Name":
"4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a7a9557e0",
"Source":
"/var/lib/docker/volumes/4500300655eb8e32a5d88174395a61ea394e17266fbde60f224d815a
7a9557e0/_data",
"Destination": "/other/userful/tools",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
...

web1 容器使用的就是 vc_data 的 volume,而且连 mount point 都是一样的。验证一下数据共享的效果:

复制代码
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
ce00b5fa458f httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32773->80/tcp, :::32773->80/tcp web3
217c1a243bb9 httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32772->80/tcp, :::32772->80/tcp web2
362f039b562e httpd "httpd-foreground" 2 minutes ago Up 2 minutes
0.0.0.0:32771->80/tcp, :::32771->80/tcp web1

[root@docker ~]# echo "This content is from a volume container!" >
~/htdocs/index.html

[root@docker ~]# curl 127.0.0.1:32773
This content is from a volume container!
[root@docker ~]# curl 127.0.0.1:32772
This content is from a volume container!
[root@docker ~]# curl 127.0.0.1:32771
This content is from a volume container!

可见,三个容器已经成功共享了 volume container 中的 volume。

下面我们讨论一下 volume container 的特点:

  1. 与 bind mount 相比,不必为每一个容器指定 host path,所有 path 都在 volume container 中定义好了,容器只需与 volume container 关联,实现了容器与 host 的解耦

  2. 使用 volume container 的容器其 mount point 是一致的,有利于配置的规范和标准化,但也带来一定的局限,使用时需要综合考虑。

另一种在容器之间共享数据的方式是 data-packed volume container,下一节讨论。

7.5 容器之间共享数据的方式:data-packed volume container

在上一节的例子中 volume container 的数据归根到底还是在 host 里,有没有办法将数据完全放到volume container 中,同时又能与其他容器共享呢?

当然可以,通常我们称这种容器为 data-packed volume container。其原理是将数据打包到镜像中,然后通过 docker managed volume 共享。

我们用下面的 Dockfile 构建镜像:

复制代码
[root@docker ~]# vim Dockerfile
FROM busybox:latest
ADD htdocs /usr/local/apache2/htdocs
VOLUME /usr/local/apache2/htdocs

ADD 将静态文件添加到容器目录 /usr/local/apache2/htdocs。

VOLUME 的作用与 -v 等效,用来创建 docker managed volume,mount point 为/usr/local/apache2/htdocs,因为这个目录就是 ADD 添加的目录,所以会将已有数据拷贝到 volume中。

修改文本内容

复制代码
[root@docker ~]# echo "This content is from a data packed volume container!" >  htdocs/index.html

build 新镜像 datapacked:

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

用新镜像创建 data-packed volume container:

复制代码
[root@docker ~]# docker create --name vc_data datapacked
55dc035deeb31907830ea45f7af5e209c7e48078d59732798c529e7445d2a196

因为在 Dockerfile 中已经使用了 VOLUME 指令,这里就不需要指定 volume 的 mount point 了。启动httpd 容器并使用 data-packed volume container:

复制代码
[root@docker ~]# docker run -d -p 80:80 --volumes-from vc_data httpd
98db78fc661441ec7ce0a2b76d62675fccf3389b7768cc9744b852645323c33b
[root@docker ~]#
[root@docker ~]# curl 127.0.0.1:80
This content is from a data packed volume container!

容器能够正确读取 volume 中的数据。data-packed volume container 是自包含的,不依赖 host 提供数据,具有很强的移植性,非常适合 只使用 静态数据的场景,比如应用的配置信息、web server 的静态文件等。

容器数据共享就讨论到这里,下一节我们学习如何对 data volume 的生命周期进行管理。

7.6 volume生命周期管理

Data Volume 中存放的是重要的应用数据,如何管理 volume 对应用至关重要。前面我们主要关注的是

volume 的创建、共享和使用,本节将讨论如何备份、恢复、迁移和销毁 volume。

(1).备份

因为 volume 实际上是 host 文件系统中的目录和文件,所以 volume 的备份实际上是对文件系统的备

份。还记得前面我们是如何搭建本地 Registry 的吗?

复制代码
[root@docker ~]# docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry
registry:2
Unable to find image 'registry:2' locally
2: Pulling from library/registry
1cc3d825d8b2: Pull complete
85ab09421e5a: Pull complete
40960af72c1c: Pull complete
e7bb1dbb377e: Pull complete
a538cc9b1ae3: Pull complete
Digest: sha256:ac0192b549007e22998eb74e8d8488dcfe70f1489520c3b144a6047ac5efbe90
Status: Downloaded newer image for registry:2
a8214201171f28a13803c647ef84acdc982465c230cb5c6a034bd05574948408
[root@docker ~]#

所有的本地镜像都存在 host 的 /myregistry 目录中,我们要做的就是定期备份这个目录。

(2).恢复

volume 的恢复也很简单,如果数据损坏了,直接用之前备份的数据拷贝到 /myregistry 就可以了。

(3).迁移

如果我们想使用更新版本的 Registry,这就涉及到数据迁移,方法是:

  1. docker stop 当前 Registry 容器。

  2. 启动新版本容器并 mount 原有 volume。

docker run -d -p 5000:5000 -v /myregistry:/var/lib/registry registry:latest

当然,在启用新容器前要确保新版本的默认数据路径是否发生变化。

(4).销毁

可以删除不再需要的 volume,但一定要确保知道自己正在做什么,volume 删除后数据是找不回来的。

docker 不会销毁 bind mount,删除数据的工作只能由 host 负责。对于 docker managed volume,在

执行 docker rm 删除容器时可以带上 -v 参数,docker 会将容器使用到的 volume 一并删除,但前提

是没有其他容器 mount 该 volume,目的是保护数据,非常合理。

如果删除容器时没有带 -v 呢?这样就会产生孤儿 volume,好在 docker 提供了 volume 子命令可以对

docker managed volume 进行维护。请看下面的例子:

复制代码
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
[root@docker ~]#
[root@docker ~]# docker run --name bbox -v /test/data busybox
[root@docker ~]#
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1

容器 bbox 使用的 docker managed volume 可以通过 docker volume ls 查看到。

删除 bbox:

复制代码
[root@docker ~]# docker rm bbox
bbox
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME
local 8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
[root@docker ~]#

因为没有使用 -v ,volume 遗留了下来。对于这样的孤儿 volume,可以用 docker volume rm 删除:

复制代码
[root@docker ~]# docker volume rm       8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
8645b8e2edb91820799e6e8807d8b3476f1c5cec40beb14240b0e5143050e5e1
[root@docker ~]# docker volume ls
DRIVER VOLUME NAME

如果想批量删除孤儿 volume,可以执行:docker volume rm $(docker volume ls -q) 或者 docker volume prune

两者的核心区别(关键风险点):

命令 作用 是否询问确认 是否危险
docker volume prune 只删除无主的卷(没有任何容器引用它) (需手动输入y) 安全
docker volume rm $(docker volume ls -q) 强制删除所有卷(无论是否有容器在用) (直接执行) 极其危险

小结

本章我们学习了以下内容:

  1. docker 为容器提供了两种存储资源:数据层和 Data Volume。

  2. 数据层包括镜像层和容器层,由 storage driver 管理。

  3. Data Volume 有两种类型:bind mount 和 docker managed volume。

  4. bind mount 可实现容器与 host 之间,容器与容器之间共享数据。

  5. volume container 是一种具有更好移植性的容器间数据共享方案,特别是 data-packed volume

container。

  1. 最后我们学习了如何备份、恢复、迁移和销毁 Data Volume。

7.7 实战:安装mysql

复制代码
#docker hub上查找mysql镜像
[root@docker ~]# docker search mysql

#从华为云加速器拉取mysql镜像到本地标签为5.7
[root@docker ~]# docker pull mysql:5.7
5.7: Pulling from library/mysql
20e4dcae4c69: Pull complete
1c56c3d4ce74: Pull complete
e9f03a1c24ce: Pull complete
68c3898c2015: Pull complete
6b95a940e7b6: Pull complete
90986bb8de6e: Pull complete
ae71319cb779: Pull complete
ffc89e9dfd88: Pull complete
43d05e938198: Pull complete
064b2d298fba: Pull complete
df9a4d85569b: Pull complete
Digest: sha256:4bc6bc963e6d8443453676cae56536f4b8156d78bae03c0145cbe47c2aad73bb
Status: Downloaded newer image for mysql:5.7
docker.io/library/mysql:5.7

使用mysql镜像

简单版

复制代码
[root@docker ~]# docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d
mysql:5.7
cf4065b04c9d68bb02ef8ccb74b8b02f357f588680328556afcf008dd69c7138

[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
cf4065b04c9d mysql:5.7 "docker-entrypoint.s…" 38 seconds ago Up 36
seconds 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp elated_gauss

[root@docker ~]# docker exec -it cf4065b04c9d bash
bash-4.2# mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.44 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>

建库建表插入数据

复制代码
mysql> CREATE DATABASE db01; 		#创建数据库叫db01
Query OK, 1 row affected (0.00 sec)
mysql> USE db01; 			#使用db01;
Database changed
mysql> CREATE TABLE tablea(id int,name varchar(20));
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO tablea VALUES(1,'gqd');
Query OK, 1 row affected (0.14 sec)
mysql> SELECT * FROM tablea;
+------+------+
| id | name |
+------+------+
| 1 | gqd |
+------+------+
1 row in set (0.00 sec)
mysql>

外部windows连接运行在docker上的mysql容器实例服务

安装 navicat170_premium_cs_x64.exe ,一直下一步直到安装完成

双击桌面图标打开

插入中文试试

为什么报错?

docker上默认字符集不支持中文

复制代码
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | latin1 |
| character_set_connection | latin1 |
| character_set_database | latin1 |
| character_set_filesystem | binary |
| character_set_results | latin1 |
| character_set_server | latin1 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

删除容器后,里面的mysql数据在不在?

复制代码
# 删除mysql容器
[root@docker ~]# docker rm -f cf4065b04c9d

容器实例删除,你还有什么?删库到跑路?

再用同样的方式创建一个mysql,数据还在么?

复制代码
[root@docker ~]# docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456
mysql:5.7
1e1a206582f2c7cf79a3c764b2c441b49d752146d87b09681f6e84dba1232361

实战版

新建mysql实例

复制代码
[root@docker ~]# docker run -d -p 3306:3306 --privileged=true -v
/gqd/mysql/log:/var/log/mysql -v /gqd/mysql/data:/var/lib/mysql -v
/gqd/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql
mysql:5.7
ff28918675ccf77947aba0b9602d40edfee1a4e9ff3122288d4c84d7c80e3f49

新建my.cnf,通过容器卷同步给mysql容器实例

复制代码
[root@docker ~]# cd /gqd/mysql/conf
[root@docker conf]# ls

[root@docker conf]# vim my.cnf 			#实现mysql支持中文
[client]
default_character_set=utf8
[mysqld]
collation_server = utf8_general_ci
character_set_server = utf8
[root@docker conf]#

重新启动mysql容器实例再重新进入并查看字符编码

复制代码
[root@docker conf]# docker restart mysql
mysql

[root@docker conf]# docker exec -it mysql bash
bash-4.2# mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.44 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+----------------------------+
| Variable_name | Value |
+--------------------------+----------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.01 sec)
mysql> CREATE DATABASE db01; #创建数据库叫db01
Query OK, 1 row affected (0.00 sec)
mysql> USE db01; #使用db01;
Database changed
mysql> CREATE TABLE tablea(id int,name varchar(20));
Query OK, 0 rows affected (0.04 sec)
mysql> INSERT INTO tablea VALUES(1,'gqd');
Query OK, 1 row affected (0.14 sec)
mysql> SELECT * FROM tablea;
+------+------+
| id | name |
+------+------+
| 1 | gqd |
+------+------+
1 row in set (0.00 sec)
mysql>

在新建库新建表再插入中文测试

不报错了!

假如当前容器实例删除,再重新来一次,之前创建的db01实例还有吗??赶紧动起来尝试一下吧!!!

再次删库跑路!!!

复制代码
[root@docker ~]# docker rm -f ff28918675cc

#重新创建容器,看看数据还在不在
[root@docker ~]# docker run -d -p 3306:3306 --privileged=true -v
/gqd/mysql/log:/var/log/mysql -v /gqd/mysql/data:/var/lib/mysql -v
/gqd/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456 --name mysql
mysql:5.7
507cd722fc40b75f5eb0202c09eb9276a7046b64797864eb5bba15144e01f661

发现数据还在

8.容器监控

Docker自带的监控子命令

当Docker部署规模逐步变大后,可视化监控容器环境的性能和健康状态将会变得越来越重要。

ps

docker ps 是我们早已熟悉的命令了,方便我们查看当前运行的容器。前面已经有大量示例,这里就不

赘述了。

复制代码
[root@docker ~ 11:16:55]# docker ps
CONTAINER ID   IMAGE     COMMAND              CREATED        STATUS              PORTS                               NAMES
adfe0329007c   httpd     "httpd-foreground"   23 hours ago   Up About a minute   0.0.0.0:80->80/tcp, :::80->80/tcp   gifted_kilby

top

查看容器的进程

docker top 查看容器进程,这个输出信息量极大,它直接撕开了容器的"外壳",让你看到了容器内部真正的进程树

复制代码
[root@docker ~ 11:18:01]# docker top gifted_kilby 		#sshtest是容器名
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                2177                2156                0                   11:15               ?                   00:00:00            httpd -DFOREGROUND
33                  2200                2177                0                   11:15               ?                   00:00:00            httpd -DFOREGROUND
33                  2201                2177                0                   11:15               ?                   00:00:00            httpd -DFOREGROUND
33                  2202                2177                0                   11:15               ?                   00:00:00            httpd -DFOREGROUND

列名			含义			具体到你这张表
UID		运行该进程的用户 ID			root(主进程)和 33(即 www-data 用户,Debian系默认)
PID		在宿主机视角下的进程 ID(注意:不是容器内部的 PID)	2177, 2200, 2201, 2202
PPID	在宿主机视角下的父进程 ID	2156(主进程的爹),2177(工作进程的爹)
C	CPU 使用率(百分比整数)	全是 0,说明刚启动,非常空闲
STIME	进程启动时间	11:15
TTY	关联的终端(? 表示无终端,后台守护)	全是 ?
TIME	累计占用 CPU 时间	都是 0,没干活
CMD	启动命令	httpd -DFOREGROUND(前台运行模式)

命令后面还可以跟上 Linux 操作系统 ps 命令的参数显示特定的信息,比如 -au 。

复制代码
[root@docker ~ 11:33:38]# docker top gifted_kilby -au
USER                PID                 %CPU                %MEM                VSZ                 RSS                 TTY                 STAT                START               TIME                COMMAND
root                2177                0.0                 0.2                 6200                4868                ?                   Ss                  11:15               0:00                httpd -DFOREGROUND
33                  2200                0.0                 0.6                 1997376             12132               ?                   Sl                  11:15               0:00                httpd -DFOREGROUND
33                  2201                0.0                 0.6                 1997376             12112               ?                   Sl                  11:15               0:00                httpd -DFOREGROUND
33                  2202                0.0                 0.6                 1997376             12172               ?                   Sl                  11:15               0:00                httpd -DFOREGROUND

stats

列出容器资源使用率

复制代码
[root@docker ~ 11:34:11]# docker stats --help

Usage:  docker stats [OPTIONS] [CONTAINER...]

Display a live stream of container(s) resource usage statistics

Aliases:
  docker container stats, docker stats

Options:
  -a, --all             Show all containers (default shows just running)
      --format string   Format output using a custom template:
                        'table':            Print output in table format with column headers
                        (default)
                        'table TEMPLATE':   Print output in table format using the given Go template
                        'json':             Print in JSON format
                        'TEMPLATE':         Print output using the given Go template.
                        Refer to https://docs.docker.com/go/formatting/ for more information about
                        formatting output with templates
      --no-stream       Disable streaming stats and only pull the first result
      --no-trunc        Do not truncate output

示例:

复制代码
[root@docker ~ 11:16:36]# docker stats 

CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
adfe0329007c   gifted_kilby   0.04%     32.86MiB / 1.706GiB   1.88%     1.25kB / 0B   21.5MB / 0B   82

CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
adfe0329007c   gifted_kilby   0.00%     32.86MiB / 1.706GiB   1.88%     1.25kB / 0B   21.5MB / 0B   82

CONTAINER ID   NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O       BLOCK I/O     PIDS
adfe0329007c   gifted_kilby   0.00%     32.86MiB / 1.706GiB   1.88%     1.25kB / 0B   21.5MB / 0B   82

默认会显示一个实时变化的列表,展示每个容器的 CPU 使用率,内存使用量和可用量,网络和磁盘的 IO数据。

注意:容器启动时如果没有特别指定内存 limit,stats 命令会显示 host 的内存总量,但这并不意味着每个 container 都能使用到这么多的内存

cAdvisor

cAdvisor 是 google 开发的容器监控工具。

复制代码
[root@docker ~]# docker run \
--volume=/:/rootfs:ro \
--volume=/var/run:/var/run:rw \
--volume=/sys:/sys:ro \
--volume=/var/lib/docker/:/var/lib/docker:ro \
--publish=8080:8080 \
--detach=true \
--name=cadvisor \
google/cadvisor:latest

也可以使用镜像hub.c.163.com/xbingo/cadvisor:latest。

通过 http://Host_IP:8080 访问 cAdvisor。首次打开比较慢,系统需要收集数据并绘制图表。

点击Docker Containers进去看容器具体信息

总结:

缺点:操作界面略显简陋,而且需要在不同页面之间跳转,并且只能监控一个 host。

优点:可以将监控到的数据导出给第三方工具,由这些工具进一步加工处理。

结论:我们把 cAdvisor 定位为一个监控数据收集器,并导出数据给第三方工具,而非展示数据。

9. 容器日志

高效的监控和日志管理对保持生产系统持续稳定地运行以及排查问题至关重要。

在微服务架构中,由于容器的数量众多以及快速变化的特性使得记录日志和监控变得越来越重要。考虑

到容器短暂和不固定的生命周期,当我们需要 debug 问题时有些容器可能已经不存在了。因此,一套集

中式的日志管理系统是生产环境中不可或缺的组成部分。

Docker logs

对于一个运行的容器,Docker 会将日志发送到 容器标准输出设备(STDOUT)和标准错误设备

(STDERR),STDOUT 和 STDERR 实际上就是容器的控制台终端。

举个例子,用下面的命令运行 httpd 容器:

复制代码
[root@docker ~ 17:23:46]# docker run -p 80:80 httpd
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. 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.2. Set the 'ServerName' directive globally to suppress this message
[Fri Aug 07 09:23:55.477519 2026] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.68 (Unix) configured -- resuming normal operations
[Fri Aug 07 09:23:55.477614 2026] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'

我们在启动日志的时候没有用 -d 参数,httpd 容器以前台方式启动,日志会直接打印在当前的终端窗口。

如果加上 -d 参数以后台方式运行容器,我们就看不到输出的日志了。

复制代码
[root@docker ~ 17:24:41]# docker run -d -p 80:80 httpd
41d79a314ca0fcdf29018753440d946f28f8f1c972f74e021455e5e9a562204c

这种情况下如果要查看容器的日志,有两种方法:

  1. attach 到该容器。

  2. 用 docker logs 命令查看日志。

先来看 attach 的方法。运行 docker attach 命令。

复制代码
[root@docker ~ 17:26:35]# docker attach 41d79a314ca0

attach 到了 httpd 容器,但并没有任何输出,这是因为当前没有新的日志信息。

为了产生一条新的日志,可以在 host 的另一个命令行终端执行 curl localhost 。

终端B:

复制代码
[root@docker ~ 17:26:56]# curl localhost
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
[root@docker ~ 17:27:02]# curl localhost
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>

这时,attach 的终端就会打印出新的日志。

终端A:

复制代码
[root@docker ~ 17:26:35]# docker attach 41d79a314ca0
172.17.0.1 - - [07/Aug/2026:09:27:02 +0000] "GET / HTTP/1.1" 200 191
172.17.0.1 - - [07/Aug/2026:09:27:07 +0000] "GET / HTTP/1.1" 200 191
172.17.0.1 - - [07/Aug/2026:09:27:08 +0000] "GET / HTTP/1.1" 200 191

attach 的方法在实际使用中不太方便,因为:

  1. 只能看到 attach 之后的日志,以前的日志不可见。

  2. 退出 attach 状态比较麻烦(Ctrl+p 然后 Ctrl+q 组合键),一不小心很容器将容器杀掉(比如按下Ctrl+C)

查看容器日志推荐的方法是用 docker logs 命令。

复制代码
[root@docker ~ 17:27:30]# docker logs 41d79a314ca0
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. 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.2. Set the 'ServerName' directive globally to suppress this message
[Fri Aug 07 09:25:02.635189 2026] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.68 (Unix) configured -- resuming normal operations
[Fri Aug 07 09:25:02.635271 2026] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
[Fri Aug 07 09:25:34.954153 2026] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. 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.2. Set the 'ServerName' directive globally to suppress this message
[Fri Aug 07 09:26:31.215061 2026] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.68 (Unix) configured -- resuming normal operations
[Fri Aug 07 09:26:31.215228 2026] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
172.17.0.1 - - [07/Aug/2026:09:27:02 +0000] "GET / HTTP/1.1" 200 191
172.17.0.1 - - [07/Aug/2026:09:27:07 +0000] "GET / HTTP/1.1" 200 191
172.17.0.1 - - [07/Aug/2026:09:27:08 +0000] "GET / HTTP/1.1" 200 191
[Fri Aug 07 09:27:29.900741 2026] [mpm_event:notice] [pid 1:tid 1] AH00491: caught SIGTERM, shutting down
[root@docker ~ 17:27:41]# 

docker logs 能够打印出自容器启动以来完整的日志,并且 -f 参数可以继续打印出新产生的日志,效果上与 Linux 命令 tail -f 一样。

10 Docker-compose

我们知道使用一个 Dockerfile 模板文件,可以让用户很方便的定义一个单独的应用容器 。然而,在日常工作中,经常会碰到需要多个容器相互配合来完成某项任务的情况。例如要实现一个 Web 项目,除了Web 服务容器本身,往往还需要再加上后端的数据库服务容器,甚至还包括负载均衡容器等。如果每个容器都要按顺序手动启停,那么维护工作量将会很大,而且工作效率也很低。

Docker Compose 可以轻松、高效地管理容器,它是一个用于定义和运行多容器的管理工具。它通过一个单独的 docker-compose.yml 模板文件(YAML 格式)定义一组相关联资源集。

Compose 中有两个重要的概念:

服务 ( service ):一个应用的容器,实际上可以包括若干运行相同镜像的容器实例。

项目 ( project ):由一组关联的应用容器组成的一个完整业务单元,在 docker-compose.yml 文件中定义。

compose 的默认管理对象是项目,通过子命令对项目中的一组容器进行便捷地生命周期管理。

Compose 项目由 Python 编写(后用Go语言重写),实现上调用了 Docker 服务提供的 API 来对容器进行管理。因此,只要所操作的平台支持 Docker API,就可以在其上利用 Compose 来进行编排管理。

命令说明

复制代码
[root@docker ~ 17:29:47]# docker compose  --help

Usage:  docker compose [OPTIONS] COMMAND

Define and run multi-container applications with Docker

Options:
      --all-resources              Include all resources, even those not used
                                   by services
      --ansi string                Control when to print ANSI control
                                   characters ("never"|"always"|"auto")
                                   (default "auto")
      --compatibility              Run compose in backward compatibility mode
      --dry-run                    Execute command in dry run mode
      --env-file stringArray       Specify an alternate environment file
  -f, --file stringArray           Compose configuration files
      --parallel int               Control max parallelism, -1 for unlimited
                                   (default -1)
      --profile stringArray        Specify a profile to enable
      --progress string            Set type of progress output (auto, tty,
                                   plain, quiet) (default "auto")
      --project-directory string   Specify an alternate working directory
                                   (default: the path of the, first specified,
                                   Compose file)
  -p, --project-name string        Project name

Commands:
  attach      Attach local standard input, output, and error streams to a service's running container
  build       Build or rebuild services
  config      Parse, resolve and render compose file in canonical format
  cp          Copy files/folders between a service container and the local filesystem
  create      Creates containers for a service
  down        Stop and remove containers, networks
  events      Receive real time events from containers
  exec        Execute a command in a running container
  images      List images used by the created containers
  kill        Force stop service containers
  logs        View output from containers
  ls          List running compose projects
  pause       Pause services
  port        Print the public port for a port binding
  ps          List containers
  pull        Pull service images
  push        Push service images
  restart     Restart service containers
  rm          Removes stopped service containers
  run         Run a one-off command on a service
  scale       Scale services 
  start       Start services
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop services
  top         Display the running processes
  unpause     Unpause services
  up          Create and start containers
  version     Show the Docker Compose version information
  wait        Block until the first service container stops
  watch       Watch build context for service and rebuild/refresh containers when files are updated

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

version

复制代码
[root@docker ~ 17:29:53]# docker compose version 
Docker Compose version v2.27.0
[root@docker ~ 17:30:23]# 

Compose 模板

模板文件是使用 Compose 的核心,涉及到的指令关键字也比较多。但大家不用担心,这里面大部分指令docker run 相关参数的含义都是类似的。默认的模板文件名称为 docker-compose.yml ,格式为 YAML 格式。

模板文件结构

version:用来定义模板文件的版本,不同版本的模板,格式也不一样。

资源列表:用来定义资源清单,包括service、secret、network、volume等。

注释行: # 开头的注释行。

示例:使用版本2模板,定义一个使用httpd镜像的services。

复制代码
version: "2"
services:
webapp:
image: httpd

详细结构参考官方

Compose文档

实战-Wordpress

回顾一下如果用docker run改如何操作:

复制代码
[root@docker ~ 17:30:23]# docker run -it -d --name db --restart always -v /db:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=huawei -e MYSQL_DATABASE=wordpress mysql
Unable to find image 'mysql:latest' locally
latest: Pulling from library/mysql
30627cea5424: Pull complete 
7e887550bdc4: Pull complete 
35475b275575: Pull complete 
27683f99b921: Pull complete 
0bb65eb170f9: Pull complete 
e480dcc782ea: Pull complete 
1791a4d7fecf: Pull complete 
71fa527c6c68: Pull complete 
c4e766e27938: Pull complete 
feffc3e2a7dd: Pull complete 
Digest: sha256:66aec17cd21a956029b83f083b813073859e8355dc1a00e55df6ba02f0e32345
Status: Downloaded newer image for mysql:latest
8724b893c3a349d07f01090b393a61ee1218d28d613a3c885350a7b3c436fbaa

[root@docker ~ 17:33:15]# docker run -tid --name blog -v /web:/var/www/html -p 80:80 --link db -e WORDPRESS_DB_HOST=db -e WORDPRESS_DB_USER=root -e  WORDPRESS_DB_PASSWORD=huawei -e WORDPRESS_DB_NAME=wordpress wordpress
Unable to find image 'wordpress:latest' locally
latest: Pulling from library/wordpress
26c307b5e35a: Pull complete 
0494d45beee5: Pull complete 
60e3a14f5e69: Pull complete 
2bfabad6b2b2: Pull complete 
cb855eea19d7: Pull complete 
84afdefd22c9: Pull complete 
a4213aad90eb: Pull complete 
329c983b5eca: Pull complete 
c9fcf2751ddc: Pull complete 
c04ad35fdccc: Pull complete 
9f76894aae6e: Pull complete 
2f0f7142dcd0: Pull complete 
b0d1a6e64d6a: Pull complete 
4401218f47e8: Pull complete 
4f4fb700ef54: Pull complete 
f2773aff8958: Pull complete 
92f555f10c62: Pull complete 
48b1128f0eb0: Pull complete 
adb4b210e521: Pull complete 
78d625816e7c: Pull complete 
703c7f88374a: Pull complete 
29b35873ce58: Pull complete 
20bba2543656: Pull complete 
5fb8f4dde2c2: Pull complete 
Digest: sha256:b2d7e3153c8a96f90305a3102fb6439335237fb1a9655b617d15c5168ce2f7a3
Status: Downloaded newer image for wordpress:latest
bb95a68011a0dfa527d9102c4cc9a19ea730c2a8d815f741497cf0e4e76ad5b4

[root@docker ~ 17:34:13]# docker ps -a
CONTAINER ID   IMAGE       COMMAND                  CREATED              STATUS              PORTS                               NAMES
bb95a68011a0   wordpress   "docker-entrypoint.s…"   32 seconds ago       Up 31 seconds       0.0.0.0:80->80/tcp, :::80->80/tcp   blog
8724b893c3a3   mysql       "docker-entrypoint.s…"   About a minute ago   Up About a minute   3306/tcp, 33060/tcp                 db

测试效果:

通过docker compose来统一管理这两个容器呢?

假设新建一个名为 wordpress 的文件夹,然后进入这个文件夹,创建 docker-compose.yml 文件

复制代码
# 删除之前的环境
[root@docker ~ 17:34:44]# docker rm -f $(docker ps -aq)
bb95a68011a0
8724b893c3a3

# 通过docker compose实现多个容器一起启动
[root@docker ~ 17:35:52]# mkdir wordpress
[root@docker ~ 17:36:05]# cd wordpress/

[root@docker wordpress]# vim docker-compose.yml
# Docker Compose 配置文件版本(使用最新稳定版)
# 使用命令启动: docker-compose up -d
# 使用命令停止: docker-compose down
version: '3.9'

services:
  # ------------------ WordPress 应用服务 ------------------
  blog:                                 # 服务名称(容器名的一部分)
    image: wordpress:latest             # 使用的镜像及标签
    restart: always                     # 容器退出时总是自动重启(保证高可用)
    links:                              # 连接其他服务(在容器内部网络可通过服务名访问)
      - db                              # 表示 blog 服务可以通过 db 这个主机名访问 MySQL
    ports:                              # 端口映射(宿主机:容器内)
      - "80:80"                         # 将宿主机的 80 端口映射到容器的 80 端口(Apache)
    environment:                        # 环境变量(WordPress 安装配置)
      - WORDPRESS_DB_HOST=db            # 数据库主机地址(值对应上面 links 的 db 服务名)
      - WORDPRESS_DB_USER=root          # 数据库用户名(与下方 db 服务一致)
      - WORDPRESS_DB_PASSWORD=huawei    # 数据库密码(必须与下方 MYSQL_ROOT_PASSWORD 完全一致)
      - WORDPRESS_DB_NAME=wordpress     # 数据库名(与下方 MYSQL_DATABASE 完全一致)
    depends_on:                         # 依赖关系(等待 db 服务启动后再启动 blog)
      - db

  # ------------------ MySQL 数据库服务 ------------------
  db:                                   # 服务名称(被 blog 通过 links 引用)
    image: mysql:latest                 # 使用的镜像及标签(建议指定具体版本,如 mysql:8.0)
    restart: always                     # 容器退出时自动重启
    environment:                        # 环境变量(MySQL 初始化配置)
      - MYSQL_ROOT_PASSWORD=huawei      # MySQL root 用户的密码(必填)
      - MYSQL_DATABASE=wordpress        # 自动创建的数据库名(必须与 WORDPRESS_DB_NAME 一致)
    volumes:                            # 数据持久化(容器删除数据不丢失)
      - db_data:/var/lib/mysql          # 将容器内 MySQL 数据目录挂载到命名卷 db_data

# 定义持久化卷(让数据独立于容器生命周期)
volumes:
  db_data:                              # 对应上面 db 服务挂载的卷名

[root@docker wordpress 17:37:33]# docker compose  config -q  		#检测语法
no configuration file provided: not found
[root@docker wordpress 17:37:45]# ls
docker_compose.yaml


你遇到的错误 no configuration file provided: not found,是因为 docker compose 默认只识别 docker-compose.yml、compose.yaml、docker-compose.yaml 等标准文件名,而你当前的文件名为 docker_compose.yaml(用的是下划线 _,不是连字符 -),所以 Docker Compose 找不到配置。
[root@docker wordpress 17:37:53]# mv docker_compose.yaml docker-compose.yaml 
[root@docker wordpress 17:39:16]# ls
docker-compose.yaml


#后端运行
[root@docker wordpress 17:39:17]# docker compose  up -d
WARN[0000] /root/wordpress/docker-compose.yaml: `version` is obsolete 
[+] Running 4/4
 ✔ Network wordpress_default   Created                                       0.2s 
 ✔ Volume "wordpress_db_data"  Created                                       0.0s 
 ✔ Container wordpress-db-1    Started                                       0.3s 
 ✔ Container wordpress-blog-1  Started                                       0.7s 

查看现象

复制代码
[root@docker wordpress 17:39:33]# docker ps
CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS         PORTS                               NAMES
1976f721aa96   wordpress:latest   "docker-entrypoint.s…"   10 seconds ago   Up 9 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   wordpress-blog-1
46392f4f35ba   mysql:latest       "docker-entrypoint.s…"   10 seconds ago   Up 9 seconds   3306/tcp, 33060/tcp                 wordpress-db-1

11 docker图形界面管理

(1)DockerUI 容器管理器的安装与使用

简介:

DockerUI是一个易用且轻量化的 Docker 管理工具,通过 Web 界面的操作,更方便对于 Docker 指令不熟悉的用户更容易操作 Docker。

功能:

Docker主机管理:数据卷管理,镜像管理,容器管理,构建管理,仓库配置管理,网络配置管理

Docker Swarm集群管理:集群概要信息,节点管理,Service管理,任务管理,密码管理,配置管理

镜像:

我今天分享的这个镜像是来自于这位大佬@joinsunsoft 的,他发布在Docker Hub的镜像地址为:https://hub.docker.com/r/joinsunsoft/docker.ui

安装

启动容器并映射8999端口:

复制代码
[root@docker wordpress 17:39:42]# docker run -d --name docker.ui --restart always -v  /var/run/docker.sock:/var/run/docker.sock -p 8999:8999 joinsunsoft/docker.ui
Unable to find image 'joinsunsoft/docker.ui:latest' locally
latest: Pulling from joinsunsoft/docker.ui
213ec9aee27d: Pull complete 
362df81e5664: Pull complete 
c3f864fca442: Pull complete 
5a55080a5af0: Pull complete 
497ed8376dfb: Pull complete 
4642bc86bdfb: Pull complete 
af43397a88d3: Pull complete 
Digest: sha256:1b70b86be661e931e112d8b59850eeac927966679c52b46064d67df2c489d7a5
Status: Downloaded newer image for joinsunsoft/docker.ui:latest
4007aa1434442171d3230eef9fe2666dc441e73cdca8e25d58fffc136c710b5a
[root@docker wordpress 17:41:05]# 

启动效果

你可以访问:http://192.168.108.30:8999

默认用户名密码:ginghan/123456

DockerUI的主界面(概览),它展示的信息还是很多的,除了容器相关,还有资源占用情况等信息。不得不说还是国人更懂国人,UI界面还是很漂亮的对吧~

(2) Docker 图形化界面管理工具 Portainer

Portainer 是一个 Docker 图形化管理工具,可以通过 Web UI 轻松的管理容器、镜像、网络、卷。同时上手难度也更大一些

Portainer 分为社区版和商业版,本文安装的是社区版(Portainer CE),该版本免费,比较适合个人用户使用

安装

复制代码
#1.创建存储卷
[root@docker ~ 17:43:56]# docker volume create portainer_data
portainer_data

#2.通过docker安装Portainer
[root@docker ~ 17:44:11]# docker run -d -p 8000:8000 -p 9443:9443 --name port --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data portainer/portainer-ce:latest
Unable to find image 'portainer/portainer-ce:latest' locally
latest: Pulling from portainer/portainer-ce
a6015988a0f3: Pull complete 
c0543fe2c6ef: Pull complete 
6b472224a244: Pull complete 
a70dc7213cc7: Pull complete 
16bafc33c96f: Pull complete 
ff9f211b5321: Pull complete 
4f4fb700ef54: Pull complete 
Digest: sha256:f6bc23d1695530a609563fd65c180aaafec0fc02e019d5fc63d16b6fbe83ad
Status: Downloaded newer image for portainer/portainer-ce:latest
a0105aebc0613dc4c77f99c1356bc5c977ae063d101df0bbc066145689904b8f


portainer_data:前面没有 / 或 ./,Docker 会把它识别为一个数据卷(Volume)的名字。!!!!!!!!!!!!!!!!!!
行为:
Docker 检查是否存在名为 portainer_data 的卷。
如果不存在 → 自动创建一个空的卷。
将容器内的 /data 目录挂载到这个卷上。

	-v 后面跟纯名字(如 portainer_ddata),Docker 就认为你在引用一个“数据卷(Volume)”,如果它不存在,Docker 会贴心地帮你自动创建;如果前面带了 / 或 ./,Docker 就认为你在引用宿主机上的“文件夹路径”,它不会帮你创建,必须提前存在。 🚀

	前面已经创建好了命令卷portainer_data,这样的好处就是指定这个命名卷的名字为portainer_data,更容易看和找到,如果不指定,系统会自动创建一个并随机用一大串数字命名

9443 端口默认会启用 SSL,如果需要直接通过 http 访问,需要加上 -p 9000:9000 访问 9000 端口

这里, -v /var/run/docker.sock:/var/run/docker.sock 参数使得Portainer能够访问Docker

守护进程,从而能够管理容器。 -v portainer_data:/data 参数则用于持久化Portainer的数据。

访问

通过 https://ip:9443 访问 Portainer,首次访问需要创建管理员账号

创建用户及密码和从日志中查看tocken登录上去

复制代码
[root@docker _data 17:58:53]# docker logs portainer 2>&1 | grep "setup_token"
2026/08/07 09:58AM INF github.com/portainer/portainer/api/cmd/portainer/main.go:256 > no administrator account configured; admin initialization and backup restore require this setup token in the X-Setup-Token header. Start with --no-setup-token to disable. | setup_token=5c15320e959e871c57de5db231120d8d1e09e2cce7723850dc68c4e234d53d74


#复制这个填上去setup_token=5c15320e959e871c57de5db231120d8d1e09e2cce7723850dc68c4e234d53d74

完成登录后,管理本机直接点击 Get Started

进入主界面可以看到 local,点击 Live connect

连接到该机器后,就可以看到 Docker 相关状态并且可以对其进行管理

12 综合实验

(1)构建WordPress

通过WordPress和mysql镜像构建WordPress应用。

复制代码
#下载镜像:
[root@docker ~]# docker pull mysql
[root@docker ~]# docker pull wordpress

#创建mysql容器,并创建wordpress数据库:
[root@docker ~]# docker run -d -p 3306:3306 \
-v /mysql:/var/lib/mysql:z \
--name mysql \
-e MYSQL_ROOT_PASSWORD=huawei \
-e MYSQL_DATABASE=wordpress \
mysql
6dd12147c9156055042587950e23d543cca47617c3a82bef14c58ffe90580301
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
PORTS NAMES
6dd12147c915 mysql "docker-entrypoint.s…" 15 seconds ago Up 14 seconds
0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql

#创建WordPress容器:
[root@docker ~]# docker inspect mysql|grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",
[root@docker ~]# docker run -d -p 80:80 -v /www:/var/www/html:z \
--name wordpress \
-e WORDPRESS_DB_HOST=172.17.0.2 \
-e WORDPRESS_DB_USER=root \
-e WORDPRESS_DB_PASSWORD=huawei \
-e WORDPRESS_DB_NAME=wordpress \
wordpress
63fa3a61f9bae2a03f3c0c21b574cca75c9a58c5c2645a6f702179c63f5f06eb


说明:
-e WORDPRESS_DB_HOST=... (defaults to the IP and port of the linked mysql container)
-e WORDPRESS_DB_USER=... (defaults to "root")
-e WORDPRESS_DB_PASSWORD=... (defaults to the value of the MYSQL_ROOT_PASSWORD
environment variable from the linked mysql container)
-e WORDPRESS_DB_NAME=... (defaults to "wordpress")

验证 :浏览器打开http://192.168.108.30,配置WordPress:

(2) Docker环境下部署Ghost开源内容管理系统

一、Ghost介绍

1.1 Ghost简介

Ghost是一款用于博客、出版物和内容网站的免费且开源的CMS(内容管理系统),它是完全基于

JavaScript编写的。Ghost的主要特点是简单易用、高度可扩展、精美的设计和优秀的性能。

1.2 Ghost特点

简单易用:Ghost的用户界面非常简单和直观,因此非常易于使用。它提供了一个简单的写作体验,让您

专注于写作。

可扩展:Ghost提供了一个强大的API,使得开发者可以轻松地扩展其功能,并将其与其他应用程序集

成。

设计优美:Ghost的设计非常美观和优雅,它为您提供了多种主题和自定义选项来满足您的需求。

性能优越:Ghost的性能非常好,因为它是基于Node.js构建的,使用了非阻塞I/O,可以处理大量的请求

和并发连接。

1.3 Ghost使用场景

Ghost适用于各种类型的博客、出版物和内容网站,包括但不限于以下场景。

个人博客:Ghost提供简单易用的界面,使个人博主能够轻松创建和管理自己的博客,并分享自己的想

法、故事和经验。

新闻网站:Ghost的高度可扩展性使其成为构建新闻网站的理想选择。它具有良好的性能,可以处理大量

的文章和高流量的访问。

音乐/艺术家网站:Ghost的精美设计和优秀的性能使其非常适合用于展示音乐家、艺术家和其他创意人

才的作品和信息。

企业博客:许多企业都拥有自己的博客来发布公司新闻、产品更新、行业见解等内容。Ghost提供了一个

灵活且易于定制的平台,可以满足企业博客的需求。

复制代码
#下载Ghost镜像
[root@docker ~]# docker pull ghost
Using default tag: latest
latest: Pulling from library/ghost
302e3ee49805: Already exists
f4523b1e3485: Pull complete
643a6ed41aef: Pull complete
23663f1b1336: Pull complete
2fad2dcef2d4: Pull complete
3f316e596507: Pull complete
bf2a403ec6b6: Pull complete
9b79a6ed50e6: Pull complete
4f4fb700ef54: Pull complete
dce26c7b2132: Pull complete
Digest: sha256:a8dbbc0bfe9b57148b97ad4fa58064dd58b3bfcd019fe90fa60e5d5cd9b77750
Status: Downloaded newer image for ghost:latest
docker.io/library/ghost:latest

部署Ghost开源内容管理系统

复制代码
#创建数据目录
[root@docker ~]# mkdir -p /data/ghost

#创建Ghost容器
[root@docker ~]# docker run -d \
--restart always \
--name ghost \
-p 2368:2368 \
-v /data/ghost:/var/lib/ghost/content \
-e NODE_ENV=development \
-e url=http://localhost:2368 \
ghost

50462e8dc684336b1124fcf5372cb78019362f78dbffde2838229011084571ca


#目录授权
[root@docker ~]# chmod -R 777 /data/ghost/\

#重启ghost容器
[root@docker ~]# docker restart ghost
ghost

检查Ghost容器状态

检查Ghost容器状态,确保容器正常启动

复制代码
[root@docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED
STATUS PORTS NAMES
50462e8dc684 ghost "docker-entrypoint.s…" About a minute
ago Up 15 seconds 0.0.0.0:2368->2368/tcp, :::2368->2368/tcp ghost

检查Ghost容器运行日志

查Ghost容器运行日志,检查有无报错,确保Ghost服务正常启动。

复制代码
[root@docker ~]# docker logs ghost
[2024-10-08 15:23:03] INFO Ghost is running in development...
[2024-10-08 15:23:03] INFO Listening on: :::2368
[2024-10-08 15:23:03] INFO Url configured as: http://192.168.3.166:2368/
[2024-10-08 15:23:03] INFO Ctrl+C to shut down
[2024-10-08 15:23:03] INFO Ghost server started in 0.4s
[2024-10-08 15:23:03] WARN Database state requires initialisation.
[2024-10-08 15:23:03] INFO Creating table: newsletters
[2024-10-08 15:23:03] INFO Creating table: posts
[2024-10-08 15:23:03] INFO Creating table: posts_meta
[2024-10-08 15:23:03] INFO Creating table: users
[2024-10-08 15:23:03] INFO Creating table: posts_authors
[2024-10-08 15:23:03] INFO Creating table: roles
[2024-10-08 15:23:03] INFO Creating table: roles_users
[2024-10-08 15:23:03] INFO Creating table: permissions
[2024-10-08 15:23:03] INFO Creating table: permissions_users
[2024-10-08 15:23:03] INFO Creating table: permissions_roles
[2024-10-08 15:23:03] INFO Creating table: settings
[2024-10-08 15:23:03] INFO Creating table: tags
[2024-10-08 15:23:03] INFO Creating table: posts_tags
[2024-10-08 15:23:03] INFO Creating table: invites
[2024-10-08 15:23:03] INFO Creating table: brute
[2024-10-08 15:23:03] INFO Creating table: sessions
[2024-10-08 15:23:03] INFO Creating table: integrations
[2024-10-08 15:23:03] INFO Creating table: webhooks
[2024-10-08 15:23:03] INFO Creating table: api_keys
[2024-10-08 15:23:03] INFO Creating table: mobiledoc_revisions
[2024-10-08 15:23:03] INFO Creating table: post_revisions
[2024-10-08 15:23:03] INFO Creating table: members
[2024-10-08 15:23:03] INFO Creating table: products
[2024-10-08 15:23:03] INFO Creating table: offers
[2024-10-08 15:23:03] INFO Creating table: benefits
[2024-10-08 15:23:03] INFO Creating table: products_benefits
[2024-10-08 15:23:03] INFO Creating table: members_products
[2024-10-08 15:23:03] INFO Creating table: posts_products
[2024-10-08 15:23:03] INFO Creating table: members_created_events
[2024-10-08 15:23:57] INFO Scheduling job clean-tokens at 38 6 15 * * *. Next run
on: Wed Oct 09 2024 15:06:38 GMT+0000 (Coordinated Universal Time)
[2024-10-08 15:23:57] INFO Ghost booted in 1.969s
[2024-10-08 15:23:57] INFO Adding offloaded job to the queue
[2024-10-08 15:23:57] INFO Scheduling job update-check at 51 38 18 * * *. Next
run on: Tue Oct 08 2024 18:38:51 GMT+0000 (Coordinated Universal Time)
[2024-10-08 15:23:57] INFO Running milestone emails job on Tue Oct 08 2024
15:24:02 GMT+0000 (Coordinated Universal Time)
[2024-10-08 15:23:57] INFO Ghost URL Service Ready in 2.202s

访问Ghost首页

访问地址: http://192.168.108.30:2368,将IP替换为自己服务器IP地址,如果无法访问到该页

面,注意自己服务器的防火墙是否关闭或者放行相关端口,使用云服务器则还需要设置安全组规

进入账号注册页面

http://192.168.108.30:2368/ghost

相关推荐
Yyyyyy~1 小时前
[Linux]常用指令1
linux
笑一下蒜了.1 小时前
Linux 存储多路径访问手册|multipath.conf 各配置段详解
android·linux·运维
营养充电站1 小时前
C++23 新特性在 CLion 中的实战体验
macos·docker·jupyter·正则表达式
文优1 小时前
Linux 常用命令速查手册(工作版)
linux·运维·docker
小叮当爱咖啡1 小时前
Day1.环境+自动化脚本
运维·自动化
小狼嚎月1 小时前
K8s PV / PVC / StorageClass 有状态应用持久化
云原生·容器·kubernetes
云飞云共享云桌面2 小时前
非标装备厂降本方案|8‑10 人三维研发共用一台主机,兼容SolidWorks等多款 3D 绘图软件
运维·服务器·3d·自动化·负载均衡·制造
重庆小透明2 小时前
深入探寻微服务【第一篇微服务的坏】
微服务·云原生·架构
@CLoudbays_Martin112 小时前
Linux 服务器如何查看是否被植入后门?
linux·服务器·网络·安全·github·ssl