服务端6月20日 11:24
Docker 数据卷怎么用?Volume、Bind Mount 和 tmpfs 有什么区别?Docker 容器本身是“可丢弃”的:容器删了,容器可写层里的文件通常也就没了。数据卷解决的就是这个问题。它把数据放到容器生命周期之外,让数据库文件、上传文件、日志、配置等数据可以在容器重建后继续存在。
说白了,镜像负责运行环境,容器负责进程,数据卷负责把真正重要的数据留下来。
## Docker 数据卷有什么用?
Docker 数据卷主要有几个作用:
- **持久化数据**:容器删除、重建、升级后,数据仍然保留。
- **容器之间共享数据**:多个容器可以挂载同一个卷,例如一个写文件、另一个读取文件。
- **方便迁移和备份**:卷可以独立备份,不必把数据混在容器可写层里。
- **降低容器可写层压力**:数据库、日志这类频繁写入的数据,不适合长期放在容器层。
- **让运行环境和数据解耦**:升级镜像时只替换应用,不动业务数据。
例如 MySQL、PostgreSQL、Redis、MinIO、Elasticsearch 这类服务,如果不把数据目录挂出来,容器一删,数据很可能也跟着消失。
## Volume、Bind Mount 和 tmpfs 有什么区别?
| 类型 | 数据位置 | 是否持久化 | 适合场景 | 注意点 |
|---|---|---|---|---|
| volume | Docker 管理的存储目录 | 是 | 数据库、生产数据、需要备份的数据 | 路径由 Docker 管理,不建议手动改内部文件 |
| bind mount | 宿主机指定目录或文件 | 是 | 本地开发、挂载配置文件、源码热更新 | 容器可直接改宿主机文件,权限和安全要小心 |
| tmpfs | 宿主机内存 | 否 | 临时缓存、敏感临时文件 | 容器停止后数据消失,不能用于持久数据 |
### volume:最适合持久化业务数据
```bash
docker volume create mysql-data
docker run -d --name mysql -e MYSQL_ROOT_PASSWORD=example -v mysql-data:/var/lib/mysql mysql:8
```
这里的 `mysql-data` 是一个命名卷,`/var/lib/mysql` 是容器里的数据库数据目录。容器删掉后,`mysql-data` 仍然存在。
### bind mount:适合开发和明确指定宿主机路径
```bash
docker run -d --name nginx -v /Users/me/site:/usr/share/nginx/html:ro nginx
```
这个例子把宿主机的 `/Users/me/site` 挂到 Nginx 的静态目录,并用 `:ro` 设置为只读。bind mount 很适合本地开发,但也更危险:如果挂载了 `/etc`、`/var/run/docker.sock` 这类敏感路径,容器就可能影响宿主机。
### tmpfs:只放临时数据
```bash
docker run --tmpfs /run:rw,noexec,nosuid,size=64m nginx
```
它适合放运行期临时文件、缓存、敏感中间文件。容器停止后,数据就没了,所以不要把数据库、上传文件放在 tmpfs 里。
## -v 和 --mount 应该用哪个?
`-v` 更短,适合日常快速使用:
```bash
docker run -v mysql-data:/var/lib/mysql mysql:8
```
`--mount` 更清晰,适合脚本、生产环境和团队协作:
```bash
docker run --mount type=volume,source=mysql-data,target=/var/lib/mysql mysql:8
```
新项目或生产脚本更推荐 `--mount`,减少误挂载的风险。
## 命名卷和匿名卷有什么区别?
命名卷有明确名字,方便复用、查看、备份和删除。
```bash
docker volume create app-data
docker run -v app-data:/data alpine
docker volume ls
docker volume inspect app-data
```
匿名卷没有手动指定名称,Docker 会生成一串 ID:
```bash
docker run -v /data alpine
```
这种写法能持久化,但不好识别,也容易越积越多。生产环境建议优先使用命名卷,不要依赖匿名卷保存关键数据。
## Docker Compose 里怎么声明数据卷?
```yaml
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes:
- pg-data:/var/lib/postgresql/data
app:
image: my-app:latest
depends_on:
- db
volumes:
pg-data:
```
这里的 `pg-data` 是命名卷。执行 `docker compose up -d` 后,Docker 会自动创建它。
本地开发也可以用 bind mount 挂源码:
```yaml
services:
app:
image: node:22
working_dir: /app
volumes:
- ./:/app
command: npm run dev
```
这类写法适合开发环境,不建议直接照搬到生产环境。生产环境更应该把代码打进镜像,数据用命名卷或外部存储管理。
## 数据卷的生命周期怎么管理?
删除容器:
```bash
docker rm mysql
```
命名卷通常不会自动删除。你需要手动删:
```bash
docker volume rm mysql-data
```
如果创建容器时使用了匿名卷,删除容器时可以加 `-v` 一起删除匿名卷:
```bash
docker rm -v container-name
```
清理所有未被容器使用的卷:
```bash
docker volume prune
```
执行 `prune` 前要谨慎确认,尤其是生产服务器。只要某个卷当前没有被容器引用,就可能被清理掉。
## 数据卷怎么备份和恢复?
备份命名卷:
```bash
docker run --rm -v mysql-data:/data:ro -v $(pwd):/backup alpine tar czf /backup/mysql-data.tar.gz -C /data .
```
恢复到新卷:
```bash
docker volume create mysql-data-new
docker run --rm -v mysql-data-new:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/mysql-data.tar.gz"
```
如果备份的是数据库,最好使用数据库自己的备份工具,例如 `mysqldump`、`pg_dump`、物理备份工具或快照方案。直接打包数据库目录时,必须确保数据库已停止或处于一致性备份状态,否则可能得到一个不能恢复的备份。
## 权限问题:UID 和 GID 为什么经常出错?
挂载卷后,容器里的进程会用自己的用户身份读写文件。这个用户的 UID/GID 可能和宿主机用户不一致,于是就会出现“容器写的文件宿主机删不了”或“容器没有权限写目录”。
```bash
docker exec -it app id
sudo chown -R 1000:1000 ./data
docker run --user 1000:1000 -v $(pwd)/data:/data my-app
```
数据库镜像通常有自己的用户。挂载目录时不要只看宿主机当前用户,要看镜像文档里要求的数据目录权限。
## 数据库使用数据卷有什么注意事项?
- 不要多个数据库容器同时写同一个数据目录,除非数据库本身明确支持这种集群模式。
- 不要随便跨版本复用数据目录,例如从 MySQL 5.7 直接换到 8.0,先看升级文档。
- 不要用普通文件同步工具实时同步数据库目录,容易同步到不一致状态。
- 不要把高频写入数据库放在性能很差的网络盘上。
- 备份不能只靠 docker volume,还要有可验证的恢复流程。
对数据库来说,卷负责保存数据,备份策略负责保证数据能恢复。两件事不能混为一谈。
## 安全上要注意什么?
- 能只读就只读,例如 `:ro` 或 `readonly`。
- 不要把宿主机根目录、系统目录、SSH 密钥目录挂进容器。
- 不要随便挂载 `/var/run/docker.sock`,这几乎等于把宿主机 Docker 控制权交给容器。
- 生产环境避免用宽泛的 bind mount,优先使用命名卷或专门的存储方案。
- 敏感临时数据可以考虑 tmpfs,减少落盘风险。
## 什么时候该选哪一种?
| 需求 | 推荐方式 |
|---|---|
| 数据库持久化 | 命名 volume |
| 上传文件持久化 | 命名 volume 或对象存储 |
| 本地开发挂源码 | bind mount |
| 挂单个配置文件 | bind mount,尽量只读 |
| 临时缓存、运行期文件 | tmpfs |
| 生产环境长期数据 | 命名 volume、外部存储或云盘方案 |
Docker 数据卷的核心不是命令有多复杂,而是把数据和容器分开管理。容器可以随时删、随时重建;真正要保护的是卷里的数据。标签
Docker
Docker 是一个开源的容器化平台,允许开发者打包应用及其全部依赖到一个可移植的容器中,然后这个容器可以在任何机器上运行,确保应用在不同环境之间运行的一致性。Docker 使用了 Linux 容器(LXC)的技术,但进行了扩展,使其更为易用、功能更全面。

服务端6月20日 11:24
Docker 镜像分层是什么?如何优化构建体积和缓存?## Docker 镜像分层到底是什么?
Docker 镜像不是一个单独的大文件,而是一组只读层叠加出来的文件系统。Dockerfile 里的多数指令,例如 `FROM`、`RUN`、`COPY`、`ADD`,都会生成新的镜像层。容器启动时,Docker 会在这些只读层上再加一层可写层,应用运行时产生的文件修改就落在这一层里。
这些层通常基于内容寻址保存。简单说,Docker 会根据层内容计算摘要,内容一样的层可以被复用、共享和缓存。两个镜像如果都基于同一个 `node:20-alpine`,底层基础镜像层通常只需要在机器上保存一份。
在 Linux 上,Docker 常见的存储驱动是 `overlay2`。它会把多个只读层通过 OverlayFS 叠在一起,对容器表现成一个完整目录。当上层修改下层已有文件时,并不是直接改原文件,而是把文件复制到可写层再修改,这就是常说的 copy-on-write。
## 分层带来了哪些好处?
分层最直接的价值是构建缓存。Docker 构建镜像时会从上到下执行 Dockerfile,如果某一层的指令和上下文没有变化,就可以直接复用缓存,不必重新执行。
它也能减少存储和传输成本。相同的基础层可以被多个镜像共享,拉取镜像时已经存在的层不会重复下载。镜像仓库推送和拉取时,也可以按层并行处理,所以一个设计合理的镜像通常构建更快、传输更省。
但分层也有副作用:每一层都会记录文件系统变化。你在一层里创建了大文件,下一层再删除它,最终镜像里仍可能保留前一层的大文件内容。很多“明明删了缓存,镜像还是很大”的问题,都和这个机制有关。
## 构建缓存为什么会失效?
Dockerfile 的缓存是顺序命中的。某一层缓存失效后,它后面的层通常也要重新构建。
最常见的坑是过早执行大范围 `COPY`:
```dockerfile
COPY . .
RUN npm install
```
只要项目里任意文件变化,`COPY . .` 这一层就会变,后面的依赖安装也会重新执行。前端或 Node.js 项目更推荐先复制依赖清单,再安装依赖,最后复制源码:
```dockerfile
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
```
这样只改业务代码时,依赖安装层仍能命中缓存。Python、Go、Java 项目也有类似思路:先复制依赖描述文件,再下载依赖,最后复制源码。
## 如何减少镜像体积?
### 选择合适的基础镜像
基础镜像决定了镜像体积的起点。能用运行时镜像就不要用完整构建环境,能用官方 slim 版本就不要默认拉 full 版本。
```dockerfile
FROM node:20-slim
```
`alpine` 很小,但不是所有场景都适合。它使用 musl libc,部分依赖 glibc 的原生库可能需要额外处理,排查成本反而更高。对 Node.js、Python 原生扩展较多的项目,`slim` 有时比 `alpine` 更稳。
### 使用多阶段构建
多阶段构建适合把“编译环境”和“运行环境”分开。第一阶段安装编译工具并产出构建结果,第二阶段只复制运行所需文件。
```dockerfile
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
```
这样最终镜像里不会带上 `node_modules`、源码、构建工具和临时缓存,只保留真正运行需要的产物。
### 清理必须发生在同一层
如果安装包和清理缓存拆成两条 `RUN`,上一层里的缓存仍然会留在镜像历史中:
```dockerfile
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
```
更好的写法是放在同一个 `RUN` 里:
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
```
包管理器缓存、临时文件、编译中间产物,都应该遵守这个原则:创建和删除放在同一层。
### 合并 RUN 指令,但别过度合并
合并 `RUN` 可以减少层数,也能避免临时文件残留。但不要为了少一层把完全无关的命令揉成一大坨,否则可读性和缓存命中都会变差。
比较好的做法是按变化频率拆分:系统依赖一层,应用依赖一层,业务代码一层。这样既便于缓存,也便于排查问题。
## .dockerignore 为什么很重要?
`.dockerignore` 用来排除不应该进入构建上下文的文件。没有它时,Docker 可能会把 `.git`、日志、测试产物、本地依赖、临时文件一起发送给 Docker daemon。
常见配置如下:
```dockerignore
.git
node_modules
dist
coverage
*.log
.env
.DS_Store
```
它不只是减少镜像体积,还会影响缓存。构建上下文越干净,`COPY . .` 越不容易因为无关文件变化而失效。
## 能不能用 squash 压成一层?
镜像 squashing 可以把多层压成更少的层,看起来能减少历史包袱。但它不是默认首选。
原因有两个:第一,压扁后层复用能力会变差,多个镜像之间不容易共享中间层;第二,它可能掩盖 Dockerfile 本身的问题,比如缓存清理位置不对、构建产物没有隔离。多数情况下,优化 Dockerfile 比依赖 squash 更可靠。
## 怎么检查镜像哪里变大了?
先用 `docker history` 看每一层的大小和对应指令:
```bash
docker history your-image:tag
```
它能快速定位是哪条 Dockerfile 指令引入了大体积文件。需要更细的分析时,可以用 `dive` 查看每一层新增、修改、删除了哪些文件:
```bash
dive your-image:tag
```
如果发现某一层新增了大量缓存,下一层又删除,说明清理时机不对;如果发现源码、测试文件、`.git` 目录被打进镜像,通常是 `.dockerignore` 没写好。
## 一个更合理的 Dockerfile 思路
以 Node.js 应用为例,比较稳的结构通常是这样:
```dockerfile
FROM node:20-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
```
这个写法的重点不是模板本身,而是顺序:依赖文件先复制,依赖安装尽量缓存;源码后复制,避免频繁改代码导致依赖层失效;最终阶段只保留运行时需要的内容。
## 小结
Docker 镜像优化的关键不是单纯减少层数,而是理解每一层留下了什么、哪些层能复用、哪些改动会让缓存失效。实际项目里优先做好几件事:基础镜像选小但别盲目选,依赖文件先复制,清理和安装放同一层,多阶段构建隔离编译产物,用 `.dockerignore` 控制构建上下文,再用 `docker history` 或 `dive` 找出真正的大层。服务端6月20日 11:19
Docker 容器隔离机制是什么?边界在哪里?Docker 的容器隔离主要靠 Linux 内核能力完成,不是靠 Docker 自己“虚拟出一台机器”。一句话说:Namespace 负责把容器“看见的世界”隔开,Cgroup 负责限制它能“用多少资源”,再叠加联合文件系统、Capabilities、seccomp、AppArmor 或 SELinux 等机制,形成一套相对完整的运行边界。
## Namespace 隔离了容器能看到什么
Namespace 可以理解成 Linux 给进程准备的“视角隔离”。同一台宿主机上的进程,放进不同 Namespace 后,看到的进程、网络、挂载点、主机名可能完全不同。
### PID Namespace:隔离进程编号
PID Namespace 让容器拥有自己的进程树。容器内的第一个进程通常看到自己是 PID 1,但在宿主机上它仍然只是一个普通进程,有宿主机上的真实 PID。
这也是为什么在宿主机执行 `ps` 能看到容器进程,而容器内默认看不到宿主机其他进程。
### NET Namespace:隔离网络栈
NET Namespace 隔离网卡、IP、路由表、端口和防火墙规则。每个容器可以有自己的虚拟网卡、独立 IP 和端口空间。
Docker 默认会通过 veth pair 和 bridge 网络把容器接到宿主机网络上。容器觉得自己有一张独立网卡,实际流量仍然经过宿主机内核转发。
### MNT Namespace:隔离文件系统挂载点
MNT Namespace 让容器看到自己的根目录和挂载结构。容器里看到的 `/` 不是宿主机真正的 `/`,而是 Docker 准备好的镜像层和可写层组合后的文件系统视图。
但如果把宿主机目录挂进去,例如 `-v /:/host`,隔离边界就会被主动削弱。所以挂载权限是容器安全里很关键的一环。
### UTS、IPC 和 USER Namespace
UTS Namespace 隔离 hostname 和 domain name。IPC Namespace 隔离 System V IPC、POSIX message queue 等进程间通信资源。USER Namespace 用来把容器内的用户 ID 映射到宿主机上的另一个用户 ID,例如容器里看起来是 root,映射到宿主机后可能只是普通用户。
这对安全很重要,因为容器内 root 如果直接等于宿主机 root,一旦逃逸风险会更高。Rootless Docker 和 userns-remap 都是在利用这个思路降低权限面。
## Cgroup 限制了容器能用多少资源
Namespace 解决“看见什么”,Cgroup 解决“能用多少”。Docker 可以通过 Cgroup 限制 CPU、内存、磁盘 I/O、进程数量等资源,避免一个容器拖垮整台宿主机。
| 资源 | 作用 |
|---|---|
| CPU | 限制 CPU 使用比例、权重或可用核心 |
| Memory | 限制内存上限,超出后可能触发 OOM |
| Block I/O | 限制磁盘读写权重或吞吐 |
| PIDs | 限制进程数量,防止 fork bomb |
| Devices | 控制容器能访问哪些设备 |
Cgroup v1 按资源类型拆成多个独立控制器,配置灵活但层级容易混乱。Cgroup v2 统一了层级模型,资源控制更一致,现代 Linux 发行版和新版本容器运行时越来越多地使用 v2。
需要注意,Cgroup 是资源控制,不是完整的安全沙箱。它能限制资源滥用,但不能替代权限隔离和系统调用过滤。
## 联合文件系统提供镜像层隔离
Docker 镜像通常由多层只读层组成,容器启动时再叠加一个可写层。常见实现包括 overlay2。
这样做有两个好处:多个容器可以共享相同镜像层,节省磁盘空间;容器写入文件时只写自己的可写层,不会直接修改镜像原始层。
不过,联合文件系统不是安全边界的全部。真正决定容器能不能访问宿主机敏感路径的,还是挂载配置、权限、Capabilities 和安全策略。
## Capabilities、seccomp 和 LSM 继续收紧权限
Linux root 权限被拆成很多 Capabilities。Docker 默认会去掉一部分高危能力,比如直接加载内核模块通常不应该出现在普通容器里。
seccomp 用来过滤系统调用。比如某些危险 syscall 可以被默认策略拦截,降低容器利用内核攻击面的机会。
AppArmor 和 SELinux 属于 Linux Security Module,可以进一步限制进程能访问哪些文件、执行哪些操作。它们更像一层强制访问控制,防止“进程有权限但不该做”的行为。
生产环境常见做法是:不要使用 `--privileged`,按需添加 Capability,启用默认 seccomp 配置,并配合 AppArmor 或 SELinux 策略。
## Docker 隔离的边界在哪里
Docker 容器和虚拟机最大的区别是:容器共享宿主机内核,虚拟机通常有独立内核。
这意味着容器隔离的边界主要在 Linux 内核能力上。如果内核存在可利用漏洞,或者容器被授予了过高权限,例如 `--privileged`、挂载 Docker socket、挂载宿主机根目录,容器就可能影响宿主机。
Rootless 模式和 USER Namespace 可以降低风险,但也不是万能的。它们能减少容器进程在宿主机上的实际权限,却不能消除所有内核攻击面,也可能受到功能兼容性限制。
## 实际使用时怎么判断是否安全
判断一个容器是否隔离得足够好,不能只看它是不是 Docker 跑起来的,还要看这些配置:
- 是否避免 `--privileged`;
- 是否限制了不必要的 Capabilities;
- 是否启用了 seccomp、AppArmor 或 SELinux;
- 是否给内存、CPU、PIDs 设置了合理 Cgroup 限制;
- 是否避免挂载宿主机敏感目录和 Docker socket;
- 是否使用 USER Namespace 或 Rootless 模式降低 root 风险;
- 镜像和宿主机内核是否及时更新。
Docker 的隔离机制不是单点能力,而是一组内核机制的组合。Namespace 让容器看起来像独立系统,Cgroup 控制资源使用,联合文件系统隔离文件变更,安全模块收紧权限。它足够适合大多数应用隔离场景,但不能把它误认为和虚拟机一样强的硬隔离。服务端6月20日 11:19
Dockerfile CMD 和 ENTRYPOINT 有什么区别?Dockerfile 里的 `CMD` 和 `ENTRYPOINT` 都和容器启动命令有关,但职责不一样:`ENTRYPOINT` 更像“固定要运行的程序”,`CMD` 更像“默认参数”或“默认命令”。
如果只记一句话:**想让镜像像一个可执行程序一样运行,用 `ENTRYPOINT`;想给容器提供一个可以轻松替换的默认启动命令,用 `CMD`。两者一起用时,通常是 `ENTRYPOINT` 写可执行文件,`CMD` 写默认参数。**
## CMD 是什么?
`CMD` 用来指定容器启动时的默认命令。它最大的特点是:**容易被 `docker run` 后面的参数覆盖**。
常见写法有三种:
```dockerfile
CMD ["node", "server.js"]
CMD node server.js
CMD ["--help"]
```
前两种是完整命令,第三种通常配合 `ENTRYPOINT` 使用,表示给 `ENTRYPOINT` 传默认参数。
比如:
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]
```
直接运行:
```bash
docker run my-node-app
```
实际执行的是:
```bash
node server.js
```
但如果这样运行:
```bash
docker run my-node-app node worker.js
```
`CMD ["node", "server.js"]` 会被 `node worker.js` 覆盖。所以 `CMD` 适合放“可以被用户改掉的默认行为”。
## ENTRYPOINT 是什么?
`ENTRYPOINT` 用来指定容器启动时固定执行的程序。普通的 `docker run` 参数不会直接覆盖它,而是会追加到 `ENTRYPOINT` 后面,作为参数传入。
```dockerfile
FROM alpine
ENTRYPOINT ["ping"]
CMD ["localhost"]
```
运行:
```bash
docker run ping-image
```
等价于:
```bash
ping localhost
```
运行:
```bash
docker run ping-image 8.8.8.8
```
等价于:
```bash
ping 8.8.8.8
```
这里 `ping` 是固定程序,`localhost` 只是默认参数。用户传了 `8.8.8.8` 后,覆盖的是 `CMD`,不是 `ENTRYPOINT`。
## exec 形式和 shell 形式有什么区别?
`CMD` 和 `ENTRYPOINT` 都有 exec 形式和 shell 形式。
exec 形式写成 JSON 数组:
```dockerfile
ENTRYPOINT ["node", "server.js"]
CMD ["--port", "3000"]
```
shell 形式写成普通字符串:
```dockerfile
ENTRYPOINT node server.js
CMD node server.js
```
推荐优先用 exec 形式,原因有两个。
第一,exec 形式不会额外套一层 shell,参数传递更准确。第二,exec 形式对信号处理更友好。容器里的主进程通常是 PID 1,Docker 停止容器时会先发送 `SIGTERM`。如果用 shell 形式,真正的业务进程可能变成 shell 的子进程,信号不一定能正确传过去,容易出现容器停止慢、进程残留、优雅退出失效等问题。
```dockerfile
ENTRYPOINT node server.js
```
实际可能是:
```bash
/bin/sh -c "node server.js"
```
更推荐:
```dockerfile
ENTRYPOINT ["node", "server.js"]
```
如果确实需要 shell 能力,比如环境变量展开、管道、`&&`,可以使用 shell 形式,但要知道它带来的信号处理问题。生产镜像里更稳妥的方式通常是把复杂逻辑放到脚本里,并在脚本最后使用 `exec`:
```sh
#!/bin/sh
set -e
exec node server.js
```
再配合:
```dockerfile
ENTRYPOINT ["./entrypoint.sh"]
```
## docker run 怎么覆盖它们?
覆盖 `CMD` 很简单,直接在镜像名后面追加命令或参数。
```bash
docker run my-image bash
```
如果镜像只有:
```dockerfile
CMD ["node", "server.js"]
```
那么 `bash` 会覆盖原来的 `CMD`。
如果镜像是:
```dockerfile
ENTRYPOINT ["node", "server.js"]
CMD ["--port", "3000"]
```
执行:
```bash
docker run my-image --port 8080
```
最终命令是:
```bash
node server.js --port 8080
```
如果连 `ENTRYPOINT` 也想覆盖,需要显式使用 `--entrypoint`:
```bash
docker run --entrypoint sh my-image
```
注意,`--entrypoint` 只替换入口程序,镜像原来的 `CMD` 仍可能作为参数拼到后面。排查问题时可以看镜像的 `Config.Entrypoint` 和 `Config.Cmd`:
```bash
docker inspect my-image
```
## 什么时候只用 CMD?
如果镜像只是给一个默认启动命令,用户经常需要替换整个命令,用 `CMD` 就够了。
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ["npm", "run", "dev"]
```
默认启动开发服务,但用户也可以方便地执行:
```bash
docker run my-app npm test
```
这种场景下,`CMD` 的可覆盖特性反而是优点。
## 什么时候用 ENTRYPOINT + CMD?
如果希望镜像表现得像一个固定工具,推荐 `ENTRYPOINT + CMD`。
```dockerfile
FROM alpine
RUN apk add --no-cache curl
ENTRYPOINT ["curl"]
CMD ["--help"]
```
默认运行会输出 curl 帮助;传入参数时:
```bash
docker run curl-image -I https://example.com
```
实际执行:
```bash
curl -I https://example.com
```
服务型镜像也可以这样写:
```dockerfile
ENTRYPOINT ["node", "server.js"]
CMD ["--port", "3000"]
```
这样镜像默认监听 3000,用户需要改端口时只覆盖参数即可。
## docker compose 里的 command 和 entrypoint 对应什么?
在 Docker Compose 里,`command` 对应 Dockerfile 里的 `CMD`,`entrypoint` 对应 Dockerfile 里的 `ENTRYPOINT`。
```yaml
services:
app:
image: my-app
command: ["--port", "8080"]
```
如果镜像里有:
```dockerfile
ENTRYPOINT ["node", "server.js"]
CMD ["--port", "3000"]
```
Compose 的 `command` 会把默认参数改成:
```bash
node server.js --port 8080
```
如果同时覆盖入口和参数:
```yaml
services:
app:
image: my-app
entrypoint: ["sh"]
command: ["-c", "env && sleep 3600"]
```
这常用于临时调试。
## 常见写法怎么选?
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 默认启动一个服务,用户可能替换整条命令 | `CMD` | 覆盖方便 |
| 镜像就是一个 CLI 工具 | `ENTRYPOINT + CMD` | 固定工具名,参数可变 |
| 服务程序固定,只想允许改参数 | `ENTRYPOINT + CMD` | 程序稳定,参数灵活 |
| 需要临时进入容器排查 | `--entrypoint sh` | 直接绕过原入口 |
| 需要信号正确传给主进程 | exec 形式 | 避免 shell 吞信号 |
实际项目里可以这样记:**CMD 管默认值,ENTRYPOINT 管主程序;exec 形式优先,shell 形式慎用;Compose 里的 command 改 CMD,entrypoint 改 ENTRYPOINT。**服务端6月20日 00:02
Docker COPY 和 ADD 有什么区别?什么时候该用 ADD?Dockerfile 里复制文件时,默认优先用 `COPY`。它的语义很单纯:把构建上下文里的文件或目录复制到镜像指定路径。`ADD` 也能复制文件,但它多了几个“隐式动作”,尤其是本地 tar 包自动解压和远程 URL 下载。正因为这些行为不够直观,日常构建里更推荐 `COPY`,只有明确需要 `ADD` 的特殊能力时再用它。
## COPY 做什么?
`COPY` 只负责复制本地文件、目录,行为可预测:
```dockerfile
COPY package.json package-lock.json ./
COPY src/ /app/src/
```
注意目标路径的斜杠:
```dockerfile
COPY file.txt /app
COPY file.txt /app/
```
如果 `/app` 不存在,第一种可能把文件复制成名为 `/app` 的文件;第二种明确表示复制到 `/app/` 目录下。写 Dockerfile 时建议目录目标都带上 `/`,减少歧义。
`COPY` 也支持通配符,例如:
```dockerfile
COPY *.json ./
```
但通配符匹配的是构建上下文里的文件,不是容器里的路径。构建上下文会受 `.dockerignore` 影响,所以别把 `node_modules`、日志、临时文件、密钥文件一起送进镜像构建,否则不仅镜像变大,还可能泄露敏感信息。
## ADD 多了哪些能力?
`ADD` 可以做 `COPY` 能做的事,还多了两类常见行为。
第一,本地 tar 包会自动解压:
```dockerfile
ADD app.tar.gz /app/
```
如果 `app.tar.gz` 是本地构建上下文里的 tar 归档,Docker 会把它解压到 `/app/`。这个功能适合你明确想把本地归档展开进镜像的场景。
第二,`ADD` 可以从远程 URL 下载文件:
```dockerfile
ADD https://example.com/tool.tar.gz /tmp/tool.tar.gz
```
但这通常不推荐。远程下载会让构建结果依赖网络、服务端响应和缓存规则,可复现性变差。使用 BuildKit 时,HTTP(S) URL 可以配合 `--checksum` 校验内容:
```dockerfile
ADD --checksum=sha256:<hash> https://example.com/tool.tar.gz /tmp/tool.tar.gz
```
有校验比裸下载安全,但大多数场景仍然更适合用 `RUN curl` 或 `wget`,因为你可以在同一层里校验、解压、删除缓存文件。
## 为什么下载文件更推荐 RUN curl 或 wget?
比如安装一个二进制包,更推荐这样写:
```dockerfile
RUN curl -fsSL https://example.com/tool.tar.gz -o /tmp/tool.tar.gz && echo "<hash> /tmp/tool.tar.gz" | sha256sum -c - && tar -xzf /tmp/tool.tar.gz -C /usr/local/bin && rm /tmp/tool.tar.gz
```
好处很直接:下载、校验、解压、清理都在同一层完成,不会把临时压缩包留在镜像层里。失败时也更容易定位问题。
## 缓存失效有什么区别?
`COPY` 和 `ADD` 都会影响 Docker 构建缓存。只要被复制的源文件内容发生变化,对应层以及后面的 `RUN` 层通常都会失效。
所以常见优化是先复制依赖描述文件,再安装依赖,最后复制业务代码:
```dockerfile
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/
```
这样业务代码变了,不会轻易让依赖安装层重新执行。
`.dockerignore` 也会影响缓存。忽略无关文件能减少构建上下文变化,避免因为日志、缓存目录、编辑器临时文件导致 Docker 反复失效。
## --chown、--chmod 和多阶段构建
`COPY` 和 `ADD` 都可以配合权限参数使用:
```dockerfile
COPY --chown=node:node --chmod=755 app.sh /usr/local/bin/app.sh
```
这比复制后再 `RUN chown`、`RUN chmod` 更干净,少一层,也更容易读懂。
多阶段构建里还常用 `COPY --from` 从前一个阶段复制产物:
```dockerfile
FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist/ /usr/share/nginx/html/
```
这也是 `COPY` 的高频用法:只把最终产物放进运行镜像,构建工具、源码缓存、依赖中间文件都不带进去。
## 安全上怎么选?
规则可以很简单:
- 只是复制文件:用 `COPY`
- 需要本地 tar 自动解压:可以用 `ADD`
- 需要下载远程文件:优先 `RUN curl` 或 `wget`,并做 checksum 校验
- 不要把密钥、`.env`、SSH 私钥放进构建上下文
- 用 `.dockerignore` 控制复制范围
- 目录目标路径尽量写成 `/path/`,避免歧义
`ADD` 不是不能用,而是它会“顺手多做事”。Dockerfile 越显式,镜像构建越稳定,也越容易排查问题。服务端6月20日 00:02
Docker 容器生命周期有哪些状态和管理命令?Docker 容器生命周期管理,核心就是管理一个容器从创建、运行、暂停、停止、重启到删除的全过程。常见状态包括 `created`、`running`、`paused`、`exited`、`restarting` 和 `dead`,对应的命令主要有 `create`、`run`、`start`、`stop`、`kill`、`pause`、`unpause`、`restart`、`rm`。
## Docker 容器有哪些生命周期状态?
| 状态 | 含义 | 常见场景 |
|---|---|---|
| `created` | 容器已创建,但还没有启动 | 执行 `docker create` 后 |
| `running` | 容器正在运行 | 执行 `docker start` 或 `docker run` 后 |
| `paused` | 容器进程被暂停 | 执行 `docker pause` 后 |
| `exited` | 容器主进程已退出 | 程序运行结束、报错退出或被停止 |
| `restarting` | 容器正在按重启策略重启 | 配置了 restart policy 后 |
| `dead` | Docker 无法正常管理该容器 | 删除失败、底层资源异常等少见情况 |
可以用 `docker ps` 查看运行中的容器,用 `docker ps -a` 查看所有容器及其状态。
## create 和 run 有什么区别?
`docker create` 只创建容器,不启动。容器创建后会停留在 `created` 状态,适合先准备配置、网络、挂载参数,再手动启动。
```bash
docker create --name app nginx
```
`docker run` 更常用,它等价于“创建 + 启动”。如果加上 `-d`,容器会在后台运行。
```bash
docker run -d --name app nginx
```
简单说:`create` 是先把容器准备好,`run` 是准备好以后立刻跑起来。
## 启动、停止和强制终止怎么用?
启动已创建或已停止的容器,用 `docker start`:
```bash
docker start app
```
停止容器优先用 `docker stop`。它会先向容器内的 PID 1 进程发送 `SIGTERM`,给程序一个优雅退出的机会;如果超时仍未退出,再发送 `SIGKILL` 强制结束。
```bash
docker stop app
docker stop -t 30 app
```
`docker kill` 则是直接发送强制终止信号,默认是 `SIGKILL`:
```bash
docker kill app
```
生产环境通常先用 `stop`,只有容器卡死、无法正常退出时才用 `kill`。尤其要注意,容器里的 PID 1 如果没有正确处理 `SIGTERM`,应用可能来不及关闭连接或刷盘,导致数据状态不一致。
## pause、unpause 和 restart 分别做什么?
`docker pause` 会暂停容器中的所有进程,容器仍存在,但进程不会继续执行:
```bash
docker pause app
docker unpause app
```
它适合短时间冻结容器,比如临时排查资源占用。但它不是正常停机方式,不能替代 `stop`。
`docker restart` 相当于先停止再启动:
```bash
docker restart app
```
如果应用需要平滑重启,要确认它能正确处理 `SIGTERM`,否则 `restart` 也可能变成一次粗暴中断。
## 如何查看容器运行情况?
生命周期管理不只是执行命令,还要能看懂容器发生了什么。
```bash
docker logs app
docker inspect app
docker stats app
docker events
```
`docker logs` 看容器标准输出和错误输出;`docker inspect` 看容器配置、网络、挂载、退出码等详细信息;`docker stats` 看 CPU、内存、网络、磁盘 IO;`docker events` 可以观察 Docker 守护进程记录的创建、启动、停止、销毁等事件。
如果容器异常退出,重点看 `docker inspect` 里的退出码。`0` 通常表示正常退出,非 `0` 往往表示程序异常、启动命令错误、权限问题或依赖服务不可用。
## restart policy 怎么控制自动重启?
Docker 可以通过重启策略控制容器退出后的行为:
| 策略 | 含义 |
|---|---|
| `no` | 默认策略,容器退出后不自动重启 |
| `on-failure` | 只有非 0 退出码时才重启 |
| `always` | 只要容器退出就自动重启,Docker 服务重启后也会拉起 |
| `unless-stopped` | 类似 `always`,但如果容器被手动停止,Docker 重启后不会再自动拉起 |
示例:
```bash
docker run -d --restart unless-stopped --name app nginx
```
服务型容器更常用 `unless-stopped`。它既能在异常退出后自动恢复,又不会和人工停机操作打架。
## 删除容器前要注意什么?
删除容器用 `docker rm`,但容器必须先停止:
```bash
docker stop app
docker rm app
```
如果确认不需要保留容器,也可以强制删除:
```bash
docker rm -f app
```
不过要分清“容器数据”和“卷数据”。删除容器不会自动删除 Docker volume 中的数据,挂载到 volume 或宿主机目录里的文件通常还在。这是好事,也是坑:好处是数据不会因为容器重建就丢;坑是不用的 volume 会越积越多,需要定期清理。
常见清理命令包括:
```bash
docker container prune
docker volume prune
docker system prune
```
清理前一定确认资源不再使用,尤其是 volume,误删后数据库、上传文件这类持久化数据可能无法恢复。
## 小结
Docker 容器生命周期可以理解为:先创建,再启动运行;运行中可以暂停、恢复、停止或重启;退出后根据退出码和重启策略决定是否自动拉起;不再需要时再删除容器并清理无用资源。日常管理时,`stop` 比 `kill` 更安全,`inspect` 和 `logs` 是排查退出问题的入口,volume 数据是否保留则要单独确认。服务端6月20日 00:02
Docker 环境变量怎么配置?ENV、ARG 和 Compose 怎么选?## Docker 环境变量到底能配置在哪?
Docker 容器环境变量常见配置方式有四类:写在 `Dockerfile` 里、运行容器时传入、通过环境变量文件批量传入、在 Docker Compose 中配置。它们看起来都叫“环境变量”,但生效时机、覆盖规则和安全风险并不一样。
一句话先说结论:**固定默认值可以放 Dockerfile,环境差异参数用运行时传入,生产敏感信息不要直接当环境变量裸奔。**
## Dockerfile 里的 ENV 和 ARG 有什么区别?
`Dockerfile` 里最容易混淆的是 `ENV` 和 `ARG`。
### ENV:构建后仍然存在
`ENV` 用来设置镜像默认环境变量,构建出的镜像和启动后的容器里都能看到。
```dockerfile
FROM node:20-alpine
ENV NODE_ENV=production
ENV APP_PORT=3000
CMD ["node", "server.js"]
```
适合放默认运行参数,比如 `NODE_ENV`、默认端口、默认语言环境。它不适合放密码、Token、数据库连接串,因为这些值可能进入镜像层历史,也可能被 `docker inspect` 看到。
### ARG:只在构建阶段使用
`ARG` 只在镜像构建时生效,默认不会保留到运行时环境里。
```dockerfile
FROM node:20-alpine
ARG BUILD_VERSION
RUN echo "build version: $BUILD_VERSION"
```
构建时传入:
```bash
docker build --build-arg BUILD_VERSION=2026.06.19 -t my-app .
```
如果需要把 `ARG` 写入运行时环境,必须显式转成 `ENV`。这也意味着它会进入最终镜像环境。不要把“ARG 构建时用”误解成“ARG 一定安全”,构建日志、镜像历史和 CI 记录里仍可能留下痕迹。
## docker run 怎么传环境变量?
运行容器时最直接的方式是 `-e` 或 `--env`。
```bash
docker run -e NODE_ENV=production -e APP_PORT=3000 my-app
```
也可以只写变量名,让 Docker 从当前 Shell 环境里取值:
```bash
export API_BASE_URL=https://api.example.com
docker run --env API_BASE_URL my-app
```
如果变量比较多,用 `--env-file` 更清爽。
```bash
docker run --env-file .env.production my-app
```
`.env.production` 示例:
```env
NODE_ENV=production
APP_PORT=3000
API_BASE_URL=https://api.example.com
```
`--env-file` 不是 Shell 脚本,通常按 `KEY=VALUE` 读取,不要指望它执行命令、展开复杂表达式。值里如果有空格、引号、换行,最好先确认解析行为。
## Docker Compose 里 environment、env_file 和 .env 有什么区别?
Compose 里有三个名字很像的东西:`.env`、`env_file`、`environment`。它们不是一回事。
### .env:主要用于 compose 文件变量插值
项目根目录的 `.env` 常用于替换 `compose.yml` 里的占位变量。
```env
IMAGE_TAG=1.2.0
HOST_PORT=8080
```
```yaml
services:
web:
image: my-app:${IMAGE_TAG}
ports:
- "${HOST_PORT}:3000"
```
这里 `.env` 的作用是让 Compose 在解析配置文件时,把 `${IMAGE_TAG}`、`${HOST_PORT}` 替换掉。它不等于自动把所有变量都塞进容器。
### env_file:把文件里的变量传给容器
```yaml
services:
web:
image: my-app:1.2.0
env_file:
- .env.production
```
这会把 `.env.production` 里的变量注入容器运行环境。
### environment:直接在 compose 里声明容器变量
```yaml
services:
web:
image: my-app:1.2.0
environment:
NODE_ENV: production
APP_PORT: "3000"
API_BASE_URL: ${API_BASE_URL}
```
`environment` 的好处是配置直观,适合少量关键变量;变量很多时,`env_file` 更容易维护。
## 环境变量优先级怎么算?
对单个 `docker run` 来说,通常可以按这个理解:
1. `docker run -e KEY=value` 显式传入的值优先级最高;
2. `docker run --env-file` 中的值次之;
3. 镜像里 `Dockerfile ENV` 的默认值最后兜底。
Compose 的优先级更细一些:`environment` 里直接声明的值通常会覆盖 `env_file` 同名变量;`env_file` 会覆盖镜像 `ENV` 默认值;`.env` 主要影响 `${VAR}` 插值,本身不自动等于容器环境变量;命令行临时覆盖通常最高。
如果变量来源很多,不要靠猜。可以用下面命令看 Compose 最终解析结果:
```bash
docker compose config
```
再进容器确认实际值:
```bash
docker compose exec web env | grep NODE_ENV
```
## 敏感信息能不能放环境变量?
能用,但不推荐把它当成安全存储。环境变量很方便,也很容易泄漏:`docker inspect` 可能看到容器环境变量,应用启动日志可能把配置整体打印出来,CI/CD 日志中也可能出现明文。
所以数据库密码、私钥、访问 Token 这类内容,生产环境更建议使用 Docker Swarm Secrets、Kubernetes Secrets、云厂商密钥管理服务,或者运行平台提供的 Secret 注入能力。
## Secrets 不是环境变量,那应用怎么读?
很多运行平台会把 Secret 挂载成文件,而不是直接放进环境变量。Docker Swarm Secrets 常见路径类似:
```text
/run/secrets/db_password
```
不少官方镜像支持 `_FILE` 约定,例如:
```env
MYSQL_PASSWORD_FILE=/run/secrets/mysql_password
POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
```
这个约定不是 Docker 强制标准,而是很多镜像和框架采用的习惯。自己写应用时也可以照这个思路做:普通配置走环境变量,敏感值走文件或密钥服务。
## 一个更接近生产的配置方式
开发环境可以简单一点:
```yaml
services:
web:
build: .
ports:
- "3000:3000"
env_file:
- .env.development
environment:
NODE_ENV: development
```
生产环境建议把默认值、环境差异和密钥分开:
```yaml
services:
web:
image: my-app:1.2.0
environment:
NODE_ENV: production
APP_PORT: "3000"
DB_HOST: db
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
external: true
```
应用启动时读取:
```js
import fs from 'node:fs';
function readSecret(name) {
const file = process.env[`${name}_FILE`];
if (file) return fs.readFileSync(file, 'utf8').trim();
return process.env[name];
}
const dbPassword = readSecret('DB_PASSWORD');
```
## 配置后怎么验证?
查看 Compose 渲染后的配置:
```bash
docker compose config
```
查看容器环境变量:
```bash
docker exec <container> env | sort
```
查看镜像默认环境变量:
```bash
docker image inspect my-app --format '{{json .Config.Env}}'
```
查看容器环境变量时要小心,不要在共享终端、CI 日志或工单截图里暴露敏感值。很多泄漏不是黑客攻破系统,而是排查问题时顺手贴了一段日志。
## 生产环境建议怎么定规则?
比较稳的做法是把变量按用途分层:镜像默认值放无敏感、跨环境都合理的默认配置;部署环境变量放不同环境会变化的普通配置;Secret 放密码、Token、私钥、证书;CI/CD 参数放构建版本、提交哈希、构建时间这类构建期信息。
还要给变量命名留点规矩。比如统一使用 `APP_`、`DB_`、`REDIS_` 前缀;布尔值固定用 `true/false`;端口统一写字符串,避免 YAML 把值解析成奇怪的类型。
最后,给应用启动加一层配置校验。缺了关键变量就直接失败,不要带着默认空值跑起来。Docker 环境变量配置没有唯一答案,关键是边界清楚:默认配置归默认配置,环境差异归部署系统,敏感信息归 Secrets。服务端6月19日 23:48
Docker 端口映射怎么配置?-p、Compose 和排查怎么做?Docker 端口映射的作用,是把容器网络命名空间里的端口发布到宿主机上。最常见的写法是 `docker run -p 8080:80 nginx`:外部访问宿主机的 `8080` 端口,请求会被转发到容器里的 `80` 端口。
这里最容易混淆的是:`EXPOSE` 只是镜像或 Dockerfile 里的声明,告诉别人“这个容器通常会监听哪些端口”;真正让宿主机能访问容器端口的,是 `-p/--publish` 或 Compose 里的 `ports`。
## `-p` 的完整写法是什么?
`-p` 的通用格式是:
```bash
docker run -p [hostIP:]hostPort:containerPort[/protocol] image
```
几个常见例子:
```bash
# 宿主机 8080 转发到容器 80
docker run -p 8080:80 nginx
# 只允许本机访问宿主机 8080
docker run -p 127.0.0.1:8080:80 nginx
# 绑定所有网卡的 8080
docker run -p 0.0.0.0:8080:80 nginx
# 映射多个端口
docker run -p 8080:80 -p 8443:443 nginx
# 映射 UDP 端口
docker run -p 5353:5353/udp some-image
```
默认协议是 TCP。如果服务用的是 UDP,比如 DNS、游戏服务、部分实时通信服务,必须显式写 `/udp`,否则你映射的是 TCP,访问当然不通。
## `0.0.0.0`、`127.0.0.1` 和安全性有什么区别?
`0.0.0.0:8080:80` 表示监听宿主机所有网卡。只要机器的公网 IP、内网 IP 能被访问,别人就可能连到这个端口。
`127.0.0.1:8080:80` 只绑定本机回环地址,通常只有宿主机自己能访问,适合数据库、管理后台、本地调试服务这类不该直接暴露的端口。
所以生产环境里不要随手写:
```bash
docker run -p 0.0.0.0:3306:3306 mysql
```
这等于把数据库端口暴露在宿主机所有网卡上。更稳妥的方式是只绑定本地地址,再通过反向代理、堡垒机、VPN 或内网访问控制处理入口。
## 为什么映射了端口还是访问不了?
端口映射只负责把流量送到容器端口,但容器里的应用也要真的监听在正确地址上。
如果应用在容器内只监听 `127.0.0.1`,外部流量通常到不了它。因为 Docker 转发过来的目标地址是容器的网络地址,不是容器里的 loopback。容器里的 Web 服务应监听:
```bash
0.0.0.0:80
```
而不是:
```bash
127.0.0.1:80
```
很多 Node、Python、Go 开发服务默认只监听 localhost,本地直接跑没问题,放进容器再做端口映射就会踩坑。
## `EXPOSE`、`-P` 和 `-p` 有什么区别?
`EXPOSE` 不会自动发布端口,例如:
```dockerfile
EXPOSE 80
```
它只是元数据。你运行容器时仍然需要:
```bash
docker run -p 8080:80 image
```
`-P` 是大写 P,会把镜像里 `EXPOSE` 声明的端口随机映射到宿主机端口:
```bash
docker run -P nginx
```
这种方式适合临时调试,不适合稳定对外服务,因为宿主机端口不固定。要知道实际映射到了哪个端口,可以查:
```bash
docker port 容器名
```
## Docker Compose 里怎么写?
Compose 里最常见的是 `ports`:
```yaml
services:
web:
image: nginx
ports:
- "8080:80"
- "127.0.0.1:8443:443"
```
`ports` 会把容器端口发布到宿主机。
`expose` 不会发布到宿主机,只是让同一个 Docker 网络里的其他服务知道这个服务使用了哪些端口:
```yaml
services:
api:
image: my-api
expose:
- "3000"
```
在同一个 Compose 网络里,服务之间通常不需要端口映射。比如 `web` 访问 `api:3000` 即可,Docker 内部 DNS 会把服务名解析到对应容器。只有当宿主机或外部用户需要访问容器时,才需要 `ports`。
## 常用排查命令有哪些?
先看 Docker 是否真的发布了端口:
```bash
docker port 容器名
```
再看容器是否在运行:
```bash
docker ps
```
检查宿主机端口是否监听:
```bash
ss -lntp | grep 8080
```
如果是 macOS 没有 `ss`,可以用:
```bash
lsof -i :8080
```
从宿主机访问测试:
```bash
curl -v http://127.0.0.1:8080
```
进容器里看应用是否监听正确地址:
```bash
docker exec -it 容器名 sh
ss -lntp
```
如果 Docker 映射没问题、容器服务也在监听,但外部机器访问不了,继续查三件事:
- 宿主机防火墙是否放行端口,比如 `ufw`、`firewalld`、云服务器安全组;
- 是否绑定成了 `127.0.0.1`,导致只能本机访问;
- 协议是否写错,UDP 服务不能只映射 TCP。
## 端口映射配置时要记住什么?
`-p 8080:80` 的方向是“宿主机端口:容器端口”,不要写反。`EXPOSE` 只是声明,`-p` 或 `ports` 才是真正发布端口。对外服务可以绑定 `0.0.0.0`,但数据库、后台管理、调试端口更适合绑定 `127.0.0.1` 或放在 Docker 内部网络里。
大多数端口映射问题最后都落在四处:Docker 没发布、应用没监听 `0.0.0.0`、宿主机端口被占用、防火墙或安全组没放行。按这个顺序查,通常很快能定位。服务端6月19日 23:48
Docker 容器时区配置怎么做才稳妥?Docker 容器里的时间不对,最先影响的一般不是页面展示,而是日志、定时任务和排查问题的效率。比如宿主机已经是北京时间,容器日志却显示 UTC,凌晨任务提前 8 小时执行,排查起来很容易绕晕。
Docker 容器时区配置常见有几种做法:设置 `TZ` 环境变量、安装 `tzdata`、挂载宿主机时区文件,或者在 Compose、Kubernetes 中统一声明。实际选哪一种,要看镜像基础系统、应用运行时和部署环境。
## 用 TZ 环境变量设置时区
最轻量的方式是在容器中设置 `TZ` 环境变量:
```dockerfile
ENV TZ=Asia/Shanghai
```
运行容器时也可以临时传入:
```bash
docker run -e TZ=Asia/Shanghai your-image
```
这种方式优点是简单、可移植,不依赖宿主机的 `/etc/localtime`。如果同一个镜像要部署到不同地区,只需要改环境变量,不用重新改镜像逻辑。
不过要注意,`TZ` 是否生效取决于镜像里是否有可用的时区数据。很多精简镜像没有完整的 timezone 数据库,只设置环境变量可能不够。
## Debian 和 Alpine 镜像要安装 tzdata
如果基础镜像是 Debian 或 Ubuntu,可以在 Dockerfile 里安装 `tzdata`:
```dockerfile
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata && ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone && rm -rf /var/lib/apt/lists/*
ENV TZ=Asia/Shanghai
```
如果是 Alpine:
```dockerfile
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai
```
Alpine 镜像更精简,很多时候没有预装时区数据。只写 `ENV TZ=Asia/Shanghai`,但没有 `tzdata`,程序可能仍然按 UTC 或默认时区处理。
## 挂载宿主机时区文件可以用,但别过度依赖
另一种常见写法是挂载宿主机的时区配置:
```bash
docker run -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro your-image
```
这样容器会跟随宿主机时区。它适合单机部署、内部工具或对宿主机环境强绑定的服务。
但它也有几个坑:
- 不同 Linux 发行版不一定都有 `/etc/timezone`;
- macOS、Windows Docker Desktop 的路径语义和 Linux 不完全一样;
- 容器和宿主机绑定过紧,迁移到 Kubernetes 或其他环境时容易失效;
- 如果宿主机时区配置错误,容器也会一起错。
所以生产环境更推荐把时区配置显式写进镜像或部署文件,而不是假设宿主机一定正确。
## Docker Compose 里怎么写
Compose 中通常直接配置环境变量:
```yaml
services:
app:
image: your-image
environment:
TZ: Asia/Shanghai
```
如果你的镜像需要系统级时区文件,也可以挂载:
```yaml
services:
app:
image: your-image
environment:
TZ: Asia/Shanghai
volumes:
- /etc/localtime:/etc/localtime:ro
```
更稳妥的做法是:镜像里安装好 `tzdata`,部署层只负责传 `TZ`。这样 Compose、Kubernetes、普通 `docker run` 都能复用同一套镜像。
## Kubernetes 中怎么配置
Kubernetes 里一般通过环境变量设置:
```yaml
containers:
- name: app
image: your-image
env:
- name: TZ
value: Asia/Shanghai
```
如果确实要挂载宿主机的 `/etc/localtime`,可以用 `hostPath`,但这会让 Pod 依赖节点环境,不利于迁移和调度。除非你很清楚集群节点的时区配置一致,否则不建议作为默认方案。
## UTC 还是本地时区,怎么选
很多团队会纠结:容器到底该用 UTC,还是用 `Asia/Shanghai`?
如果是国际化系统、跨地区服务、分布式链路追踪,UTC 更适合作为统一存储和计算时间。数据库、消息、审计日志使用 UTC,可以减少夏令时和跨时区换算问题。
如果是面向国内业务、内部管理后台、定时任务强依赖本地时间,用 `Asia/Shanghai` 会更直观。比如每天 9 点发报表、凌晨 2 点跑清算,本地时区能降低理解成本。
比较稳的原则是:**系统内部时间尽量统一,展示层再转换成本地时区**。如果日志、数据库、应用各用各的时区,问题会非常难查。
## 日志和 cron 最容易暴露时区问题
时区配置不一致,最常见的两个问题是日志和定时任务。
日志方面,容器内 `date` 显示北京时间,但应用日志仍然是 UTC,通常说明应用运行时没有读取系统时区,或者日志框架单独配置了时区。
cron 方面,容器里的 cron 会按容器系统时区执行。如果容器实际是 UTC,而你按北京时间写了 crontab,任务就会偏 8 小时。
可以先在容器里确认时间:
```bash
docker exec -it container-name date
```
也可以看时区文件:
```bash
docker exec -it container-name sh -c 'date && ls -l /etc/localtime'
```
如果安装了 `tzdata`,还可以检查:
```bash
zdump Asia/Shanghai | head
```
## 应用运行时也可能有自己的时区规则
容器系统时区正确,不代表应用一定正确。Java、Node.js、Python 都可能有自己的处理方式。
### Java
Java 常见做法是设置 JVM 参数:
```bash
-Duser.timezone=Asia/Shanghai
```
Spring Boot 项目还可能涉及 Jackson 序列化时区、数据库连接时区等配置。如果接口返回时间偏移,不能只看容器的 `date`。
### Node.js
Node.js 会受 `TZ` 环境变量影响,但不同运行环境和镜像差异较大。建议在启动前设置:
```bash
TZ=Asia/Shanghai node server.js
```
如果项目使用 dayjs、moment、luxon 这类库,还要确认是否启用了对应的 timezone 插件或配置。
### Python
Python 的 `datetime.now()`、`time.localtime()` 会受系统时区影响,但推荐业务代码使用明确的 timezone-aware datetime,避免混用 naive datetime。
例如在 Python 3.9+ 中可以使用:
```python
from zoneinfo import ZoneInfo
from datetime import datetime
now = datetime.now(ZoneInfo("Asia/Shanghai"))
```
这样代码表达更清楚,也不完全依赖容器系统配置。
## NTP 负责校准时间,不负责选择时区
NTP 解决的是“时间准不准”,不是“显示哪个时区”。容器通常不需要单独跑 NTP 客户端,因为容器共享宿主机内核时间,宿主机时间同步正常,容器拿到的时间基准也正常。
如果容器时间整体漂移,应该优先检查宿主机或节点的 NTP、chrony、systemd-timesyncd 配置。容器里单独跑 NTP 反而会增加权限和运维复杂度。
## 推荐做法
如果只是普通业务容器,推荐这样处理:
1. 镜像中安装 `tzdata`;
2. 使用 `ENV TZ=Asia/Shanghai` 或部署文件传入 `TZ`;
3. 不默认依赖宿主机 `/etc/localtime`;
4. Java、Node.js、Python 等运行时单独确认时区行为;
5. 用 `docker exec date`、应用日志、cron 执行时间一起验证。
容器时区配置看起来只是一个小参数,但它会影响日志排查、定时任务、数据审计和用户看到的时间。最怕的不是用 UTC 或北京时间,而是系统里同时混着几套时间规则。服务端6月19日 19:34
Docker 容器成本优化有哪些实用方法?Docker 容器成本优化不能只盯着“少跑几个容器”。真正花钱的地方通常藏在镜像存储、节点空转、日志膨胀、网络出口、过度预留和不合理扩缩容里。比较稳妥的做法是先看账单和监控,再决定优化顺序。
## 先把镜像做小
镜像越大,构建、拉取、存储和发布都会变慢,也会增加镜像仓库费用。常见做法有三类:
- 使用更轻的基础镜像,例如 `alpine`、`slim` 或 `distroless`;
- 用多阶段构建,只把运行时真正需要的二进制、依赖和配置复制到最终镜像;
- 清理构建缓存、包管理器缓存、临时文件,避免把测试文件、源码和文档一起打进生产镜像。
不过轻量镜像不是无脑选择。比如 Alpine 使用 musl libc,某些依赖在兼容性和性能上可能踩坑。更稳的做法是对核心服务做一次启动耗时、镜像体积和运行稳定性的对比,再决定是否切换。
## 管好镜像仓库生命周期
很多团队只优化 Dockerfile,却忘了 registry 也在持续花钱。每次 CI/CD 都推一个新 tag,半年后镜像仓库里可能堆着几千个历史版本。
可以设置镜像生命周期策略:
- 生产镜像保留最近 N 个稳定版本;
- 开发、测试、PR 临时镜像设置较短过期时间;
- 未被部署引用的镜像定期清理;
- 大镜像单独告警,避免某次构建把体积突然拉高。
如果使用 Harbor、ECR、GCR、ACR 等仓库,通常都支持保留规则或自动清理策略。注意不要只按 tag 删除,最好确认当前集群、回滚版本和灾备流程不会依赖这些镜像。
## 正确设置 requests 和 limits
在 Kubernetes 里,成本浪费最常见的来源之一是资源申请不准。`requests` 决定调度时预留多少资源,`limits` 决定容器最多能用多少资源。
如果 `requests` 设得太高,节点看起来已经满了,但真实 CPU 使用率可能只有 20%。这会导致集群不断扩容,钱花在空转节点上。反过来,如果设得太低,Pod 容易被挤在一起,出现 CPU 抢占、内存 OOM 或延迟抖动。
建议做法是:
- 根据最近 7 到 30 天的真实监控数据设置 requests;
- CPU requests 可以相对保守,CPU limits 不一定必须设置得很死;
- 内存 limit 要更谨慎,因为超过后可能直接 OOMKilled;
- 对不同服务分层,核心链路比离线任务留更多余量。
有条件的话,可以配合 VPA 或成本分析工具给出建议值,但不要让它在生产环境里随意自动改核心服务配置。
## 自动扩缩容要设好上下限
HPA、KEDA、Cluster Autoscaler 可以让容器数量和节点数量跟随负载变化,但配置不好也会浪费钱。
关键是三个参数:最小副本数、最大副本数和扩缩容指标。最小副本数太高,低峰期也会空跑;太低,流量上来时冷启动又可能扛不住。最大副本数太高,异常流量或错误指标可能把成本瞬间拉爆。
比较实用的做法是:
- 核心在线服务保留合理的最低副本数;
- 后台任务、消费型服务按队列长度或事件数扩缩容;
- 设置最大副本数,避免异常流量导致无限扩容;
- 配置缩容冷却时间,防止频繁扩缩容造成抖动。
扩缩容不是为了“永远省钱”,而是在低峰少花钱、高峰不崩。
## 提高节点装箱率
容器成本优化还有一个很容易被忽略的词:bin packing,也就是把 Pod 更合理地装进节点里。
如果每个服务的 requests 都偏大,或者节点规格选得不合适,就会出现很多碎片资源:这个节点还剩一点 CPU,那个节点还剩一点内存,但都不足以再调度一个 Pod。结果就是集群明明总体资源没用完,却还要继续加节点。
可以从这些方向优化:
- 选择更匹配业务负载的节点规格;
- 把 CPU 密集型和内存密集型服务合理混部;
- 使用 node affinity、taints、tolerations 控制关键服务位置;
- 对离线任务使用低优先级,避免抢占在线服务资源;
- 定期查看节点利用率和不可调度 Pod 的原因。
Kubernetes 的调度不是魔法,它只能根据你填的 requests 做判断,所以前面的资源设置会直接影响装箱率。
## 使用 Spot 或抢占式实例要留后手
Spot、Preemptible、竞价实例通常能明显降低节点成本,适合跑可重试、可中断、无状态或离线计算任务。
但它们的风险也很明确:实例可能随时被回收。如果把核心数据库、关键在线服务、长时间不可重试任务放上去,省下来的钱可能不够一次故障损失。
更稳的使用方式是:
- 在线核心服务优先跑在按量或预留实例上;
- 批处理、CI、异步消费、数据处理放到 Spot 节点池;
- 配置 PodDisruptionBudget,避免一次回收影响太多副本;
- 应用层支持重试、断点续跑和幂等处理;
- 保留一定按量节点兜底。
Spot 是降成本工具,不是免费午餐。
## 做 rightsizing,不要长期用大规格
很多容器一开始为了省事会给很大的 CPU、内存和节点规格,后面业务稳定了却没人再回头看。这类“历史遗留余量”会持续烧钱。
Rightsizing 的思路是把实际使用量、峰值、SLO 和资源配置放在一起看:
- 长期 CPU 使用率很低的服务,可以下调 requests;
- 内存稳定且峰值清晰的服务,可以收紧 limit;
- 节点长期低利用率,可以换更小规格或减少节点数;
- 有明显周期波动的业务,用定时扩缩容比固定高配更划算。
不要只看平均值。平均 CPU 10% 的服务,可能每天有 10 分钟冲到 90%。优化前要看 P95、P99 和业务高峰窗口。
## 控制日志、存储和网络出口费用
容器本身不贵,旁边的配套资源经常更贵。
日志是典型例子。默认把 debug 日志全量打到集中式日志系统,时间一长,采集、索引和存储都会变成大头。可以按环境和服务级别调整日志等级,给高频日志做采样,设置合理保留周期。
存储也类似。共享数据卷可以避免重复存储,但要注意容量、快照、备份和 IOPS 是否过度配置。临时文件、缓存目录、构建产物最好有明确清理策略。
网络出口费用更容易被低估。跨可用区、跨地域、出公网传输都可能收费。如果服务频繁拉取大镜像、跨区访问对象存储,账单会很难看。可以通过就近部署、镜像缓存、私有网络访问和减少跨区调用来控制成本。
## 清理资源要安全
`docker system prune`、清理未使用镜像和删除停止容器确实能释放空间,但生产环境不能随手执行。
更安全的做法是:
```bash
docker system df
docker image prune -a --filter "until=168h"
docker container prune --filter "until=168h"
```
先查看占用,再按时间窗口清理。不要在不了解依赖的情况下删除 volume,因为数据卷里可能有业务数据。Kubernetes 环境下,也要区分节点本地缓存、PVC、镜像缓存和日志文件,清理策略不能一刀切。
## 用监控和成本分摊定位问题
没有成本归因,优化只能靠猜。建议至少按 namespace、应用、团队、环境打标签,把 CPU、内存、存储、日志、网络和节点费用分摊到具体业务。
常见观察指标包括:
- Pod 的 CPU、内存 requests 与真实使用量对比;
- 节点利用率和空闲资源;
- 镜像体积和拉取频率;
- 日志写入量、索引量和保留周期;
- 网络出口流量和跨区流量;
- 每个 namespace 或团队的单位成本。
这样才能知道该先优化镜像、节点、日志,还是网络。很多时候,最值得动手的不是技术上最酷的部分,而是账单里增长最快的那一项。
## Docker 和 Kubernetes 场景有什么不同
如果只是单机 Docker,重点通常是镜像体积、容器数量、资源限制、磁盘清理和日志轮转。
如果是 Kubernetes,成本优化会多出调度和集群层面的内容:requests/limits、HPA、VPA、Cluster Autoscaler、节点池、Pod 分布、PDB、namespace 成本分摊、Spot 节点池等都要一起看。
所以 Docker 容器成本优化可以按这个顺序推进:先减小镜像和仓库存储,再校准资源申请,然后优化扩缩容和节点装箱率,最后处理日志、存储、网络出口和成本归因。这样改动风险比较低,也更容易看到真实账单变化。服务端6月19日 19:34
Docker 容器灾难恢复计划要备份和演练什么?Docker 容器灾难恢复计划,不能只写一句“定期备份镜像和数据卷”。真正出问题时,决定恢复速度的往往不是镜像在不在,而是配置能不能还原、数据是不是一致、依赖服务有没有顺序、账号密钥是否还能用,以及团队是否知道第一步该做什么。
一个可执行的 Docker 灾备方案,至少要回答三个问题:丢了什么能恢复、多久能恢复、最多能接受丢多少数据。
## 先定清楚 RTO 和 RPO
灾难恢复计划先别急着写命令,先定两个指标:
- **RTO(恢复时间目标)**:服务中断后,最多允许多久恢复。例如官网 30 分钟、内部报表 4 小时。
- **RPO(恢复点目标)**:最多允许丢多少数据。例如订单库最多丢 5 分钟数据,日志系统可以丢 1 小时。
这两个值会直接影响备份频率、存储成本和架构复杂度。RPO 要求越小,越不能只靠每天一次的文件备份,通常需要数据库主从、增量备份、对象存储版本控制或跨区域复制。RTO 要求越短,就越依赖自动化脚本、预热环境和清晰的恢复 runbook。
## Docker 灾备到底要备份什么
很多事故恢复失败,是因为只备份了镜像,却漏掉了运行时配置和数据。Docker 环境至少要覆盖下面几类资产。
### 镜像和镜像仓库
镜像可以用 `docker save` 导出:
```bash
docker save -o app-web.tar registry.example.com/app/web:2026-06-01
docker load -i app-web.tar
```
但 `docker save/load` 更适合少量镜像或离线环境兜底,不适合作为长期主备方案。它不会替你管理镜像版本、扫描漏洞或清理过期层,也不解决服务如何重新跑起来。更稳妥的方式是维护私有镜像仓库,并备份 registry 存储后端、访问凭证、复制策略和保留规则。
### 编排配置和启动参数
恢复容器不能靠记忆。下面这些配置都应该进入版本管理或安全备份:
- `docker-compose.yml`、`.env`、override 文件;
- Kubernetes 的 Deployment、StatefulSet、Service、Ingress、ConfigMap、Secret、PVC 等 YAML;
- 容器启动参数,例如端口映射、网络、挂载路径、健康检查、重启策略;
- Nginx、网关、服务发现、负载均衡配置;
- 定时任务、消费者、后台 worker 的启动方式;
- CI/CD 部署脚本和环境变量模板。
如果历史服务没有 compose 文件,可以用下面的命令把现有容器配置先导出来,作为整理依据:
```bash
docker inspect app-web > app-web.inspect.json
```
`docker inspect` 更像事故调查记录,真正可维护的恢复配置应该沉淀为 Compose、Helm Chart、Kustomize 或 Terraform 等可重复执行的文件。
### 数据卷、挂载目录和上传文件
容器本身应该尽量无状态,真正要命的是 volume、数据库和用户上传文件。Docker volume 可以用临时容器打包:
```bash
docker run --rm -v app_data:/data -v /backup:/backup alpine tar czf /backup/app_data_$(date +%F).tar.gz -C /data .
```
恢复时反向解包:
```bash
docker run --rm -v app_data:/data -v /backup:/backup alpine tar xzf /backup/app_data_2026-06-01.tar.gz -C /data
```
如果 volume 中存的是数据库文件,不建议在数据库运行时直接 tar 目录。MySQL、PostgreSQL、MongoDB、Redis 都应该使用各自的备份工具或快照机制,例如 `mysqldump`、`pg_dump`、WAL 归档、逻辑备份、存储卷快照等。
### 数据库、外部依赖和密钥
应用能不能恢复,还取决于 MySQL、PostgreSQL、Redis、消息队列、对象存储、CDN、DNS、证书、OAuth 回调地址、支付和短信服务。建议维护一张依赖关系图:哪个容器依赖哪个数据库、队列、存储桶、域名和密钥。恢复时先恢复底层依赖,再恢复业务服务。
`.env`、Kubernetes Secret、TLS 证书、JWT 密钥、数据库密码不能和普通配置一样随便放进仓库。它们需要加密备份,并明确谁有权限解密。灾备演练时要验证密钥是否能被正确拉取,而不是只验证文件存在。
## 恢复流程要写成 runbook
灾难发生时,没人愿意在凌晨临时翻聊天记录。恢复流程应该写成 runbook,按步骤执行:
1. 确认事故范围:单容器异常、宿主机损坏、机房故障,还是镜像仓库不可用。
2. 冻结现场信息:保留日志、容器 inspect、宿主机磁盘和网络状态。
3. 准备目标环境:确认 Docker 版本、内核参数、磁盘挂载、网络、防火墙、时区和系统依赖。
4. 恢复镜像:优先从镜像仓库拉取固定 tag 或 digest,必要时使用 `docker load`。
5. 恢复配置:应用 compose、Kubernetes manifests、环境变量、Secret、证书和网关配置。
6. 恢复数据:先恢复数据库,再恢复 volume、上传文件、对象存储索引等业务数据。
7. 按依赖顺序启动服务:数据库、缓存、队列、后端服务、前端网关、定时任务依次恢复。
8. 验证功能:检查健康接口、登录、下单、上传、异步任务、回调等关键路径。
9. 切换流量:通过 DNS、负载均衡、网关或 Kubernetes Service 将流量切到恢复环境。
10. 复盘记录:记录实际 RTO、实际 RPO、失败步骤和需要补齐的自动化脚本。
容器状态是 `running` 不代表业务已经恢复。真正的验证应该来自业务探针,例如能否创建订单、能否写入数据库、队列是否消费、文件是否能访问。
## 高可用设计不能等灾难后再补
备份解决的是“坏了以后能不能回来”,高可用解决的是“坏的时候能不能少中断”。Docker 单机部署至少要配置重启策略、健康检查、磁盘和 Docker daemon 监控,并把日志采集到集中系统。
如果业务要求更高,应该考虑多节点和跨区域:使用 Kubernetes、Docker Swarm 或云容器服务做多副本调度;数据库采用主从复制、跨可用区部署或托管数据库;镜像仓库做跨区域复制;对象存储开启版本控制和跨区域复制;流量入口支持 DNS Failover 或负载均衡故障转移。
跨区域灾备要特别小心数据一致性。应用服务跨区域比较容易,数据库跨区域才是难点。同步复制延迟低但成本高,异步复制便宜但可能丢数据,这要回到 RPO 来取舍。
## 备份是否可用,必须靠演练证明
没有恢复演练的备份,只能算心理安慰。建议至少做三类测试:备份完整性检查、局部恢复演练、全链路灾备演练。演练时要记录实际 RTO、实际 RPO、失败步骤、人工操作和业务验证结果。
如果每次演练都要靠某个老员工“凭经验操作”,这本身就是风险。灾备计划应该让新同事也能按文档恢复到可用状态。
## 监控和告警也属于灾备的一部分
灾备不是从服务挂掉才开始。建议监控容器重启次数、退出码、健康检查状态、宿主机 CPU/内存/磁盘/inode、Docker daemon 状态、镜像仓库拉取失败率、数据库复制延迟、备份任务成功率、队列积压和关键接口成功率。
备份任务也要有告警。最糟糕的情况不是备份失败,而是备份失败了三个月没人知道。
## 常见坑
1. 只备份容器,不备份数据卷:容器能启动,但业务数据没了。
2. 只备份数据,不备份配置:数据在,但服务跑不起来。
3. 镜像使用 latest 标签:恢复时拉到的可能不是事故前版本。
4. 数据库热备方式不对:直接压缩运行中的数据目录,恢复后数据损坏。
5. Secret 没有备份或无法解密:服务启动后连不上数据库和第三方接口。
6. 没有依赖顺序:应用先启动,数据库和队列没恢复,导致大量报错。
7. 没有恢复验证:容器显示 running,但核心业务路径不可用。
8. 备份和生产在同一台机器:宿主机磁盘坏了,备份也一起没了。
## 一份可落地的 Docker 灾备清单
- RTO、RPO 已按业务等级定义;
- 镜像使用固定 tag 或 digest,并有可用镜像仓库;
- Compose、Kubernetes manifests、环境变量模板进入版本管理;
- Secret、证书、Token 有加密备份和恢复权限说明;
- volume、上传文件、对象存储有独立备份策略;
- 数据库使用官方工具或可靠快照备份;
- 依赖关系图清楚标出数据库、队列、缓存、外部接口;
- 恢复 runbook 写明执行顺序、命令、负责人和验证方式;
- 备份任务、复制延迟、容器健康状态都有监控告警;
- 至少定期做一次局部恢复演练,核心系统做全链路演练。
Docker 容器灾难恢复计划的重点,不是把所有命令都背下来,而是把“能不能恢复”变成一件可验证、可重复、可交接的事。镜像、配置、数据、密钥、依赖和演练缺一不可。服务端6月19日 19:07
Docker 容器安全扫描怎么做?工具怎么选?容器安全扫描的核心,是在镜像进入生产环境前尽早发现风险:基础镜像有没有 CVE,应用依赖有没有漏洞,构建文件里有没有把密钥写进去,许可证会不会带来合规问题。它只适用于自己有权构建、维护或部署的镜像和仓库,属于防御性安全检查,不是拿工具去扫描别人的系统。
## 容器安全扫描到底要扫什么?
只扫“镜像漏洞”远远不够。一次比较完整的容器安全扫描,通常会覆盖这些内容:
| 扫描对象 | 重点看什么 | 常见处理方式 |
| --- | --- | --- |
| 操作系统包 | glibc、openssl、curl、apt/apk/yum 包里的 CVE | 更新基础镜像、升级系统包、换更小的基础镜像 |
| 应用依赖 | npm、pip、Maven、Go module、Ruby gem 等依赖漏洞 | 升级依赖、替换废弃库、锁定安全版本 |
| 敏感信息 | Token、私钥、数据库密码、云厂商 AK/SK | 立即撤销泄露凭据,改用 Secret Manager 或 CI 密钥注入 |
| Dockerfile 和 IaC | root 用户运行、特权模式、暴露多余端口、危险挂载 | 调整 Dockerfile、Kubernetes YAML、Helm Chart、Terraform 配置 |
| SBOM | 镜像里到底包含哪些包、版本和来源 | 生成 CycloneDX 或 SPDX 清单,便于审计和追踪 |
| License | GPL、AGPL、商业限制许可证等 | 按公司策略拦截或人工确认 |
| 签名和来源 | 镜像是否来自可信构建流程,是否被篡改 | 使用 cosign 等工具签名和验签 |
如果团队只在发布前跑一次漏洞扫描,很容易漏掉两类问题:一类是密钥、配置和许可证风险,另一类是镜像发布后新披露的 CVE。因此扫描不应该只发生在构建阶段,还要覆盖仓库中的存量镜像和生产准入环节。
## 常用工具怎么选?
| 工具 | 特点 | 更适合的场景 | 注意点 |
| --- | --- | --- | --- |
| Docker Scout | Docker 官方工具,和 Docker CLI、Docker Hub、Docker Desktop 集成较好,能给出基础镜像更新建议 | 已经使用 Docker 官方生态,希望快速看到镜像风险和修复建议 | 企业级策略、跨平台治理能力要看套餐和使用方式 |
| Trivy | 开源,能扫镜像、文件系统、Git 仓库、Kubernetes 配置、IaC、Secret、SBOM、License | 中小团队、CI/CD 集成、想用一个工具覆盖多类风险 | 默认规则很多,建议按项目配置忽略规则和严重级别 |
| Grype | Anchore 开源漏洞扫描器,常和 Syft 搭配生成 SBOM | 想先生成 SBOM,再基于 SBOM 做漏洞分析的团队 | Secret、IaC 不是它的核心能力,需要配合其他工具 |
| Clair | 开源镜像漏洞扫描服务,适合接入镜像仓库 | 自建 Registry、需要服务化扫描能力 | 主要聚焦镜像包漏洞,落地和维护成本比 CLI 工具高 |
| Anchore | 企业级供应链安全平台,包含策略、SBOM、合规和治理能力 | 大型团队、多业务线、需要统一策略管理和审计 | 成本和平台建设复杂度更高 |
| Snyk | 商业服务,依赖漏洞、容器、IaC、许可证和开发者工作流体验较成熟 | 已经重度使用 GitHub/GitLab/Jira,希望把修复流转起来 | 扫描效果和可用功能与套餐相关 |
简单选型可以这样看:个人项目或小团队先用 Trivy;偏 SBOM 工作流可用 Syft + Grype;已经在 Docker Hub 和 Docker Desktop 里协作,可以先接 Docker Scout;需要企业级策略、审计和工单流转,再评估 Anchore 或 Snyk;自建镜像仓库并愿意维护扫描服务,可以考虑 Clair。
## CI/CD 里怎么设置拦截规则?
安全扫描最怕两个极端:要么只出报告不拦截,大家慢慢无视;要么所有漏洞都拦,最后流水线天天红。比较稳的做法是分层处理。
可以先设置这些阈值:
- **Critical 必须阻断发布**,尤其是有可用修复版本、可远程利用、出现在运行路径上的漏洞。
- **High 默认阻断**,但允许短期例外;例外必须写明原因、负责人和过期时间。
- **Medium 进入缺陷池**,结合是否暴露在公网、是否运行在生产环境决定修复优先级。
- **Secret 命中直接阻断**,并且不能只删除代码,还要轮换已经泄露的密钥。
- **License 命中按公司策略处理**,例如 AGPL、未知许可证进入人工审批。
- **基础镜像过旧要提醒或阻断**,例如超过 30-60 天没有重建,哪怕应用代码没变也要重新构建。
一个常见的 Trivy CI 命令大概是这样:
```bash
trivy image --severity HIGH,CRITICAL --exit-code 1 your-image:tag
```
但生产里不要只靠一条命令。更合理的是把策略写进 CI 模板:哪些严重级别失败、哪些目录跳过、哪些漏洞暂时忽略、忽略到什么时候失效。这样规则才不会散落在每个仓库里。
## 如何处理误报和“看起来没法修”的漏洞?
容器扫描会遇到误报,尤其是发行版 backport 补丁。比如 Debian 或 Ubuntu 可能已经把修复补丁回合到旧版本包里,但版本号没有升到上游公告里的新版本,工具如果只看版本号,就可能误判。
处理方式不是简单把漏洞加入 ignore,而是先确认三件事:
1. 漏洞对应的包是否真的存在于最终镜像层里;
2. 漏洞代码路径是否会被应用调用;
3. 发行版安全公告是否已经说明该版本已修复。
如果确实是误报,可以加入忽略文件,但要写清原因和过期时间。没有过期时间的忽略规则,最后往往会变成“永久免死金牌”。
还有一种情况是漏洞暂时没有修复版本。这时可以做风险缓解:移除不需要的包,关闭相关功能,限制容器权限,缩小网络访问范围,或者换一个维护更及时的基础镜像。
## 基础镜像更新和重建为什么重要?
很多团队以为应用代码没变,就不用重新构建镜像。实际不是这样。基础镜像里的 openssl、glibc、ca-certificates 等包会持续更新,旧镜像即使昨天还是安全的,今天也可能因为新披露的 CVE 变成高危。
建议把基础镜像治理纳入日常流程:
- 优先使用官方、可信、仍在维护的基础镜像;
- 尽量固定 digest,避免同一个 tag 在不同时间指向不同内容;
- 定期刷新基础镜像并重新构建业务镜像;
- 使用多阶段构建,最终镜像只保留运行所需文件;
- 能用 slim、distroless、alpine 时再用,不要为了小而牺牲兼容性和可维护性;
- 删除构建工具、包管理缓存和临时文件,减少攻击面。
修复漏洞不一定是“在容器里 apt upgrade 一下”。更推荐修改 Dockerfile 或基础镜像版本,然后重新构建、扫描、签名和发布,保证过程可追踪。
## 签名、准入控制和运行时扫描怎么配合?
镜像扫描解决的是“镜像里有什么风险”,签名解决的是“这个镜像是不是可信流程产物”,准入控制解决的是“能不能进入集群”。三者最好连起来。
常见链路是:
1. CI 构建镜像;
2. 生成 SBOM;
3. 执行漏洞、Secret、IaC、License 扫描;
4. 达到策略阈值后用 cosign 签名;
5. 推送到镜像仓库;
6. Kubernetes 准入控制检查签名、镜像来源、扫描结果和基础安全配置;
7. 不满足条件的镜像禁止部署。
准入控制可以用 Kyverno、OPA Gatekeeper、Connaisseur 或云厂商自带策略能力。规则不必一开始写得很重,可以先从“禁止 latest 标签”“必须来自可信 Registry”“必须非 root 运行”“必须有签名”这些低争议规则开始。
运行时扫描也有边界。镜像扫描看不到容器启动后的异常行为,例如进程被注入、容器逃逸尝试、异常网络连接、运行时挂载了危险目录。这部分要靠 Falco、eBPF Runtime Security、Kubernetes Audit、云安全产品或主机入侵检测来补齐。
所以,容器安全不是某一个工具的事。镜像扫描负责提前发现已知风险,CI 策略负责把风险挡在发布前,签名和准入控制负责防止不可信镜像进入环境,运行时监控负责发现上线后的异常行为。把这几步串起来,才算真正把 Docker 容器安全扫描落到了工程流程里。服务端6月19日 18:55
Docker 容器与 Kubernetes 是什么关系?Docker 容器和 Kubernetes 不是替代关系。Docker 更像开发者打包、构建、运行容器的一套工具,Kubernetes 是在一组机器上管理容器的编排系统。生产集群里,Kubernetes 通常不会直接调用 Docker CLI,而是通过 CRI 调用 containerd、CRI-O 这类运行时,再由 runc 创建真正的 Linux 容器进程。
还有一个容易误解的点:Kubernetes 不再内置 dockershim,不等于 Docker 镜像不能用了。Docker 构建出的镜像只要符合 OCI 标准,containerd 和 CRI-O 仍然可以正常拉取、运行。
## Docker 负责什么?
日常说 Docker,通常混着指几件事:
- **Docker CLI / Dockerfile / BuildKit**:写 Dockerfile、构建镜像、推送到镜像仓库;
- **Docker Engine**:在本机管理容器的守护进程;
- **containerd / runc**:拉取镜像、管理容器生命周期、创建容器进程的底层组件。
本地开发时,Docker Desktop 或 Docker Engine 很方便。一个 `docker build` 生成镜像,一个 `docker run` 就能验证服务能不能跑起来。它主要解决的是“怎么把应用和依赖打成一个可迁移的包”。
## Kubernetes 负责什么?
Kubernetes 关心的是另一层问题:当容器不只一个,而是跑在几十台、几百台机器上时,谁来决定它们放在哪台机器?挂了谁来拉起?流量怎么进来?副本怎么扩容?
这些才是 Kubernetes 的核心职责:
- **调度**:根据资源、亲和性、污点等规则把 Pod 放到合适节点;
- **自愈**:容器或节点异常时重新创建 Pod;
- **扩缩容**:按副本数或指标调整服务规模;
- **服务发现与负载均衡**:用 Service、Ingress 等把流量导向后端 Pod;
- **配置与发布管理**:配合 ConfigMap、Secret、Deployment 做滚动发布和回滚。
所以,Docker 解决的是“怎么把应用装进容器并运行”,Kubernetes 解决的是“怎么在集群里可靠地管理大量容器”。
## Docker 和 Kubernetes 怎么衔接?
Kubernetes 不会直接执行 `docker run`。它通过 **CRI(Container Runtime Interface)** 和节点上的容器运行时通信。常见运行时是 **containerd** 和 **CRI-O**。运行时再调用更底层的 **runc** 创建容器进程。
早期 Kubernetes 为了兼容 Docker Engine,内置过一个 dockershim 适配层。Kubernetes v1.20 开始弃用 dockershim,v1.24 正式移除。移除的是内置 Docker Engine 适配层,不是 Docker 镜像格式。
现在大多数新集群会直接使用 containerd。Docker Engine 本身不是 CRI 运行时;如果确实要让 Kubernetes 继续对接 Docker Engine,需要额外安装 **cri-dockerd** 来做适配。
## 实际项目里怎么分工?
常见流程是:
1. 开发者写 Dockerfile;
2. 用 Docker CLI 或 CI 中的 BuildKit 构建镜像;
3. 把镜像推到镜像仓库;
4. Kubernetes 从仓库拉取镜像;
5. 节点上的 containerd 运行容器;
6. Kubernetes 负责调度、扩容、重启和对外暴露服务。
本地开发继续用 Docker 很正常,因为它简单、反馈快。生产环境通常把 Docker 的“构建镜像”能力留在开发机或 CI,把“运行和编排”交给 Kubernetes + containerd。这样分工更清楚,也更符合现在 Kubernetes 集群的主流做法。服务端6月19日 18:55
Docker 容器版本管理如何避免 latest 和回滚事故?Docker 容器版本管理的关键不是给镜像随手打个 `v1.0.0`,而是让每一次发布都能回答三件事:运行的到底是哪份镜像、它从哪次代码构建出来、出问题时能不能快速回到上一版。
## 镜像标签怎么设计?
推荐同时使用三类标签:
- **不可变发布标签**:`1.8.3` 或 `v1.8.3`,一旦推送后禁止覆盖。
- **可追踪构建标签**:`1.8.3-git.ab12cd3-build.4821`,把 semver、Git SHA、CI build id 绑在一起。
- **晋级标签**:`dev`、`staging`、`prod` 只表示当前环境正在验证或运行哪个版本,不要把它当长期版本号。
`latest` 可以用于本地测试,但生产环境尽量不要使用。它会随着推送变化,同一个部署文件今天和明天拉到的可能不是同一份镜像,排查事故时会很痛苦。
## 为什么要固定 digest?
标签可以被覆盖,digest 不会。部署到生产时最好写成:
```yaml
image: registry.example.com/app/api:1.8.3@sha256:xxxx
```
标签方便人读,digest 保证机器拉到的字节内容一致。Kubernetes、Docker Compose、Helm 都可以使用 digest pinning,这也是做可重复部署的底线。
## CI/CD 应该怎么发版?
常见流程是:代码合并后构建镜像,打上 semver + Git SHA + build id,生成 SBOM,使用 cosign 签名,再推送到 registry。镜像通过测试后,不要重新构建一份“生产镜像”,而是把同一个 digest 从 `staging` 晋级到 `prod`。
发版记录也要跟上。release notes 至少写清楚变更内容、镜像 digest、数据库迁移、配置变动和回滚方式。出了问题时,靠“我记得好像是上周那个包”基本等于没有版本管理。
## 环境标签有哪些坑?
`dev`、`staging`、`prod` 这类标签适合表达环境状态,但它们天然是可变的。如果团队直接部署 `app:prod`,又没有记录它当时指向哪个 digest,回滚时很容易回到错误版本。更稳的做法是:环境标签只做索引,部署清单仍固定具体版本和 digest。
## 回滚怎么做?
Docker Compose 可以把镜像改回上一版后执行:
```bash
docker compose pull
docker compose up -d
```
Kubernetes 更推荐保留 Deployment 修订记录:
```bash
kubectl rollout undo deployment/api
kubectl rollout status deployment/api
```
前提是上一版镜像还在 registry 里。所以 registry retention 不能只按“保留最近 N 天”粗暴清理,至少要保留当前线上版本、上一稳定版、最近若干个发布版,以及审计要求范围内的镜像。
## 安全和治理要补哪些?
版本管理不只是回滚,还包括供应链安全。建议为镜像生成 SBOM,用 cosign 或类似工具签名,并在部署阶段校验签名。旧镜像要定期清理,但不能删掉仍被生产、回滚或审计依赖的版本。
一句话,生产环境的 Docker 版本管理要做到:标签可读、digest 可验、构建可追踪、晋级不重构、回滚有旧包、清理有规则。`latest` 很省事,但它省掉的,往往是事故发生后最需要的证据。服务端6月19日 16:50
Docker自动化测试CI怎么稳定落地?很多团队把测试搬进 Docker 后,第一反应是“终于不用在 CI 机器上装一堆依赖了”。但只把 `npm test` 或 `pytest` 放进容器,还不能算真正的 Docker 自动化测试。更关键的是:测试依赖怎么启动、数据库怎么隔离、报告怎么拿出来、并行任务怎么互不影响,以及失败后现场是否还能定位。
## Docker 适合放在哪些测试环节里
Docker 最适合解决两类问题:环境不一致和依赖难复现。本地能过、CI 挂掉,往往不是代码突然变坏,而是 Node、JDK、浏览器、数据库版本或系统库不一样。把测试环境写进镜像后,测试运行条件就从“某台机器刚好装好了”变成“镜像里明确声明了”。
常见做法可以分成四层:
- **单元测试**:在测试镜像中运行 Jest、Vitest、pytest、JUnit 等,不依赖外部服务,执行最快。
- **集成测试**:用 Docker Compose 或 Testcontainers 启动数据库、Redis、MQ、对象存储等依赖,验证真实交互。
- **E2E / 浏览器测试**:用 Selenium Grid、Playwright 容器或内置浏览器镜像跑完整页面流程。
- **性能测试**:用 JMeter、k6 或 Locust 容器生成压力,配合独立网络和报告目录保存结果。
这几层不要混在一个命令里。单元测试应该几分钟内结束;集成测试可以慢一点,但要稳定;E2E 和性能测试成本高,通常放在合并前、夜间任务或发布前流水线里。
## 单元测试:先把运行环境固定住
单元测试最简单,核心是用测试镜像锁定语言版本和依赖安装方式。比如 Node 项目可以这样写一个多阶段测试镜像:
```dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS test
COPY . .
ENV NODE_ENV=test
CMD npm test -- --runInBand
FROM deps AS build
COPY . .
RUN npm run build
```
多阶段构建的好处是,依赖安装、测试、构建可以复用同一套基础层。CI 里缓存 Docker layer 后,速度通常比每次从零安装依赖更稳。对 Java、Go、Python 项目也一样:测试阶段保留测试工具和调试符号,生产阶段只复制构建产物,避免把测试依赖带进运行镜像。
单元测试容器里最好显式传入环境变量,例如:
```bash
docker run --rm \
-e NODE_ENV=test \
-e TZ=Asia/Shanghai \
-v ./reports:/app/reports \
myapp:test
```
`-v ./reports:/app/reports` 很重要。容器退出后文件系统会消失,测试报告、覆盖率、截图、trace 如果不挂载出来,CI 页面上只能看到一段失败日志,排查会很痛苦。
## 集成测试:Docker Compose 管依赖,健康检查管时机
集成测试难点不在测试代码,而在“依赖什么时候真的可用”。数据库容器启动完成不代表可以接收连接,消息队列端口打开也不代表 topic 已经初始化。所以 Compose 文件里应该写 healthcheck,并让测试服务等待依赖健康后再跑。
```yaml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: pg_isready -U test -d app_test
interval: 3s
timeout: 3s
retries: 20
tmpfs:
- /var/lib/postgresql/data
redis:
image: redis:7-alpine
healthcheck:
test: redis-cli ping
interval: 3s
timeout: 3s
retries: 20
test:
build:
context: .
target: test
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgres://test:test@db:5432/app_test
REDIS_URL: redis://redis:6379
NODE_ENV: test
volumes:
- ./reports:/app/reports
command: npm run test:integration
```
这里的 `tmpfs` 会让 PostgreSQL 数据写在内存里,测试结束后自然消失,适合临时数据库。也可以每个测试任务创建随机库名,例如 `app_test_${CI_JOB_ID}`,跑完再 drop。原则只有一个:测试数据必须是一次性的,不能让上一次失败污染下一次结果。
如果项目语言支持,Testcontainers 更灵活。它可以在测试代码里按需启动容器,拿到动态端口,再把连接信息注入测试。适合需要并行跑很多测试文件,或每个测试套件都要独立数据库的场景。
```javascript
import { PostgreSqlContainer } from "@testcontainers/postgresql";
const db = await new PostgreSqlContainer("postgres:16-alpine").start();
process.env.DATABASE_URL = db.getConnectionUri();
// run integration tests
await db.stop();
```
Compose 更像“固定一套测试环境”,Testcontainers 更像“测试自己声明需要什么依赖”。团队规模小、依赖固定时用 Compose 就够;测试隔离要求高、并行任务多时,Testcontainers 会更省心。
## E2E 和浏览器测试:别忘了浏览器也是依赖
端到端测试经常因为浏览器版本、字体、系统库不同而误报。把 Playwright 或 Selenium 放进容器,可以把浏览器、驱动和系统依赖一起锁住。
Playwright 官方镜像已经带好浏览器:
```yaml
services:
app:
build: .
environment:
NODE_ENV: test
healthcheck:
test: wget -qO- http://localhost:3000/health || exit 1
interval: 5s
timeout: 3s
retries: 30
e2e:
image: mcr.microsoft.com/playwright:v1.44.0-jammy
working_dir: /work
volumes:
- ./:/work
- ./reports/e2e:/work/playwright-report
environment:
BASE_URL: http://app:3000
depends_on:
app:
condition: service_healthy
command: npx playwright test --reporter=html
```
Selenium 也类似,可以用 `selenium/standalone-chrome` 或 Selenium Grid。重点是测试代码访问服务时不要写 `localhost:3000`。在 Compose 网络里,`localhost` 指的是当前容器自己,应该用服务名,比如 `http://app:3000`。
E2E 报告建议至少保留三样东西:失败截图、视频或 trace、浏览器控制台日志。很多 UI 问题只看断言信息看不出来,trace 文件往往能省半小时。
## 性能测试:容器化工具不等于随便压测
JMeter、Locust、k6 都可以容器化运行,但性能测试要特别注意网络和资源隔离。压测容器和被测服务如果跑在同一台 CI 机器上,CPU 抢占会让结果变形;如果目标是外部测试环境,又要避免多个流水线同时压测同一套服务。
Locust 的例子:
```yaml
services:
locust:
image: locustio/locust:2.31.1
volumes:
- ./perf:/mnt/locust
- ./reports/perf:/reports
environment:
TARGET_HOST: http://app:3000
command: -f /mnt/locust/locustfile.py --headless -u 100 -r 10 -t 5m --html /reports/locust.html
```
JMeter 也可以把 `.jmx` 脚本和结果目录挂进去,输出 JTL、HTML 报告。性能测试不要只看平均响应时间,至少要记录 p95、p99、错误率和吞吐量。CI 里可以设置门槛,例如错误率超过 1% 或 p95 超过 800ms 就失败。
## CI/CD 里怎么串起来
一条实用的流水线通常是这样的:
```yaml
stages:
- unit
- integration
- e2e
- performance
unit:
script:
- docker build --target test -t myapp:test .
- docker run --rm -v $PWD/reports:/app/reports myapp:test
integration:
script:
- docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test
after_script:
- docker compose -f docker-compose.test.yml down -v --remove-orphans
```
`--abort-on-container-exit` 可以在测试容器结束后停止依赖服务,`--exit-code-from test` 能把测试失败正确传给 CI。`down -v --remove-orphans` 则负责清理匿名卷、网络和残留容器。很多“偶发失败”其实是清理不干净造成的,比如旧数据库卷还在、旧网络里有同名服务、上一次的测试容器没退出。
并行测试时要避免共享固定资源。常见做法包括:
- 用 `COMPOSE_PROJECT_NAME=$CI_JOB_ID` 给每个任务生成独立网络和容器名。
- 给数据库名、Redis key 前缀、对象存储 bucket 加随机后缀。
- 不把端口映射到宿主机,容器之间走内部网络,减少端口冲突。
- 报告目录按任务拆开,例如 `reports/$CI_NODE_INDEX`。
## Docker 自动化测试的优势
把测试放进 Docker 后,收益通常很直接:
1. **环境一致**:本地、CI、预发用同一份镜像,减少“只在某台机器失败”。
2. **依赖可复制**:数据库、缓存、浏览器、压测工具都能随测试启动。
3. **清理成本低**:容器、网络、临时卷可以在任务结束后统一销毁。
4. **并行更容易**:每个任务一套隔离网络和临时数据库,互不抢状态。
5. **报告可沉淀**:覆盖率、JUnit XML、HTML 报告、trace 都能通过 volume 交给 CI 归档。
这些优势不是 Docker 自动发生的,需要在镜像、网络、数据和报告目录上提前设计。否则容器只是把混乱从宿主机搬进了另一个黑盒。
## 常见坑
**第一,依赖没健康就开始测。** 只写 `depends_on` 不够,要配 healthcheck,或者在测试入口里等待服务可用。
**第二,测试数据不隔离。** 共享一个长期数据库最容易制造偶发失败。优先用临时库、tmpfs、事务回滚或 Testcontainers。
**第三,报告留在容器里。** 容器退出后现场没了,记得把 `reports`、`coverage`、`playwright-report` 挂载到宿主机或 CI artifact。
**第四,网络理解错。** Compose 里服务互访用服务名,不是宿主机的 localhost。需要访问宿主机时,再考虑 `host.docker.internal` 或显式网络配置。
**第五,清理命令太温柔。** CI 失败后也要执行 `docker compose down -v --remove-orphans`,否则下一次任务可能接住上一次的脏状态。
## 什么时候不该全都放进容器
Docker 很适合标准化测试环境,但不是越多越好。纯函数单元测试如果本机运行只要 10 秒,没必要每次都强制走 Compose。性能测试如果需要稳定数据,也不应该和普通 PR 流水线挤在同一台 runner 上。更合理的方式是分层:提交时跑快速单元测试和必要集成测试,合并前跑 E2E,发布前或定时跑性能测试。
最终目标不是“所有测试都容器化”,而是让每类测试都有可复现的环境、清晰的隔离边界和可追踪的结果。做到这三点,Docker 自动化测试才真正能在 CI/CD 里长期稳定运行。服务端6月6日 20:29
Docker Swarm 怎么用?集群部署和运维命令Docker Swarm 是 Docker 自带的容器编排工具——不需要额外安装,几条命令就能把多台服务器组成集群,部署高可用服务。和 Kubernetes 比,Swarm 简单得多,适合中小规模部署。
## Swarm vs Kubernetes:怎么选
| 维度 | Docker Swarm | Kubernetes |
|------|-------------|------------|
| 复杂度 | 低(几条命令) | 高(概念多、配置复杂) |
| 学习成本 | 半天 | 几周 |
| 适用规模 | 3-50 节点 | 10-10000+ 节点 |
| 自动扩缩容 | 手动 | 自动(HPA) |
| 自愈能力 | 有(重启失败容器) | 强(多种控制器) |
| 生态 | Docker 原生 | CNCF 全家桶 |
**选 Swarm 的情况**:团队小、服务器少、不想花时间学 K8s
**选 K8s 的情况**:大规模部署、需要自动扩缩容、团队有 K8s 经验
## 初始化集群
### Manager 节点
```bash
# 在第一台服务器上初始化
docker swarm init --advertise-addr 192.168.1.10
# 输出加入命令
# docker swarm join --token SWMTKN-xxx 192.168.1.10:2377
```
### Worker 节点
```bash
# 在其他服务器上执行加入命令
docker swarm join --token SWMTKN-xxx 192.168.1.10:2377
```
### 查看集群状态
```bash
# 查看所有节点
docker node ls
# 输出
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# abc123 * node1 Ready Active Leader
# def456 node2 Ready Active
# ghi789 node3 Ready Active
```
Manager 节点负责调度和管理,Worker 节点只跑容器。Manager 也会跑容器——小集群不需要单独的管理节点。
### 节点管理
```bash
# 标记节点角色(让某些任务只跑在特定节点)
docker node update --label-add role=backend node2
docker node update --label-add role=database node3
# 排空节点(维护时把容器迁移走)
docker node update --availability drain node2
# 恢复节点
docker node update --availability active node2
```
## 部署服务
### 基本部署
```bash
# 部署一个服务,3 个副本
docker service create \
--name myapp \
--replicas 3 \
--publish 3000:3000 \
myapp:latest
```
Swarm 自动把 3 个副本分散到不同节点,内置负载均衡——访问任意节点的 3000 端口都会被路由到健康的副本。
### 使用 docker-compose 部署
```yaml
# docker-compose.yml
services:
api:
image: myapp:latest
ports:
- "3000:3000"
deploy:
replicas: 3
update_config:
parallelism: 1 # 每次更新 1 个副本
delay: 10s # 间隔 10 秒
failure_action: rollback # 失败自动回滚
rollback_config:
parallelism: 0 # 回滚时一次全部替换
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '1.0'
memory: 2G
redis:
image: redis:7
deploy:
replicas: 1
placement:
constraints:
- node.role == manager # 只跑在 manager 节点
postgres:
image: postgres:16
volumes:
- pg_data:/var/lib/postgresql/data
deploy:
replicas: 1
placement:
constraints:
- node.labels.role == database # 只跑在标记了 database 的节点
volumes:
pg_data:
```
```bash
# 部署
docker stack deploy -c docker-compose.yml myapp
# 查看服务
docker service ls
# 查看某个服务的副本
docker service ps myapp_api
```
## 滚动更新
```bash
# 更新镜像版本
docker service update --image myapp:v2.0 myapp_api
# 查看更新进度
docker service ps myapp_api
```
`update_config` 控制更新策略:
- `parallelism: 1` 每次只更新 1 个副本
- `delay: 10s` 每个副本更新后等 10 秒再更新下一个
- `failure_action: rollback` 新版本启动失败时自动回滚到旧版本
### 手动回滚
```bash
# 回滚到上一版本
docker service rollback myapp_api
```
## Overlay 网络:跨主机通信
Swarm 模式下的 Overlay 网络让不同主机上的容器直接通信:
```bash
# 创建 Overlay 网络
docker network create -d overlay my-net
```
```yaml
services:
api:
networks:
- my-net
redis:
networks:
- my-net
networks:
my-net:
external: true
```
api 容器在 node1,redis 在 node2——通过 `redis:6379` 直接访问,和单机体验一样。
## 配置和敏感信息
### Config(非敏感配置)
```bash
# 创建配置
echo "server.port=8080" | docker config create app_config -
```
```yaml
services:
api:
configs:
- source: app_config
target: /app/config.properties
configs:
app_config:
external: true
```
### Secret(敏感信息)
```bash
# 创建 Secret
echo "db_password_123" | docker secret create db_password -
```
```yaml
services:
api:
secrets:
- db_password
secrets:
db_password:
external: true
```
Secret 在容器内挂载为 `/run/secrets/db_password`,只有容器内可读,不会出现在 `docker inspect` 里。
## 常用运维命令
```bash
# 查看服务日志
docker service logs myapp_api
# 扩缩容
docker service scale myapp_api=5
# 查看服务详情
docker service inspect myapp_api
# 删除服务
docker service rm myapp_api
# 删除整个 Stack
docker stack rm myapp
# 查看集群事件
docker events --filter type=service
```
## 什么时候该从 Swarm 迁移到 K8s
- 需要自动扩缩容(HPA)
- 需要 CronJob(定时任务)
- 需要 Ingress 控制器(7 层路由)
- 需要 Pod 级别的健康检查
- 集群超过 50 个节点
在以上需求出现之前,Swarm 够用且省心。
服务端6月6日 20:29
Docker 怎么限制容器资源?CPU、内存和磁盘 IO 配置不限制容器资源,一个失控的容器就能吃光宿主机内存,拖垮同一台机器上的所有服务。Docker 提供了 CPU、内存、磁盘 IO 的精细限制手段,核心是 `docker run` 的资源参数和 docker-compose 的 `deploy.resources` 配置。
## 内存限制:最常用也最重要
```bash
# 限制最大内存 4GB
docker run -d --name myapp --memory=4g myapp:latest
# 限制内存 + 禁用 swap
docker run -d --name myapp --memory=4g --memory-swap=4g myapp:latest
```
`--memory-swap=4g` 等于 `--memory` 的值意味着容器不能用 swap。如果 `--memory-swap` 比 `--memory` 大,差值就是允许的 swap 大小。
### docker-compose 配置
```yaml
services:
app:
image: myapp:latest
deploy:
resources:
limits:
memory: 4G # 硬上限,超过会被 OOM Kill
reservations:
memory: 1G # 软保底,调度时保证至少 1G
```
### OOM 时会发生什么
容器内存超过 `limits.memory` 时,内核 OOM Killer 杀掉容器里内存占用最大的进程:
```bash
# 检查容器是否被 OOM 杀掉
docker inspect myapp --format '{{.State.OOMKilled}}'
# true = 被 OOM 杀了
# 查看 OOM 事件
dmesg | grep -i oom
```
### OOM 优先级调整
多个容器抢内存时,可以设 OOM 优先级:
```bash
# 不容易被 OOM Kill(-1000 到 1000,越小越不容易被杀)
docker run -d --name important-app --oom-score-adj=-500 myapp
# 容易被 OOM Kill(优先杀这个)
docker run -d --name cache-app --oom-score-adj=500 redis
```
核心服务(数据库、API 网关)设低值,缓存类服务(Redis、CDN)设高值。
## CPU 限制
### 限制 CPU 核数
```bash
# 最多用 2 核
docker run -d --cpus=2.0 myapp
# 最多用 0.5 核
docker run -d --cpus=0.5 myapp
```
```yaml
services:
app:
deploy:
resources:
limits:
cpus: '2.0'
reservations:
cpus: '0.5'
```
`--cpus=2.0` 不是绑核——容器可以在任意 2 个核心上运行,只是总使用时间不超过 200%。绑核用 `--cpuset-cpus`:
```bash
# 只在第 0 和第 2 个核心上运行
docker run -d --cpuset-cpus=0,2 myapp
# 只在第 1-3 个核心上运行
docker run -d --cpuset-cpus=1-3 myapp
```
绑核适合对 CPU 缓存一致性敏感的应用(如高性能计算),一般 Web 服务不需要。
### CPU 权重
多个容器抢 CPU 时,按权重分配:
```bash
# 默认权重 1024
# 高权重 = 抢到更多 CPU 时间
docker run -d --cpu-shares=2048 high-priority-app
docker run -d --cpu-shares=512 low-priority-app
```
注意:`--cpu-shares` 只在 CPU 资源紧张时生效。CPU 空闲时低权重容器也能用满 CPU。
## 磁盘 IO 限制
限制容器读写磁盘的速率,防止一个容器把磁盘 IO 吃光:
```bash
# 限制写速率 10MB/s
docker run -d \
--device-write-bps /dev/sda:10mb \
myapp
# 限制读速率 20MB/s
docker run -d \
--device-read-bps /dev/sda:20mb \
myapp
# 限制 IOPS(每秒 IO 操作数)
docker run -d \
--device-write-iops /dev/sda:1000 \
myapp
```
磁盘 IO 限制在 docker-compose 里不支持——需要 `docker run` 方式启动。
## PIDs 限制:防止进程炸弹
限制容器内的进程数,防止 fork 炸弹:
```bash
# 最多 100 个进程
docker run -d --pids-limit=100 myapp
```
```yaml
services:
app:
deploy:
resources:
limits:
pids: 100
```
没有这个限制,一个容器可以 fork 出几千个进程,耗尽宿主机的 PID 表。
## 运行时修改资源限制
不需要重建容器就能调整限制:
```bash
# 动态调整内存限制
docker update --memory=8g myapp
# 动态调整 CPU 限制
docker update --cpus=4.0 myapp
# 同时调整多个
docker update --memory=8g --cpus=4.0 myapp
```
注意:`docker update` 不能修改 `--pids-limit` 和 `--cpuset-cpus`——这些需要重建容器。
## 资源限制最佳实践
| 容器类型 | 内存 | CPU | 其他 |
|---------|------|-----|------|
| Web API | 2-4G | 1-2 核 | pids-limit: 200 |
| 数据库 | 4-16G | 2-4 核 | 绑核、禁 swap |
| 缓存 Redis | 2-8G | 1-2 核 | 禁 swap |
| 日志采集 | 512M-1G | 0.5 核 | 限制磁盘 IO |
| 后台任务 | 1-2G | 0.5-1 核 | pids-limit: 50 |
**原则**:
- 所有生产容器都设内存限制,防止单个容器拖垮宿主机
- 数据库容器禁 swap(`--memory-swap` 等于 `--memory`)
- 核心服务设低 OOM 优先级
- 不确定资源需求时先设宽松限制,用 `docker stats` 观察实际用量再收紧
服务端6月6日 20:29
Docker 私有仓库怎么搭建?Registry 和 Harbor 选型Docker Hub 是公开的,你的私有镜像不想让外人看到。企业内部需要一个私有 Registry 存放自己的镜像,CI/CD 推送镜像到私有仓库,生产服务器从私有仓库拉取。
## 最简方案:Docker 官方 Registry
Docker 官方提供了一个极简的 Registry 镜像,几分钟就能跑起来:
```bash
# 启动私有仓库
docker run -d -p 5000:5000 --name registry registry:2
```
```bash
# 推送镜像
docker tag myapp:latest localhost:5000/myapp:latest
docker push localhost:5000/myapp:latest
# 拉取镜像
docker pull localhost:5000/myapp:latest
```
这就够了——一个能推能拉的私有仓库。但生产环境需要持久化存储、认证、TLS。
### 持久化存储
```yaml
services:
registry:
image: registry:2
ports:
- "5000:5000"
volumes:
- registry_data:/var/lib/registry
volumes:
registry_data:
```
默认镜像存在 `/var/lib/registry`,用 Volume 持久化防止容器重启后镜像丢失。
### 启用 TLS
HTTP 模式下 Docker 客户端会拒绝推送(安全限制)。加 TLS:
```yaml
services:
registry:
image: registry:2
ports:
- "5000:5000"
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
volumes:
- registry_data:/var/lib/registry
- ./certs:/certs:ro
```
```bash
# 自签名证书(测试用)
mkdir certs
openssl req -newkey rsa:4096 -nodes -sha256 \
-keyout certs/domain.key \
-x509 -days 365 \
-out certs/domain.crt \
-subj "/CN=registry.example.com"
```
所有拉取镜像的机器都要信任这个证书:
```bash
# 把证书复制到 Docker 信任目录
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp certs/domain.crt /etc/docker/certs.d/registry.example.com:5000/ca.crt
sudo systemctl restart docker
```
### 基本认证
```bash
# 创建用户密码文件
mkdir auth
docker run --entrypoint htpasswd httpd:2 -Bbn admin password123 > auth/htpasswd
```
```yaml
services:
registry:
image: registry:2
environment:
REGISTRY_AUTH: htpassd
REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm"
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
volumes:
- ./auth:/auth:ro
```
```bash
# 登录后才能推送
docker login registry.example.com:5000
# Username: admin
# Password: password123
```
## Harbor:企业级私有仓库
Docker 官方 Registry 功能太简陋——没有 Web 界面、没有镜像扫描、没有 RBAC。Harbor 是 VMware 开源的企业级 Registry,补全了这些能力。
### Docker Compose 部署 Harbor
```bash
# 下载 Harbor
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xzf harbor-offline-installer-v2.10.0.tgz
cd harbor
# 编辑配置
cp harbor.yml.tmpl harbor.yml
```
```yaml
# harbor.yml 关键配置
hostname: registry.example.com
http:
port: 80
https:
port: 443
certificate: /certs/domain.crt
private_key: /certs/domain.key
harbor_admin_password: Harbor12345
data_volume: /data/harbor
```
```bash
# 安装
./install.sh
# 访问 Web 界面
# https://registry.example.com
# 用户名: admin 密码: Harbor12345
```
### Harbor 的企业级功能
| 功能 | 说明 |
|------|------|
| Web 管理界面 | 浏览镜像、标签、层信息 |
| RBAC | 项目级权限控制(只读/开发/管理员) |
| 镜像扫描 | Trivy 集成,自动扫描漏洞 |
| 镜像签名 | Docker Content Trust,防止篡改 |
| 垃圾回收 | 清理无引用的镜像层,回收磁盘 |
| 复制规则 | 跨 Registry 同步镜像 |
| 审计日志 | 记录所有推送/拉取操作 |
### 项目和权限
Harbor 用"项目"组织镜像,类似 GitHub 的仓库:
```
项目: frontend
├── api-gateway:v1.2.3
├── web-app:v2.0.0
└── auth-service:v1.0.0
项目: backend
├── user-service:v3.1.0
└── order-service:v2.5.0
```
每个项目可以设不同的成员和权限——前端团队只能推拉 frontend 项目,后端团队只能推拉 backend 项目。
## 云厂商托管 Registry
不想自己运维 Registry,用云厂商的托管服务:
| 云厂商 | 服务名 | 特点 |
|--------|--------|------|
| AWS | ECR | 与 IAM 集成,按存储计费 |
| GCP | Artifact Registry | 与 GCP IAM 集成 |
| Azure | ACR | 与 Azure AD 集成 |
| 阿里云 | 容器镜像服务 | 国内访问快 |
```bash
# AWS ECR 示例
aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
```
托管服务的优势:不需要维护服务器、自动 TLS、自动漏洞扫描。劣势:网络延迟(国内拉海外镜像慢)、费用随存储量增长。
## 选择决策
| 场景 | 推荐方案 |
|------|---------|
| 个人/小团队测试 | Docker 官方 Registry |
| 团队 5-20 人 | Harbor(Docker Compose 部署) |
| 企业生产环境 | Harbor(高可用部署) |
| 全部在云上 | 云厂商托管 Registry |
| 国内访问为主 | 阿里云容器镜像服务 |
**起步建议**:先用官方 Registry 跑起来,等需要 Web 界面和权限管理时迁移到 Harbor。
服务端6月6日 20:29
Docker 容器日志怎么管理?轮转、结构化和聚合方案容器日志管理不只是 `docker logs` ——那只能看单个容器的标准输出。生产环境需要日志轮转防止磁盘撑满、日志聚合实现集中查询、结构化日志方便检索。这篇从本地管理到集中式方案逐步展开。
## docker logs 的局限
```bash
docker logs myapp # 查看日志
docker logs -f myapp # 实时跟踪
docker logs --tail 100 myapp # 最近 100 行
```
问题:
- 容器删了日志就没了
- 多容器没法一起搜
- 没有日志轮转,磁盘会被撑满
- 没有结构化字段,搜索靠 grep
## 本地日志轮转:防止磁盘撑满
### json-file 驱动的轮转配置
```yaml
services:
app:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "10m" # 单个日志文件最大 10MB
max-file: "3" # 最多保留 3 个文件
```
这样每个容器最多占 30MB 日志(10MB × 3 个文件)。超过 10MB 自动轮转,超过 3 个文件自动删除最老的。
### local 驱动:更省磁盘
```yaml
services:
app:
logging:
driver: local
options:
max-size: "10m"
max-file: "5"
```
`local` 驱动用压缩存储,同样内容比 json-file 省 50% 空间。而且日志格式更易读。
### 全局配置
不想每个容器都写 logging 配置?在 `daemon.json` 里设全局默认:
```json
// /etc/docker/daemon.json
{
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
```
重启 Docker 后所有容器都用这个配置。**强烈建议加上**——我见过太多服务器因为 Docker 日志占满磁盘而崩溃。
## 结构化日志:让检索更高效
非结构化日志只能全文搜索。结构化日志可以按字段过滤:
```python
# Python - 用 structlog 输出 JSON
import structlog
logger = structlog.get_logger()
logger.info("user_login", user_id=123, ip="1.2.3.4")
```
```javascript
// Node.js - 用 pino 输出 JSON
const pino = require('pino')()
pino.info({ userId: 123, action: 'login' }, 'User logged in')
```
输出示例:
```json
{"level":"info","time":1704067200,"userId":123,"action":"login","msg":"User logged in"}
```
在日志聚合平台里可以按 `userId=123` 或 `action=login` 精确过滤,不用全文搜索。
## 日志级别管理
```bash
# 动态调整日志级别(不需要重启容器)
# Spring Boot
curl -X POST http://localhost:8080/actuator/loggers/com.example \
-d '{"configuredLevel": "DEBUG"}'
# Node.js(需要应用支持)
# 通过环境变量控制
LOG_LEVEL=debug node app.js
```
生产环境默认 INFO 级别,排查问题时临时切 DEBUG,不需要重新部署。
## 日志聚合:集中式管理
### 轻量方案:Grafana Loki
Loki 只索引标签不索引正文,存储成本是 ELK 的 1/10:
```yaml
services:
loki:
image: grafana/loki:2.9.0
ports:
- "3100:3100"
promtail:
image: grafana/promtail:2.9.0
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:10.3.0
ports:
- "3000:3000"
```
Promtail 自动从 Docker 容器目录读取日志,推送到 Loki。Grafana 查询:
```logql
{container_name="myapp"} |= "error" | json | level="error"
```
### 重量方案:EFK Stack
需要全文搜索、复杂聚合时用 EFK(Elasticsearch + Fluentd + Kibana):
```yaml
services:
elasticsearch:
image: elasticsearch:8.12.0
environment:
- discovery.type=single-node
- xpack.security.enabled=false
volumes:
- es_data:/usr/share/elasticsearch/data
fluentd:
image: fluent/fluentd:v1.16
volumes:
- ./fluentd/conf:/fluentd/etc
ports:
- "24224:24224"
kibana:
image: kibana:8.12.0
ports:
- "5601:5601"
```
容器配置 Fluentd 日志驱动:
```yaml
services:
app:
logging:
driver: fluentd
options:
fluentd-address: localhost:24224
tag: myapp
```
EFK 最少需要 4GB 内存。团队小于 10 人用 Loki 就够了。
## 日志管理最佳实践
| 检查项 | 建议 |
|--------|------|
| 日志轮转 | 全局配 `max-size: 10m, max-file: 3` |
| 日志级别 | 生产用 INFO,排查切 DEBUG |
| 结构化 | 用 JSON 格式输出 |
| 敏感信息 | 不在日志里打印密码、token |
| 聚合 | 小团队用 Loki,大团队用 EFK |
| 持久化 | 关键日志用 Volume 存储,不依赖容器可写层 |
| 监控 | 对 ERROR 日志设置告警 |
**起步建议**:先配好本地日志轮转(10 分钟的事),再按需加 Loki 聚合。
服务端6月6日 20:29
Docker 容器间怎么通信?同一项目、跨项目和跨主机方案两个容器要互相访问,怎么连通?同一个 compose 项目的容器用服务名直接访问,不同项目的容器需要共享网络,跨主机通信就得用 Overlay 网络。这篇按场景从简单到复杂讲清楚。
## 同一 docker-compose 项目:默认网络
Docker Compose 自动为每个项目创建一个网络,项目内的容器可以互相用服务名访问:
```yaml
# docker-compose.yml
services:
api:
image: myapp-api
ports:
- "3000:3000"
redis:
image: redis:7
postgres:
image: postgres:16
```
```bash
# api 容器内直接用服务名访问
curl http://redis:6379 # 访问 Redis
curl http://postgres:5432 # 访问 PostgreSQL
```
不需要 IP,不需要 `--link`——Docker 内置 DNS 自动把服务名解析为容器 IP。
### 自定义网络名
默认网络名是 `项目名_default`。如果要自定义:
```yaml
services:
api:
networks:
- frontend
- backend
redis:
networks:
- backend
networks:
frontend:
backend:
```
api 同时在 frontend 和 backend 两个网络里——可以访问两边。redis 只在 backend 里——frontend 网络的容器访问不到 redis,实现网络隔离。
## 不同 docker-compose 项目:外部网络
两个独立的 compose 项目需要通信时,共享一个外部网络:
```bash
# 创建共享网络
docker network create shared-net
```
```yaml
# 项目 A: docker-compose.yml
services:
api:
networks:
- shared-net
networks:
shared-net:
external: true
```
```yaml
# 项目 B: docker-compose.yml
services:
worker:
networks:
- shared-net
networks:
shared-net:
external: true
```
项目 A 的 api 和项目 B 的 worker 通过服务名互相访问。
## 容器访问宿主机
容器里需要访问宿主机上的服务(比如宿主机上的 MySQL):
```bash
# 专用 DNS 名
curl http://host.docker.internal:3306
```
`host.docker.internal` 是 Docker Desktop 提供的特殊 DNS,自动解析为宿主机 IP。Linux 上需要手动添加:
```yaml
services:
api:
extra_hosts:
- "host.docker.internal:host-gateway"
```
## 容器间直接用 IP
不推荐但有时需要:
```bash
# 查看容器 IP
docker inspect myapp --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 查看所有容器的 IP
docker network inspect bridge --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{end}}'
```
容器 IP 每次重启可能变化——硬编码 IP 是反模式,应该用服务名或 DNS。
## 端口映射:容器对外暴露服务
```yaml
services:
api:
ports:
- "3000:3000" # 宿主机 3000 → 容器 3000
- "8080:80" # 宿主机 8080 → 容器 80
- "127.0.0.1:3306:3306" # 只允许本机访问
```
`127.0.0.1:3306:3306` 这种写法限制了只监听 loopback 接口——外部无法访问,只有宿主机本身可以连。适合数据库等不需要对外暴露的服务。
### 端口冲突排查
```bash
# 查看宿主机端口占用
lsof -i :3000
# 或
ss -tlnp | grep 3000
# Docker 占用的端口
docker port myapp
```
## 网络模式选择
| 模式 | 说明 | 适用场景 |
|------|------|---------|
| bridge(默认) | 容器有独立 IP,通过 NAT 访问外部 | 大部分场景 |
| host | 容器直接用宿主机网络栈,无隔离 | 需要极致网络性能 |
| none | 无网络 | 离线计算任务 |
| overlay | 跨主机容器通信 | Docker Swarm / 多主机 |
### host 模式
```yaml
services:
api:
network_mode: host
```
host 模式下容器没有独立 IP,直接用宿主机的端口和网络。好处是没有 NAT 性能损耗,坏处是端口冲突风险高(容器和宿主机共享端口空间)。
**不要在生产环境用 host 模式**——失去了网络隔离,一个容器被攻破等于宿主机被攻破。
## 跨主机通信:Overlay 网络
Docker Swarm 多主机环境下,不同主机上的容器需要通信:
```bash
# 创建 Overlay 网络
docker network create -d overlay my-overlay
# 在 Overlay 网络上启动服务
docker service create --network my-overlay --name api myapp
docker service create --network my-overlay --name worker myworker
```
Overlay 网络底层用 VXLAN 隧道——api 和 worker 即使跑在不同的物理机上,也能通过服务名直接通信,和单机体验一致。
## DNS 排查
容器间访问不通时,先排查 DNS:
```bash
# 进入容器测试 DNS 解析
docker exec myapp nslookup redis
docker exec myapp ping redis
# 查看 DNS 配置
docker exec myapp cat /etc/resolv.conf
# 临时指定 DNS
docker run --dns 8.8.8.8 myapp
```
常见问题:自定义了 `docker-compose.yml` 的 `networks` 但忘了在服务里引用,或者两个服务不在同一个网络里。