标签

Docker

Docker 是一个开源的容器化平台,允许开发者打包应用及其全部依赖到一个可移植的容器中,然后这个容器可以在任何机器上运行,确保应用在不同环境之间运行的一致性。Docker 使用了 Linux 容器(LXC)的技术,但进行了扩展,使其更为易用、功能更全面。

Docker
查看更多相关内容
服务端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 数据卷的核心不是命令有多复杂,而是把数据和容器分开管理。容器可以随时删、随时重建;真正要保护的是卷里的数据。
服务端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` 但忘了在服务里引用,或者两个服务不在同一个网络里。