# Docker 笔记Day02:企业级应用镜像构建,Docker文件系统驱动,容器网络,镜像瘦身,容器编排
一、构建企业级应用镜像
本章将构建两个企业级应用镜像:Nginx 和 Tomcat。
1.1 构建 Nginx 镜像(通过远程yum仓库)
创建构建环境
mkdir /nginx
cd /nginx
创建 Nginx 配置文件
创建 Nginx 配置文件,覆盖默认配置,用自定义的配置来启动 Nginx
#这个配置文件名称是固定的,用来替换容器中原先的配置文件
[root@hd1 nginx]# vim default.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
location / {
root /var/lib/nginx/html; #网页根目录
index index.html; #默认首页
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}
这个文件我们会在构建镜像时拷贝到Alpine下的/etc/nginx/conf.d/目录
编写 Dockerfile
当构建文件名为Dockerfile,且就在本目录时,构建镜像可以不使用 -f 指定构建文件
写Dockerfile的时候,我们只需要按照:FROM → RUN → COPY → ENTRYPOINT 的套路来写就行了
[root@hd1 nginx]# vim dockerfile
注意,alpine是基于deban系的,所以我们
FROM alpine:latest
# 替换国内源
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update #更新 Alpine 系统的软件包索引。相当于: yum clean all && yum -y makecache
RUN apk add --no-cache nginx #安装一个nginx软件
COPY default.conf /etc/nginx/http.d/default.conf #拷贝并覆盖原配置文件
EXPOSE 80
# 入口命令必须是长久命令,前台运行
# -g 指定运行方式,daemon off (关闭守护程序)表示前台运行。因为容器要求主进程前台运行,否则容器会立刻退出
ENTRYPOINT ["/usr/sbin/nginx","-g","daemon off;"]
文中为了方便写注释,将run拆开写了。真实最佳实践是:RUN sed -i ‘s/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g’ && apk update && pk add --no-cache nginx
将镜像内部:的源dl-cdn.alpinelinux.org替换为mirrors.aliyun.com/g(国内源)。这一步替换的是 Alpine 系统内部的软件包源(即 apk 源)
--no-cache:不缓存这些包,从而减少镜像体积。
注意:如果 Dockerfile 文件名为
dockerfile,构建时不需要-f指定文件名。
构建并运行
# 构建镜像(-t 指定镜像名和标签,. 表示当前目录)
docker build -t nginx:v20 .
# 查看镜像是否构建成功
docker images | grep nginx
# 基于刚才的镜像启动容器
docker run -d -p 8000:80 --name html2 nginx:v20
# 查看具体入口命令(不截断)
docker ps --no-trunc -a
假如我们的基础镜像是rocky:
FROM rocky:latest
#这是阿里云镜像官方的语法
RUN sed -i 's|mirror.rockylinux.org|mirrors.aliyun.com/rockylinux|g' /etc/yum.repos.d/*.repo && \
yum clean all && \
yum makecache -y && \
yum -y install nginx
EXPOSE 80
COPY index.html /usr/share/nginx/html/
#入口命令必须是长久命令,前台运行,-g指定运行方式,daemon off表示前台运行
ENTRYPOINT ["/usr/sbin/nginx","-g","daemon off;"]
将镜像导出为离线压缩包
#-o 指定输出文件的路径和名称。这里指定为当前目录下的 alpine2026.tar.gz
[root@hd1 ~]# docker save -o alpine2026.tar.gz alpine:latest
1.2 构建 Tomcat 镜像(通过宿主机压缩包)
创建构建环境
mkdir tomcat8
cd tomcat8
将 apache-tomcat-8.5.69.tar.gz 和 jdk-8u301-linux-x64.rpm 放到 tomcat8 目录下。
编写 Dockerfile
[root@hd1 tomcat8]# cat dockerfile
FROM centos:v1
LABEL author=kisaki
ADD jdk-8u301-linux-x64.rpm /usr/local/
ADD apache-tomcat-8.5.69.tar.gz /usr/local/
RUN cd /usr/local && rpm -ivh jdk-8u301-linux-x64.rpm #切入软件包所在目录并执行安装
RUN mv /usr/local/apache-tomcat-8.5.69 /usr/local/tomcat8 #将tomcat程序放入一个名称简单的目录
#启动tomcat。因为是短期命令会导致退出,所以加上一个tail -F 持续查看日志,变成一个持续命令
ENTRYPOINT /usr/local/tomcat8/bin/startup.sh && tail -F /usr/local/tomcat8/logs/catalina.out
EXPOSE 8080
注意:基础镜像centos:v1需要提前构建好,也可使用其他可用基础镜像,如 centos:7,可直接使用docker pull从官网拉取
构建并运行
注意:当构建文件名为Dockerfile,且就在本目录时,构建镜像可以不使用 -f 指定构建文件
# 构建镜像
docker build -t="tomcat8:v1" .
# 查看生成的镜像
docker images | grep tomcat
REPOSITORY TAG IMAGE ID CREATED SIZE
mytomcat v1 57e91fcd0e64 11 seconds ago 707MB
# 运行容器
# --ulimit 设置文件描述符和进程数限制
docker run -d -p 9090:8080 --ulimit nofile=65536:65536 --ulimit nproc=65536:65536 --name=tomcat1 tomcat8:v1
–ulimit nofile:文件描述符限制,表示最大打开的文件数
–ulimit nproc:进程/线程数限制
踩坑记录
如果我们在运行容器的时候,没有加上 –ulimit 进行资源限制,我们会发现无法访问到tomcat的默认首页
使用 docker logs ,却提示tomcat正常运行。
[root@hd1 tomcat]# docker run -dt -p 9090:8080 --name mytomcat1 mytomcat:v1
eb8ad3c6066e117501e7f0eb878dd59b2820eccc211830336647b7ef093375d5
[root@hd1 tomcat]# docker logs mytomcat1
Using CATALINA_BASE: /usr/local/tomcat8
Using CATALINA_HOME: /usr/local/tomcat8
Using CATALINA_TMPDIR: /usr/local/tomcat8/temp
Using JRE_HOME: /usr
Using CLASSPATH: /usr/local/tomcat8/bin/bootstrap.jar:/usr/local/tomcat8/bin/tomcat-juli.jar
Tomcat started.
登录进入容器,查看日志,发现报错显示“无法分配文件描述符表”(文件描述符用尽),原因正是系统或容器限制了最大打开文件数(nofile),而 Tomcat 在启动时需要的文件描述符数量超过了限制
[root@eb8ad3c6066e /]# cat /usr/local/tomcat8/logs/catalina.out
library initialization failed - unable to allocate file descriptor table - out of memory[root@eb8ad3c6066e /]#

这就是为什么我们在运行tomcat容器的时候要使用–ulimit 进行资源限制
1.3 练习:构建 Apache 镜像
#创建构建环境
mkdir /dockerfile
cd /dockerfile
#拉取centos7镜像
ocker pull centos:7
#编写Dockerfile
vim Dockerfile
FROM centos:7
RUN curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && yum clean all && yum makecache -y && RUN yum -y install httpd && yum clean all
#yum clean all用来清空缓存, yum makecache -y 用来下载最新的软件包列表和元数据到本地缓存。这两个属于优化项,不影响功能
EXPOSE 80
#FOREGROUND:前台运行
ENTRYPOINT ["/usr/sbin/httpd","-D","FOREGROUND"]
踩坑记录:centos:7没有预装wget,所以使用curl -o
开始构建:
[root@hd1 httpd]# docker build -t myapache:v1 .
#可以看得到Dockerfile中的命令按次序的执行了
[+] Building 163.4s (7/7) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 326B 0.0s
=> [internal] load metadata for docker.io/library/centos:7 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> CACHED [1/3] FROM docker.io/library/centos:7 0.0s
=> [2/3] RUN curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos- 134.1s
=> [3/3] RUN yum -y install httpd 28.1s
=> exporting to image 1.2s
=> => exporting layers 1.2s
=> => writing image sha256:9a01ddd08880290a0294baf393bd2e84190752e2fcee8d986819281a38cc8518 0.0s
=> => naming to docker.io/library/myapache:v1 0.0s
启动容器:
[root@hd1 httpd]# docker run --name http1 -dt -p 8000:80 myapache:v1
接下来,我们登录进容器,修改它的网页首页:
[root@hd1 httpd]# docker run --name http1 -dt -p 8000:80 myapache:v1
[root@964d07d0aec0 /]# vi /var/www/htmlindex.html
<h1>Hello! root</h1>

导出为离线包:
docker save -o httpd2026.tar.gz httpd:v1
导入离线包为镜像:
docker load -i httpd2026.tar.gz
二、Docker 文件系统驱动
容器文件系统是 overlayFS(联合文件系统,说是unionfs也可以)
容器的默认存储驱动是 overlay2。overlay2 是 Docker 在 Linux 上的默认存储驱动(是overlay升级版),支持写时复制(CoW),高效且稳定
容器文件系统基于
联合文件系统(UnionFS)技术,Docker 当前默认使用 overlay2 存储驱动(基于 Linux 内核的OverlayFS)
Docker 镜像由一系列层组成,每层代表 Dockerfile 中的一条指令。除最后一层外的每一层都是只读的。
┌─────────────────────────────────────────┐
│ 容器层(可读写) │ ← 容器运行时添加
├─────────────────────────────────────────┤
│ 镜像层3(只读) │ ← CMD / ENTRYPOINT
├─────────────────────────────────────────┤
│ 镜像层2(只读) │ ← RUN / COPY / ADD
├─────────────────────────────────────────┤
│ 镜像层1(只读) │ ← 基础镜像
└─────────────────────────────────────────┘
核心概念:
- 镜像层是只读的,所有容器共享
- 容器层是可读写的,每个容器独立
- 写时复制(Copy-on-Write)机制保证效率
共享的镜像数据(只读层),只有在容器首次尝试写入(修改)文件时,才会按需将该文件复制到该容器的可写层(容器层)中,此后该容器内的所有读写操作都基于这个副本进行,而其他容器继续共享原始镜像数据。
简单点说,就是镜像大家共享,生成容器的时候会根据这个镜像生成副本,这个方便就是容器的可写层,此后这个可写层就相当于是这个容器(镜像+可写层是一个完整的容器,这里是打一个比方)
每一条“顶级 Dockerfile 指令”(如 FROM、RUN、COPY)都会产生一个新层。但 RUN 指令内部用 && \ 连接多个Shell 命令,并不会增加层数。
所以在构建镜像时,多条 RUN 指令应该尽可能合并成一条(使用&& \ )
#查看宿主机,容器和镜像的信息,比如宿主机运行了多少容器,有多少镜像,文件系统类型等
docker info
......
Server:
Containers: 2
Running: 2
Paused: 0
Stopped: 0
Images: 8
Server Version: 26.1.3
Storage Driver: overlay2 #联合文件系统
Backing Filesystem: extfs
......
Insecure Registries:
127.0.0.0/8
Registry Mirrors:
https://docker.m.daocloud.io/
https://docker.1ms.run/
https://docker.xuanyuan.me/
Live Restore Enabled: false
xfs文件系统:擅长处理大文件,比如大模型参数
ext4文件系统:文件系统更稳定
三、Docker 容器网络
3.1 容器网络介绍
在启动Docker服务的时候,会生成一个 docker0 的虚拟网桥(相当于逻辑上虚拟的交换机和网卡的双重角色)。这个网桥会被分配 172.17.0.1/16的网段
可以看到,我们的宿主机上存在一个docker0的网桥和veth开头的端口:
[root@hd1 ~]# ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:f3ff:fe37:84ca prefixlen 64 scopeid 0x20<link>
ether 02:42:f3:37:84:ca txqueuelen 0 (Ethernet)
RX packets 21223 bytes 989261 (966.0 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 24498 bytes 275566303 (262.8 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.11 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::20c:29ff:fe8e:b6e3 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:8e:b6:e3 txqueuelen 1000 (Ethernet)
RX packets 1437467 bytes 1531542402 (1.4 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 811611 bytes 1050670571 (1001.9 MiB)
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 11600 bytes 697024 (680.6 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 11600 bytes 697024 (680.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
veth7c8f0a8: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::4cd0:acff:fed7:353a prefixlen 64 scopeid 0x20<link>
ether 4e:d0:ac:d7:35:3a txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 17 bytes 1366 (1.3 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethd47c032: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::445c:6ff:fe75:d062 prefixlen 64 scopeid 0x20<link>
ether 46:5c:06:75:d0:62 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 14 bytes 1076 (1.0 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
这是容器中的网络,除了回环地址,还有一个eth0的接口。
/ # ipaddr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN 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
100: eth0@if101: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
那么eth0和vethxxxx又是什么呢?

网桥docker0位于宿主机,docker0可以通过宿主机物理网卡连接外网。图中的veth pair是一条虚拟网线,用于连接网桥和容器,这条虚拟网线在网桥(或者说在宿主机上)的端口叫veth xxx,在容器的端口叫eth0。
每个容器通过 veth pair (虚拟网线)连接到宿主机上的 docker0 网桥,容器间通过网桥通信,访问外网时通过 NAT 转发。这张图准确反映了这一流程
图中三个容器都在同一网段。docker0的地址也是容器的网关地址,所以在功能上,docker0相当于一个虚拟的三层交换机
Docker 网络模式(网络驱动)
| 网络模式 | 配置 | 说明 |
|---|---|---|
| bridge(桥接)模式 | --net=bridge | 默认值,在 Docker 网桥 docker0 上为容器创建新的网络栈 |
| none 模式 | --net=none | 不配置网络,用户可以稍后进入容器自行配置 |
| container 模式 | --net=container:name/id | 容器和另一个容器共享 Network Namespace。Kubernetes 中的 Pod 就是多个容器共享一个 Network Namespace |
| host 模式 | --net=host | 容器和宿主机共享 Network Namespace |
| 用户自定义 | --net=mynet | 用户使用 network 命令自定义网络,容器可通过容器名相互访问 |
Bridge 模式是 Docker 默认的网络模式。容器通过 veth pair 连接到宿主机的 docker0 虚拟网桥。容器访问外网时,依赖宿主机的 IP 转发(ip_forward) 和 NAT 伪装(MASQUERADE) 将源 IP 转换为宿主机 IP;外网访问容器则需依赖 -p 参数配置的 端口映射(DNAT)。该模式简单易用,但容器间无法通过容器名直接通信,生产环境更推荐使用用户自定义的桥接网络。”
默认情况下,桥接模式的容器都是和 docker0 网桥连接的
但是,两个独立的自定义 bridge 网络中的容器是不互通的,因为这两个自定义的bridge 网络相当于两个不同的网桥。如果想要这两个容器进行通信,建议把他们放到同一个自定义网络之中
3.2 容器网络实战
none 模式
Docker 网络 none 模式是指创建的容器没有网络地址,只有 lo 网卡。
在运行容器时通过 –net=none指定none模式
[root@hd1 ~]# docker run -td --name none --net=none alpine
[root@hd1 ~]# docker exec -it none /bin/bash
[root@05dbf3f2daaf /]# ip addr
# 只有 lo 本地回环地址
none模式的容器可以完全隔绝外部网络风险,也可以在容器启动后手动配置网络。适合那些需要精细控制网络的极少数场景
container 模式
新容器与已存在的容器共享 Network Namespace(IP 地址相同)。
这个模式主要用来保护后端的容器
# 先运行一个普通容器
[root@hd1 ~]# docker run --name container1 -td alpine
# 再运行一个 container2,与 container1 共享网络空间
# 它们的 IP 地址是一样的
[root@hd1 ~]# docker run --name container2 --net=container:container1 -it alpine
假设a是web容器,b是数据库容器,b共享了a的网络,但外人只能访问到a,不能访问到b,只有a能通过127.0.0.1:3306 来访问到b容器,由此实现对数据库的保护
(注意,本模式下,不能通过socket文件访问数据库)
host 模式
共享宿主机的网络,与 bridge 模式对比,host 有更好的网络性能(因为不用做nat转换,节省了cpu性能),适合生产环境的 Web 服务器。
# 运行一个 host 模式容器
[root@hd1 ~]# docker run --name host -t -d --net=host alpine
# 查看容器网络空间,与宿主机一致
[root@hd1 ~]# docker exec -it host /bin/sh
/ # ifconfig
# 可以看到 docker0、ens33 等宿主机网卡
docker0 Link encap:Ethernet HWaddr 02:42:F3:37:84:CA
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
inet6 addr: fe80::42:f3ff:fe37:84ca/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:21223 errors:0 dropped:0 overruns:0 frame:0
TX packets:24498 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:989261 (966.0 KiB) TX bytes:275566303 (262.7 MiB)
ens33 Link encap:Ethernet HWaddr 00:0C:29:8E:B6:E3
inet addr:192.168.1.11 Bcast:192.168.1.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fe8e:b6e3/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:1438040 errors:0 dropped:0 overruns:0 frame:0
TX packets:812061 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1531589551 (1.4 GiB) TX bytes:1050714957 (1002.0 MiB)
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:11736 errors:0 dropped:0 overruns:0 frame:0
TX packets:11736 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:705184 (688.6 KiB) TX bytes:705184 (688.6 KiB)
veth7c8f0a8 Link encap:Ethernet HWaddr 4E:D0:AC:D7:35:3A
inet6 addr: fe80::4cd0:acff:fed7:353a/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:17 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:1366 (1.3 KiB)
vethd47c032 Link encap:Ethernet HWaddr 46:5C:06:75:D0:62
inet6 addr: fe80::445c:6ff:fe75:d062/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:14 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:1076 (1.0 KiB)
3.3 自定义桥接网络(了解)
自定义网络中,容器不仅能通过 IP 地址通信,还能将容器名解析到 IP 地址,这种功能称为自动发现。Docker 默认的网络驱动是 bridge。
# 创建一个自定义桥接网络 my-net
docker network create my-net
# 删除网络 my-net
docker network rm my-net
# 查看所有网络
docker network ls
# 查看网络的详细信息
docker network inspect my-net
四、镜像瘦身
镜像瘦身方法
| 方法 | 说明 |
|---|---|
| 1. 使用 Alpine 此类小体型基础镜像 | Alpine 镜像极小(约 5MB) |
| 2. 合并 RUN 指令(通过&&合并语句) | 减少镜像层数 |
| 3. 删除构建期间的临时文件 | 用 && rm -rf 清理缓存 |
| 4. 多阶段构建 | 第一阶段编译,第二阶段只复制产物 |
| 5. 使用 DockerSlim | 第三方工具自动瘦身镜像 |
知识补充:jar包和war包
jar 包和 war 包都是 Java 生态中的压缩文件包,里面装着编译好的 Java 程序(.class 文件)、配置文件、依赖库等
二者区别:
- JAR 包(Java Archive)
- 自带嵌入式 Web 服务器(如 Tomcat、Jetty)。只要安装了 JRE(Java 运行环境),就能直接启动
- WAR 包(Web Archive)
- 必须放置在外部的 Web 服务器(如 Tomcat、Jetty、WebLogic)的 webapps 目录下才能运行。它自己不带“房子”
4.1 多阶段构建jar包
多阶段构建是指:第一阶段用maven工具构建/编译jar包;第二阶段将jar包通过dockerfile方式到精简基础镜像中。
案例:Java 应用多阶段构建
# ========== 第一阶段:构建阶段 ==========
# 使用 Maven 镜像来构建应用程序
FROM maven:3.8.6-openjdk-17 AS builder
# 设置工作目录
WORKDIR /app
# 首先只复制 pom.xml 文件(利用 Docker 缓存层),pom.xml 是 Maven 项目的核心配置文件
COPY pom.xml .
# 下载依赖(如果 pom.xml 没有变化,这层缓存会被复用)
RUN mvn dependency:go-offline -B
# 复制源代码
COPY src ./src
# 构建应用程序(打包并跳过测试)
RUN mvn package -DskipTests
# ========== 第二阶段:运行阶段 ==========
# 使用更小的基础镜像来运行应用程序
FROM openjdk:17-jre-slim
# 设置工作目录
WORKDIR /app
# 从构建阶段复制构建好的 JAR 文件
COPY --from=builder /app/target/*.jar app.jar
# 创建一个非 root 用户来运行应用程序(增强安全性)
RUN groupadd -r javaapp && useradd -r -g javaapp javaapp \
&& chown -R javaapp:javaapp /app
USER javaapp
# 暴露应用程序端口(根据应用程序调整)
EXPOSE 8080
# 设置 JVM 参数(根据需要进行调整)
ENV JAVA_OPTS="-Xms256m -Xmx512m"
# 运行应用程序
ENTRYPOINT java $JAVA_OPTS -jar app.jar
4.2 DockerSlim 瘦身
DockerSlim 是一个开源工具,可以对已经存在的镜像进行瘦身。
它可以自动分析容器运行时行为,剔除不需要的文件,大幅减小镜像体积。这个工具并非对所有容器都会生效
语法:mint build --target 要瘦身的镜像名 --tag 瘦身后的镜像名
下载与安装:
# 下载(从 GitHub Releases 获取最新版本)
wget https://github.com/slimtoolkit/slim/releases/download/1.40.11/dist_linux.tar.gz
# 解压
[root@hd1 ~]# tar xf dist_linux.tar.gz
# 将 mint 和 mint-sensor 二进制文件移动到系统 PATH 下,方便后续使用
[root@hd1 ~]# cd dist_linux
[root@hd1 dist_linux]# mv mint /usr/local/bin/
[root@hd1 dist_linux]# mv mint-sensor /usr/local/bin/
使用 DockerSlim 瘦身:
# 将 nginx:latest 镜像瘦身为 nginx-slim:latest
[root@hd1 dist_linux]# mint build --target nginx:latest --tag nginx-slim:latest
# 查看瘦身效果
[root@hd1 ~]# docker images | grep nginx
nginx-slim latest bb0c8065d246 About a minute ago 11.7MB
nginx latest d1a364dc548d 4 years ago 133MB
效果:从 133MB 瘦身到 11.7MB,体积减少约 91%!
五、容器编排工具
Docker 公司为容器编排开发了两款工具:
| 工具 | 说明 |
|---|---|
| Docker Compose(docker软件自带) | 单台物理主机上编排容器 |
| Docker Swarm | 跨主机容器编排工具(已逐渐被 K8s 取代,但仍在部分环境中使用) |
5.1 Docker Compose 基础
- 软件的架构分为两种:单体架构和微服务架构
- 单体架构:对外体现为一个程序。
- 微服务架构:大软件分成几个或多个单独的程序,每个功能一个独立容器,协同工作(适合复杂、高并发业务)(各个程序之间通过端口进行通信)
对于Java开发来说,单体架构主流开发框架是spring boot,微服务开发框架是spring cloud
一个复杂应用 = Nginx + Web + MySQL + Redis,不是启动一个容器就能完成的,需要多个不同的容器协作。
基本步骤:
- 创建一个目录
- 创建一个
docker-compose.yaml文件,定义服务 - 使用
docker-compose命令启动服务
一个容器对应一个应用 —— 微服务架构。
安装 Docker Compose
Docker Compose安装非常简单,下载一个compuse二进制文件,放到 PATH 目录,加执行权限即可。
因为 Docker Compose 是一个静态编译的二进制文件,它本身就是一个完整的可执行程序,下载后直接就能运行
compose文件可以从github上下载:https://github.com/docker/compose/releases/download/v5.1.2/docker-compose-linux-x86_64
# 上传 docker-compose 二进制文件
[root@hd1 ~]# mv docker-compose-linux-x86_64 /usr/bin/docker-compose
[root@hd1 ~]# chmod +x /usr/bin/docker-compose
5.2 Compose 文件的核心结构
一个标准的 docker-compose.yml 文件,主要包含以下四个顶级配置:
version: "3.8" # 1. 版本声明
services: # 2. 服务定义 (核心)
web:
# ... 服务配置
db:
# ... 服务配置
networks: # 3. 网络配置 (可选)
frontend:
volumes: # 4. 卷配置 (可选)
db-data:
1. 版本声明 (version,建议省略)
- 作用:指定 Compose 文件格式的版本,不同版本支持的特性不同。
- 说明:目前最常用的是
'3'或更高版本(如'3.8')。version必须放在文件的第一行。
在现版本中建议省略版本声明,因为会警告,告诉你这个写了也会被忽略,不如删掉。这也是官方强烈建议的
2. 服务定义 (services,必须)
这是 Compose 文件的核心,用于定义需要运行的每一个容器。
- 基本配置
image: 指定容器使用的镜像,是必须的container_name: 为容器指定一个自定义名称。如果不指定,Compose 会自动生成。ports: 进行端口映射。volumes: 挂载数据卷或宿主机目录。environment: 设置环境变量。depends_on: 定义服务之间的依赖关系和启动顺序。restart: 定义容器的重启策略。command: 覆盖容器启动时的默认命令。links: 在两个容器之间建立“通信链路”,让当前容器能够通过容器名(或自定义别名)访问另一个容器(也可以定义容器别名)。如果进行了网络定义,则不需要links
- 高级配置
build: 指定从 Dockerfile 构建镜像。networks: 指定容器连接到哪个网络(需要和顶层字段networks进行区分,顶层networks主要用来定义/声明网络)。
Nginx + PHP + MySQL 的示例:
services:
# =============================================
# 员工1:Nginx(前台接待)
# =============================================
nginx:
image: nginx:alpine # 用哪个系统镜像(装了什么软件)
container_name: web-nginx # 给容器起个固定名字(不写则自动生成)
ports: # 门牌号映射(宿主机:容器内)
- "8080:80" # 宿主机访问 8080,就等于进了容器的 80 端口
volumes: # 文件共享(宿主机文件夹 ↔ 容器文件夹)
- ./html:/usr/share/nginx/html # 宿主机当前目录的 html 文件夹,挂载成容器的网站根目录
depends_on: # 依赖关系(先启动谁)
- php # 必须先启动 php,再启动我(nginx)
- mysql
networks: # 加入哪个局域网
- webnet # 加入名叫 webnet 的网络,大家用容器名互相访问
# =============================================
# 员工2:PHP(后厨大师傅)
# =============================================
php:
image: php:8.2-fpm-alpine # 用 PHP-FPM 专用镜像
container_name: web-php
volumes: # 共享同一个 html 文件夹(和 nginx 看同一份菜谱)
- ./html:/var/www/html # 挂载路径和 nginx 不一样,但指向宿主机同一个目录
networks:
- webnet
# =============================================
# 员工3:MySQL(仓库管理员)
# =============================================
mysql:
image: mysql:8.0
container_name: web-mysql
environment: # 环境变量(相当于入职时给的“初始配置”)
MYSQL_ROOT_PASSWORD: 123456 # 设置 root 密码
MYSQL_DATABASE: wordpress # 自动创建一个名叫 wordpress 的数据库
volumes:
- mysql_data:/var/lib/mysql # 重要!这里用的是 Docker 卷(不是 ./开头),数据由 Docker 管理,删除容器数据还在
networks:
- webnet
# =============================================
# 员工们的“公司内部网络”(大家通过名字互相找)
# =============================================
networks:
webnet: # 定义这个网络(默认就是 bridge 模式)
driver: bridge
# =============================================
# Docker 管理的仓库(存放 MySQL 的数据,防止丢失)
# =============================================
volumes:
mysql_data: # 只声明一个名字,让 Docker 帮我们管理存储位置
3. 网络配置 (networks,可选)
- 作用:定义容器之间通信的网络。如果不自定义,Compose 会为项目创建一个默认网络,服务可以通过服务名相互访问。
Web 服务 + Redis 缓存的微型架构示例:
services:
web:
image: python:3.9-alpine
# 模拟一个简单的 Web 服务(这里用 sleep 保持运行,便于演示)
command: sh -c "while true; do echo 'Web running...'; sleep 10; done"
ports:
- "5000:5000"
networks:
- app-net
# 不写 links,直接通过 redis 容器名访问
redis:
image: redis:alpine
networks:
- app-net
# 暴露端口给其他容器,但不映射到宿主机
# 不需要 ports,只在内部网络通信
# 定义自定义网络
networks:
app-net:
driver: bridge # 默认就是 bridge,可以省略
这里的网络模式和我们前文的网络模式一致
4. 卷配置 (volumes,可选)
- 作用:定义数据卷,用于持久化存储数据。Bind Mount 还是 Docker Volume,具体看写法:
- 路径以 . 或 / 开头,指定宿主机具体目录,为Bind Mount
- 没有 / 或 .,只是一个名字,为Docker Volume具名卷。只写了容器路径,就是匿名卷
示例:
services:
# Web 应用(Nginx)
web:
image: nginx:latest
ports:
- "8080:80"
volumes:
# 【Bind Mount】挂载宿主机当前目录下的 ./html 到容器网页目录
# 修改本地 html 文件,容器内实时生效(适合开发调试)
- ./html:/usr/share/nginx/html
# 【命名卷】挂载 nginx-logs 卷到日志目录
# 容器删除后日志依然保留,方便排查问题
- nginx-logs:/var/log/nginx
# 数据库(MySQL)
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: myapp
volumes:
# 【命名卷】mysql-data 存放数据库文件
# 即使容器删除重建,数据也不会丢失(持久化存储)
- mysql-data:/var/lib/mysql
# 【Bind Mount】挂载宿主机的 ./init-sql 目录到容器初始化目录
# 把 SQL 文件放进去,容器首次启动时会自动执行(方便初始化)
- ./init-sql:/docker-entrypoint-initdb.d
# ==============================================
# 顶层声明(Define Volumes)
# 这里声明的卷,会被服务里的挂载引用
# ==============================================
volumes:
# 声明一个名为 mysql-data 的卷(由 Docker 管理,存储位置在 /var/lib/docker/volumes/)
mysql-data:
driver: local
# 声明一个名为 nginx-logs 的卷
nginx-logs:
driver: local
volumes在顶层配置时只具备声明的作用,声明我要配置哪些卷,方便在后续进行引用
5.3 常用配置详解
1. 端口映射 (ports)
ports 用于将容器内部的端口映射到宿主机上。两种常用写法:
| 写法 | 示例 | 说明 |
|---|---|---|
| 短语法 | - "8080:80" | 宿主机端口 8080 映射到容器端口 80。 |
| 长语法 | - target: 80 published: 8080 protocol: tcp mode: host | 更明确地指定目标端口、发布端口、协议和模式。 |
2. 数据卷挂载 (volumes)
volumes 用于持久化数据或在宿主机和容器间共享文件。
| 写法 | 示例 | 说明 |
|---|---|---|
| 短语法 | - ./html:/usr/share/nginx/html | 将宿主机目录 ./html 挂载到容器目录。 |
| 短语法 | - db-data:/var/lib/mysql | 使用名为 db-data 的 Docker 数据卷进行持久化。 |
| 长语法 | - type: volume source: db-data target: /var/lib/mysql | 明确指定挂载类型、源和目标的配置。 |
3. 环境变量 (environment 与 env_file)
用于向容器传递配置信息。
environment: 直接在文件中定义环境变量。env_file: 从外部文件(如.env)加载环境变量。环境变量通常以KEY=VAL的格式写在.env文件中,然后在 Compose 文件中引用,例如DB_PASSWORD: ${DB_PASS}。
4. 依赖控制 (depends_on)
- 作用:控制服务的启动顺序。例如,先启动
db再启动web。 - 注意:
depends_on只控制启动顺序,不保证依赖的服务已经完全就绪(比如数据库已准备好接受连接)。如果需要更健壮的就绪检查,可以配合condition使用。
(WordPress + MySQL)常用配置完整示例(注意,仅是方便理解的示例):
services:
# ============================================================
# 服务1:WordPress(网站应用)
# ============================================================
wordpress:
image: wordpress:latest
container_name: my-wordpress
ports:
# 【知识点1:端口映射 - 短语法】
- "8080:80"
# 宿主机 8080 → 容器 80
environment:
# 【知识点3:环境变量 - environment 方式(直接定义)】
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress123
WORDPRESS_DB_NAME: wordpress
env_file:
# 【知识点3:环境变量 - env_file 方式(从外部文件加载)】
- ./wp.env
# 可以把重复的变量放外部文件,避免写太长
volumes:
# 【知识点2:数据卷挂载 - bind mount(宿主机目录)】
- ./wp-content:/var/www/html/wp-content
# 宿主机 ./wp-content → 容器插件/主题目录(方便开发调试)
depends_on:
# 【知识点4:依赖控制】
- db
# 先启动 db,再启动 wordpress
networks:
- wp-net
# ============================================================
# 服务2:MySQL(数据库)
# ============================================================
db:
image: mysql:8.0
container_name: my-db
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress123
volumes:
# 【知识点2:数据卷挂载 - Docker 命名卷(短语法)】
- db-data:/var/lib/mysql
# db-data 是 Docker 管理的卷,删除容器数据还在
ports:
# 【知识点1:端口映射 - 长语法(更明确)】
- target: 3306
published: 33060
protocol: tcp
mode: host
# 宿主机 33060 → 容器 3306
networks:
- wp-net
# ============================================================
# 网络定义
# ============================================================
networks:
wp-net:
driver: bridge
# ============================================================
# 卷定义(声明 Docker 管理的命名卷)
# ============================================================
volumes:
db-data:
# 只声明名子,不指定路径,Docker 自动在 /var/lib/docker/volumes/ 下创建
配套的外部环境变量文件(wp.env)内容:
# wp.env 内容(和 environment 配合使用)
WORDPRESS_TABLE_PREFIX=wp_
WORDPRESS_DEBUG=true
这样可以把敏感信息或环境差异放在外部,不用改 Compose 文件
5.4 案例1:Haproxy 负载均衡 Web 应用
架构图: Haproxy 作为负载均衡器,将请求分发到 web1 和 web2 两个 Nginx 节点。

[root@hd1 ~]# mkdir h1
[root@hd1 ~]# cd h1
创建 Haproxy 配置文件
[root@hd1 h1]# mkdir haproxy
[root@hd1 h1]# vim haproxy/haproxy.cfg
#添加下面的配置
global #全局 配置段
log 127.0.0.1 local2
maxconn 4000 #最大并发连接数
user haproxy
group haproxy
defaults #默认配置段,自动继承给后面的 frontend 和 backend,除非它们在各自段里重写
mode http
log global
option httplog
option dontlognull
timeout http-request 10s
timeout queue 1m
frontend balancer
bind 0.0.0.0:8081 #前端暴露的端口号,Haproxy 在所有网卡(0.0.0.0)的 8081 端口上监听请求
default_backend app #跳向后端的集群名
backend app #后端集群
balance roundrobin
server web1 web1:80 check
server web2 web2:80 check
编写 docker-compose.yaml
[root@hd1 h1]# vim docker-compose.yaml
version: '3' #版本号,可以去掉
services: #定义三个微服务,web1,web2,haproxy_load
web1:
image: nginx:latest #指定镜像
restart: always #设置重启策略
volumes:
- ./web1:/usr/share/nginx/html/ #映射
web2:
image: nginx:latest
restart: always
volumes:
- ./web2:/usr/share/nginx/html/
haproxy_load:
image: haproxy
user: "0" #用户为root
privileged: true
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro #只读权限
links: #表示连接,连接两个容器
- web1
- web2
ports: #端口映射
- "8081:8081"
expose:
- "8081"
注意,这里的volumes 写法,本质上就是 Bind Mount(绑定挂载)
links已过时:在新版 Docker Compose 中,links不是必需的,服务可以通过服务名直接通信(自定义网络中默认支持)。
需注意,在配置容器的 Haproxy 负载均衡时,后端服务器要使用容器名进行指定,不能使用 IP 地址进行指定。
准备 Web 页面
[root@hd1 h1]# mkdir web1 web2
[root@hd1 h1]# echo "111" > web1/index.html
[root@hd1 h1]# echo "222" > web2/index.html
启动与测试
注意!docker-compose命令必须在docker-compose.yaml当前目录才能使用!
# 导入 Haproxy 镜像(如果有离线包)
[root@hd1 h1]# docker load -i haproxy.tar.gz
#或者直接从官网拉取haproxy
[root@hd1 h1]# docker pull haproxy
# 启动 compose(前台运行)。注意!前台运行会出现类似“卡住”的现象,建议使用后台启动
[root@hd1 h1]# docker-compose up
# 后台启动
[root@hd1 h1]# docker-compose up -d
# 停止容器
[root@hd1 h1]# docker-compose stop
# 删除容器
[root@hd1 h1]# docker-compose rm
# 查看 compose 启动的服务
[root@hd1 h1]# docker-compose ps --services
测试负载均衡:
[root@hd1 h1]# curl 192.168.1.11:8081
111
[root@hd1 h1]# curl 192.168.1.11:8081
222
5.4 案例2:Python Flask + Redis 访问计数器
创建一个 Python Web 应用,使用 Flask 框架,将用户访问次数写入 Redis,通过 Web 首页显示访问次数。
创建工程目录
mkdir pythondir
cd pythondir
创建 Flask 应用
cat > app.py << 'EOF'
from flask import Flask
from redis import Redis
app = Flask(__name__)
redis = Redis(host='redis',port=6379)
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello world! I have been seen %s time.' % redis.get('hits')
if __name__ == "__main__":
app.run(host="0.0.0.0",debug=True)
EOF
编写 Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.6-alpine
ADD . /code
WORKDIR /code
RUN pip install redis flask
CMD python app.py
EOF
编写 docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: '3'
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
EOF
启动与测试
# 启动两个容器(-d 表示后台启动)
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs
访问测试:
浏览器访问 http://192.168.1.11:5000,每次刷新页面,访问次数会累加。
Hello world! I have been seen 1 time.
Hello world! I have been seen 2 time.
5.5 常用 Compose 命令
所有命令都需要在 docker-compose.yml 文件所在的目录下执行。
docker compose up -d:在后台创建并启动所有服务。-d代表“detach”模式。docker compose down:停止并删除所有容器、网络等资源。docker compose logs -f:实时查看所有服务的聚合日志。docker compose ps:列出当前项目所有容器的状态。docker compose exec <服务名> <命令>:在指定的运行中容器内执行命令。
六、Docker Compose 常用命令速查
| 命令 | 说明 |
|---|---|
docker-compose up | 启动所有服务(前台运行) |
docker-compose up -d | 启动所有服务(后台运行) |
docker-compose stop | 停止所有服务 |
docker-compose start | 启动已停止的服务 |
docker-compose restart | 重启所有服务 |
docker-compose down | 停止并删除所有容器 |
docker-compose rm | 删除已停止的容器 |
docker-compose ps | 查看服务状态 |
docker-compose logs | 查看服务日志 |
docker-compose logs -f | 实时查看日志 |
docker-compose exec <服务名> <命令> | 在服务容器中执行命令 |
docker-compose build | 重新构建镜像 |
docker-compose pull | 拉取最新镜像 |
七、核心知识点总结
7.1 构建镜像要点
FROM:指定基础镜像,尽量选择 Alpine 以减小体积RUN:构建时执行的命令,多条 RUN 应合并以减小层数COPYvsADD:优先使用COPY,ADD有自动解压功能CMDvsENTRYPOINT:CMD可被覆盖,ENTRYPOINT固定主进程EXPOSE:声明端口(仅文档作用,实际映射需用-p)
Dockerfile我们按照:FROM → RUN → COPY → ENTRYPOINT 的套路来写
7.2 网络模式选择
--net=<模式>
| 场景 | 推荐模式 |
|---|---|
| 默认单机容器 | bridge(默认) |
| 需要高性能网络 | host |
| 调试/测试隔离 | none |
| 多个容器共享网络 | container |
| 生产环境多服务 | 用户自定义 bridge |
7.3 镜像瘦身原则
- 使用 Alpine 基础镜像
- 合并 RUN 指令,减少层数
- 构建后删除临时文件
- 多阶段构建(编译产物分离)
- 使用 DockerSlim 工具自动瘦身
7.4 Docker Compose 使用场景
- 多容器本地开发环境
- 微服务架构的单机部署
- CI/CD 测试环境
- 生产环境建议使用 K8s 替代 Swarm
八、踩坑记录
以下是我在实操过程中遇到的问题,以及问题解决的记录
1. Alpine 镜像的 sed -i 替换源失败(apk 源格式差异)
现象:
在基于 alpine:latest 的 Dockerfile 中执行:
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
构建时报错 sed: can't read /etc/apk/repositories: No such file or directory,或替换后 apk update 依然超时。
原因:
- 新版 Alpine(如 v3.18+)的
/etc/apk/repositories文件内容可能不是简单的http://dl-cdn.alpinelinux.org/...格式,还可能包含@edge等标签。 sed的正则替换要求原字符串必须完全匹配,如果格式有细微变化(如https替换http),替换就会失效。- 有些版本 Alpine 的仓库源写在
/etc/apk/repositories,但格式是http://dl-cdn.alpinelinux.org/alpine/v3.18/main,直接替换域名即可。
解决方案:
方案一:使用 sed 进行更通用的域名替换
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
先进入容器用 cat /etc/apk/repositories 查看实际内容,确认域名是否是 dl-cdn.alpinelinux.org。
方案二:直接覆盖整个 repositories 文件(最稳妥)
RUN echo "http://mirrors.aliyun.com/alpine/v3.18/main" > /etc/apk/repositories && \
echo "http://mirrors.aliyun.com/alpine/v3.18/community" >> /etc/apk/repositories
方案三:使用 --repository 参数临时指定源(不修改源配置文件)
RUN apk add --no-cache --repository http://mirrors.aliyun.com/alpine/v3.18/main nginx
如果 Alpine 版本固定,直接查清该版本的官方源格式,再针对性替换。或者直接用
sed只替换域名部分,不要整行替换。
2. RUN 指令拆开写导致镜像体积膨胀
现象:
最初构建镜像的时候,将RUN指令分散拆开,结果构建出的镜像体积比预期大很多,一个简单的 Nginx 镜像居然有 200+ MB。
原因:
Dockerfile 中的每条 RUN、COPY、ADD 指令都会生成一个独立的镜像层。如果多个 RUN 没有合并,每一层都会保留中间状态的文件系统快照,即使后面删除了文件,这些文件在之前的层中依然存在,导致镜像体积无法真正减小。
错误写法:
FROM alpine:latest
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update
RUN apk add --no-cache nginx
RUN rm -rf /var/cache/apk/*
正确写法(合并 RUN):
FROM alpine:latest
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
apk update && \
apk add --no-cache nginx && \
rm -rf /var/cache/apk/*
记住:
RUN指令内部的&&只是 Shell 语法,不会增加层数;但每条独立的RUN都会增加层数。
查看层数:
docker history 镜像名:标签
可以看到每一层的大小,从而定位是哪一层产生了大文件。
3. OverlayFS 的“上层遮蔽下层”导致调试困惑
现象:
进入容器后,发现某个目录下明明有文件,但在宿主机对应的 OverlayFS 下层目录里却找不到,或者修改了某个文件后,宿主机上对应镜像的层却没有变化,让人怀疑文件到底存不存在。
原因:
OverlayFS 的机制是上层(容器层)会遮蔽下层(镜像层)。当你修改了容器内 /etc/nginx/nginx.conf 后:
- 这个文件被**写时复制(CoW)**到了容器层(
/var/lib/docker/overlay2/<容器ID>/diff/)。 - 底层的镜像层文件依然保持原样,没有变化。
- 从容器内看,文件是修改后的;从宿主机看镜像层,文件还是旧的。
验证方法:
# 1. 查看容器的 OverlayFS 挂载信息
docker inspect 容器名 | grep -A 10 "GraphDriver"
# 2. 进入容器的联合挂载点查看(merged 目录是“合并视图”)
cd /var/lib/docker/overlay2/<容器ID>/merged/
# 这里看到的是合并后的文件系统(镜像层 + 容器层的合并结果)
# 3. 查看容器的“差异层”(只有容器修改过的文件)
cd /var/lib/docker/overlay2/<容器ID>/diff/
容器内看到的文件 = 镜像层(只读) + 容器层(可写,优先遮蔽下层同名文件)。修改文件后,只是容器层多了一份副本,镜像层纹丝不动。
4. --net=host 模式下端口冲突导致服务无法启动
现象:
执行 docker run --net=host nginx,发现 Nginx 报错 Address already in use,容器启动失败。
原因:
host 模式让容器直接使用宿主机的网络栈,容器内的端口直接绑定在宿主机上。如果宿主机上已经有一个 Nginx 占用了 80 端口,容器里的 Nginx 再去绑定 80 端口,就会因为端口冲突而失败。
解决方案:
- 方案一:确认宿主机端口未被占用,或停止占用该端口的进程。
- 方案二:如果宿主机的 80 端口被其他服务占用,可以在容器内改用其他端口,但
host模式下无法像 bridge 模式那样做端口映射(如-p 8080:80),因为host模式下容器直接使用宿主机端口,没有“映射”一说。 - 方案三(推荐):不需要极致网络性能时,直接使用默认的
bridge模式 +-p端口映射,避免端口冲突。
# 检查宿主机端口占用
ss -tlnp | grep 80
5. 自定义桥接网络中容器无法访问外网
现象:
创建了自定义桥接网络 docker network create my-net,并将容器加入该网络后,容器内 ping 8.8.8.8 无响应,并且 curl 外网超时。
原因:
自定义桥接网络默认也是 bridge 驱动,理论上应该和默认 docker0 一样具备 NAT 转发能力。但可能的原因有:
- 宿主机未开启 IP 转发(
net.ipv4.ip_forward=1)。 - 自定义网络创建时指定了
--internal参数,导致该网络无法访问外网。 - 防火墙/iptables 规则被清空或覆盖(如
iptables -F会删掉 Docker 添加的 NAT 规则)。
排查步骤:
# 1. 检查 IP 转发是否开启
sysctl net.ipv4.ip_forward
# 应该输出 1,如果是 0 则需开启
# 2. 检查自定义网络是否有 internal 标记
docker network inspect my-net | grep internal
# 如果为 true,则该网络完全隔离,无法访问外网
# 3. 检查 NAT 规则是否存在
iptables -t nat -L -n | grep MASQUERADE
# 应该有针对容器网段的 MASQUERADE 规则
解决方案:
# 开启 IP 转发(临时)
sysctl -w net.ipv4.ip_forward=1
# 永久开启(写入 /etc/sysctl.conf)
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p
# 如果是 internal 网络,重建网络(不加 --internal)
docker network rm my-net
docker network create my-net
6. DockerSlim 瘦身后容器启动失败或功能缺失
现象:
使用 mint build --target nginx:latest --tag nginx-slim:latest 瘦身完成后,基于瘦身镜像启动容器,发现 Nginx 报错缺少某些文件或动态链接库(如 error while loading shared libraries),或者某些功能(如 SSL、Gzip)失效。
原因:
DockerSlim 的工作原理是动态分析:它会在容器运行时监控所有被访问的文件和系统调用,然后只保留“被使用”的文件,删除“未被使用”的文件。
- 如果某个文件只有在特定条件下才会被访问(如错误页面、某些模块懒加载),DockerSlim 在分析期间没有触发该条件,就会认为该文件是“无用”的并删除。
- 动态链接库(
.so文件)如果只在特定模块中被依赖,也可能被误删。
解决方案:
- 方案一:在瘦身时添加
--include-path或--include-file参数,强制保留某些目录或文件。mint build --target nginx:latest --tag nginx-slim:latest \ --include-path /usr/lib/nginx/modules \ --include-path /etc/nginx/conf.d - 方案二:如果瘦身导致功能严重缺失,说明该镜像不适合用 DockerSlim 自动瘦身,考虑手动优化 Dockerfile(如用 Alpine 基础镜像、多阶段构建)。
- 方案三(调试):用
--exec参数进入瘦身后的容器,手动排查缺失文件。# 瘦身时添加 --exec 参数,在瘦身完成后可以进入容器检查 mint build --target nginx:latest --tag nginx-slim:latest --exec
DockerSlim 适合“通用型”镜像(如纯静态 Nginx、Node.js 应用),对于依赖复杂、需要特定运行时环境的镜像(如 Python 科学计算、Java 应用),效果可能不理想,优先选择多阶段构建。
7. 镜像层数过多导致 docker push/pull 极慢
现象:
构建了一个镜像,docker push 到私有仓库或 docker pull 到其他机器时,速度异常缓慢,且推送/拉取过程中出现大量 layer 传输。
原因:
镜像的每一层在推送/拉取时都是独立的压缩包。如果 Dockerfile 写得不好(比如每安装一个软件包就写一个 RUN),会导致镜像层数非常多。Docker 在传输时需要逐个计算校验和、压缩、传输、解压,层数越多,耗时就成倍增加。
查看层数:
docker history 镜像名:标签
如果输出超过 10 行(不含 <missing> 的基础镜像层),说明层数偏多,需要进行瘦身优化。
解决方案:
- 合并
RUN指令(见踩坑记录 2)。 - 使用
--squash参数构建镜像(实验性功能,合并所有层为一个层,但会丢失层缓存优势):# 需要开启 Docker 实验性功能(/etc/docker/daemon.json 添加 "experimental": true) docker build --squash -t 镜像名:标签 . - 多阶段构建(生产推荐):最终阶段只
COPY必要的产物,层数很少。
8. 容器 hostname 无法通过 /etc/hosts 修改
现象:
进入容器后执行 echo "127.0.0.1 myapp" >> /etc/hosts,发现修改不生效或容器重启后丢失。
原因:
容器的 /etc/hosts 文件由 Docker 运行时动态管理,它不是存储在容器层中的普通文件,而是通过 OverlayFS 的 mount 方式挂载进来的。每次容器启动时,Docker 会根据 --hostname 参数重新生成。
正确设置容器 hostname:
# 启动容器时指定 hostname
docker run --hostname myapp -td alpine
# 或使用 --add-host 添加自定义 hosts 解析
docker run --add-host myservice:192.168.1.100 -td alpine
验证:
docker exec -it 容器名 cat /etc/hostname
docker exec -it 容器名 cat /etc/hosts
/etc/hosts 中会看到 172.17.0.x myapp 这样的映射。
注意:不要试图在容器内手动修改
/etc/hosts来持久化主机名,重启容器后会丢失。生产环境如果需要容器间通过自定义域名通信,建议使用自定义桥接网络(容器名自动解析)或服务发现(Consul、Eureka)来解决。
9. docker rmi -f 强制删除镜像后,依赖该镜像的容器依然存在
现象:
执行 docker rmi -f nginx:latest 成功删除了镜像,但 docker ps -a 看到之前基于该镜像创建的容器依然还在(状态为 Exited 或 Running)。
原因:
docker rmi -f 强制删除镜像,只是在镜像列表中移除了该镜像的“标签引用”,但实际上容器依然持有对该镜像层数据的引用。如果容器正在运行,该镜像的文件系统层依然存在于磁盘上,不会真正被清理。等容器被删除后,这些层才会被垃圾回收。
后果:
- 磁盘空间没有真正释放(容器还在引用这些层)。
- 如果该镜像是某个容器的唯一依赖,你无法用
docker run再从这个已删除的镜像创建新容器,但已有容器依然能正常运行。
正确做法:
# 先删除依赖该镜像的所有容器(包括停止的)
docker rm -f $(docker ps -aq --filter ancestor=nginx:latest)
# 再删除镜像
docker rmi nginx:latest
# 或者一次性清理所有未被使用的镜像(更安全)
docker image prune -a
教训:
-f不等于“真正彻底删除”,它只是“强制移除引用”。想要真正释放空间,必须确保没有容器在使用该镜像。
更多推荐
所有评论(0)