Docker面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月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:最适合持久化业务数据docker volume create mysql-datadocker 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:适合开发和明确指定宿主机路径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:只放临时数据docker run --tmpfs /run:rw,noexec,nosuid,size=64m nginx它适合放运行期临时文件、缓存、敏感中间文件。容器停止后,数据就没了,所以不要把数据库、上传文件放在 tmpfs 里。-v 和 --mount 应该用哪个?-v 更短,适合日常快速使用:docker run -v mysql-data:/var/lib/mysql mysql:8--mount 更清晰,适合脚本、生产环境和团队协作:docker run --mount type=volume,source=mysql-data,target=/var/lib/mysql mysql:8新项目或生产脚本更推荐 --mount,减少误挂载的风险。命名卷和匿名卷有什么区别?命名卷有明确名字,方便复用、查看、备份和删除。docker volume create app-datadocker run -v app-data:/data alpinedocker volume lsdocker volume inspect app-data匿名卷没有手动指定名称,Docker 会生成一串 ID:docker run -v /data alpine这种写法能持久化,但不好识别,也容易越积越多。生产环境建议优先使用命名卷,不要依赖匿名卷保存关键数据。Docker Compose 里怎么声明数据卷?services: db: image: postgres:16 environment: POSTGRES_PASSWORD: example volumes: - pg-data:/var/lib/postgresql/data app: image: my-app:latest depends_on: - dbvolumes: pg-data:这里的 pg-data 是命名卷。执行 docker compose up -d 后,Docker 会自动创建它。本地开发也可以用 bind mount 挂源码:services: app: image: node:22 working_dir: /app volumes: - ./:/app command: npm run dev这类写法适合开发环境,不建议直接照搬到生产环境。生产环境更应该把代码打进镜像,数据用命名卷或外部存储管理。数据卷的生命周期怎么管理?删除容器:docker rm mysql命名卷通常不会自动删除。你需要手动删:docker volume rm mysql-data如果创建容器时使用了匿名卷,删除容器时可以加 -v 一起删除匿名卷:docker rm -v container-name清理所有未被容器使用的卷:docker volume prune执行 prune 前要谨慎确认,尤其是生产服务器。只要某个卷当前没有被容器引用,就可能被清理掉。数据卷怎么备份和恢复?备份命名卷:docker run --rm -v mysql-data:/data:ro -v $(pwd):/backup alpine tar czf /backup/mysql-data.tar.gz -C /data .恢复到新卷:docker volume create mysql-data-newdocker 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 可能和宿主机用户不一致,于是就会出现“容器写的文件宿主机删不了”或“容器没有权限写目录”。docker exec -it app idsudo chown -R 1000:1000 ./datadocker 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 数据卷的核心不是命令有多复杂,而是把数据和容器分开管理。容器可以随时删、随时重建;真正要保护的是卷里的数据。
服务端阅读 06月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:COPY . .RUN npm install只要项目里任意文件变化,COPY . . 这一层就会变,后面的依赖安装也会重新执行。前端或 Node.js 项目更推荐先复制依赖清单,再安装依赖,最后复制源码:COPY package.json package-lock.json ./RUN npm ciCOPY . .这样只改业务代码时,依赖安装层仍能命中缓存。Python、Go、Java 项目也有类似思路:先复制依赖描述文件,再下载依赖,最后复制源码。如何减少镜像体积?选择合适的基础镜像基础镜像决定了镜像体积的起点。能用运行时镜像就不要用完整构建环境,能用官方 slim 版本就不要默认拉 full 版本。FROM node:20-slimalpine 很小,但不是所有场景都适合。它使用 musl libc,部分依赖 glibc 的原生库可能需要额外处理,排查成本反而更高。对 Node.js、Python 原生扩展较多的项目,slim 有时比 alpine 更稳。使用多阶段构建多阶段构建适合把“编译环境”和“运行环境”分开。第一阶段安装编译工具并产出构建结果,第二阶段只复制运行所需文件。FROM node:20-slim AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildFROM nginx:alpineCOPY --from=build /app/dist /usr/share/nginx/html这样最终镜像里不会带上 node_modules、源码、构建工具和临时缓存,只保留真正运行需要的产物。清理必须发生在同一层如果安装包和清理缓存拆成两条 RUN,上一层里的缓存仍然会留在镜像历史中:RUN apt-get update && apt-get install -y curlRUN rm -rf /var/lib/apt/lists/*更好的写法是放在同一个 RUN 里: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。常见配置如下:.gitnode_modulesdistcoverage*.log.env.DS_Store它不只是减少镜像体积,还会影响缓存。构建上下文越干净,COPY . . 越不容易因为无关文件变化而失效。能不能用 squash 压成一层?镜像 squashing 可以把多层压成更少的层,看起来能减少历史包袱。但它不是默认首选。原因有两个:第一,压扁后层复用能力会变差,多个镜像之间不容易共享中间层;第二,它可能掩盖 Dockerfile 本身的问题,比如缓存清理位置不对、构建产物没有隔离。多数情况下,优化 Dockerfile 比依赖 squash 更可靠。怎么检查镜像哪里变大了?先用 docker history 看每一层的大小和对应指令:docker history your-image:tag它能快速定位是哪条 Dockerfile 指令引入了大体积文件。需要更细的分析时,可以用 dive 查看每一层新增、修改、删除了哪些文件:dive your-image:tag如果发现某一层新增了大量缓存,下一层又删除,说明清理时机不对;如果发现源码、测试文件、.git 目录被打进镜像,通常是 .dockerignore 没写好。一个更合理的 Dockerfile 思路以 Node.js 应用为例,比较稳的结构通常是这样:FROM node:20-slim AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciFROM node:20-slim AS buildWORKDIR /appCOPY --from=deps /app/node_modules ./node_modulesCOPY . .RUN npm run buildFROM node:20-slim AS runtimeWORKDIR /appENV NODE_ENV=productionCOPY package.json package-lock.json ./RUN npm ci --omit=dev && npm cache clean --forceCOPY --from=build /app/dist ./distCMD ["node", "dist/index.js"]这个写法的重点不是模板本身,而是顺序:依赖文件先复制,依赖安装尽量缓存;源码后复制,避免频繁改代码导致依赖层失效;最终阶段只保留运行时需要的内容。小结Docker 镜像优化的关键不是单纯减少层数,而是理解每一层留下了什么、哪些层能复用、哪些改动会让缓存失效。实际项目里优先做好几件事:基础镜像选小但别盲目选,依赖文件先复制,清理和安装放同一层,多阶段构建隔离编译产物,用 .dockerignore 控制构建上下文,再用 docker history 或 dive 找出真正的大层。
服务端阅读 06月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 NamespaceUTS 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 控制资源使用,联合文件系统隔离文件变更,安全模块收紧权限。它足够适合大多数应用隔离场景,但不能把它误认为和虚拟机一样强的硬隔离。
服务端阅读 06月20日 11:19

Dockerfile CMD 和 ENTRYPOINT 有什么区别?

Dockerfile 里的 CMD 和 ENTRYPOINT 都和容器启动命令有关,但职责不一样:ENTRYPOINT 更像“固定要运行的程序”,CMD 更像“默认参数”或“默认命令”。如果只记一句话:想让镜像像一个可执行程序一样运行,用 ENTRYPOINT;想给容器提供一个可以轻松替换的默认启动命令,用 CMD。两者一起用时,通常是 ENTRYPOINT 写可执行文件,CMD 写默认参数。CMD 是什么?CMD 用来指定容器启动时的默认命令。它最大的特点是:容易被 docker run 后面的参数覆盖。常见写法有三种:CMD ["node", "server.js"]CMD node server.jsCMD ["--help"]前两种是完整命令,第三种通常配合 ENTRYPOINT 使用,表示给 ENTRYPOINT 传默认参数。比如:FROM node:20-alpineWORKDIR /appCOPY . .CMD ["node", "server.js"]直接运行:docker run my-node-app实际执行的是:node server.js但如果这样运行:docker run my-node-app node worker.jsCMD ["node", "server.js"] 会被 node worker.js 覆盖。所以 CMD 适合放“可以被用户改掉的默认行为”。ENTRYPOINT 是什么?ENTRYPOINT 用来指定容器启动时固定执行的程序。普通的 docker run 参数不会直接覆盖它,而是会追加到 ENTRYPOINT 后面,作为参数传入。FROM alpineENTRYPOINT ["ping"]CMD ["localhost"]运行:docker run ping-image等价于:ping localhost运行:docker run ping-image 8.8.8.8等价于:ping 8.8.8.8这里 ping 是固定程序,localhost 只是默认参数。用户传了 8.8.8.8 后,覆盖的是 CMD,不是 ENTRYPOINT。exec 形式和 shell 形式有什么区别?CMD 和 ENTRYPOINT 都有 exec 形式和 shell 形式。exec 形式写成 JSON 数组:ENTRYPOINT ["node", "server.js"]CMD ["--port", "3000"]shell 形式写成普通字符串:ENTRYPOINT node server.jsCMD node server.js推荐优先用 exec 形式,原因有两个。第一,exec 形式不会额外套一层 shell,参数传递更准确。第二,exec 形式对信号处理更友好。容器里的主进程通常是 PID 1,Docker 停止容器时会先发送 SIGTERM。如果用 shell 形式,真正的业务进程可能变成 shell 的子进程,信号不一定能正确传过去,容易出现容器停止慢、进程残留、优雅退出失效等问题。ENTRYPOINT node server.js实际可能是:/bin/sh -c "node server.js"更推荐:ENTRYPOINT ["node", "server.js"]如果确实需要 shell 能力,比如环境变量展开、管道、&&,可以使用 shell 形式,但要知道它带来的信号处理问题。生产镜像里更稳妥的方式通常是把复杂逻辑放到脚本里,并在脚本最后使用 exec:#!/bin/shset -eexec node server.js再配合:ENTRYPOINT ["./entrypoint.sh"]docker run 怎么覆盖它们?覆盖 CMD 很简单,直接在镜像名后面追加命令或参数。docker run my-image bash如果镜像只有:CMD ["node", "server.js"]那么 bash 会覆盖原来的 CMD。如果镜像是:ENTRYPOINT ["node", "server.js"]CMD ["--port", "3000"]执行:docker run my-image --port 8080最终命令是:node server.js --port 8080如果连 ENTRYPOINT 也想覆盖,需要显式使用 --entrypoint:docker run --entrypoint sh my-image注意,--entrypoint 只替换入口程序,镜像原来的 CMD 仍可能作为参数拼到后面。排查问题时可以看镜像的 Config.Entrypoint 和 Config.Cmd:docker inspect my-image什么时候只用 CMD?如果镜像只是给一个默认启动命令,用户经常需要替换整个命令,用 CMD 就够了。FROM node:20-alpineWORKDIR /appCOPY . .CMD ["npm", "run", "dev"]默认启动开发服务,但用户也可以方便地执行:docker run my-app npm test这种场景下,CMD 的可覆盖特性反而是优点。什么时候用 ENTRYPOINT + CMD?如果希望镜像表现得像一个固定工具,推荐 ENTRYPOINT + CMD。FROM alpineRUN apk add --no-cache curlENTRYPOINT ["curl"]CMD ["--help"]默认运行会输出 curl 帮助;传入参数时:docker run curl-image -I https://example.com实际执行:curl -I https://example.com服务型镜像也可以这样写:ENTRYPOINT ["node", "server.js"]CMD ["--port", "3000"]这样镜像默认监听 3000,用户需要改端口时只覆盖参数即可。docker compose 里的 command 和 entrypoint 对应什么?在 Docker Compose 里,command 对应 Dockerfile 里的 CMD,entrypoint 对应 Dockerfile 里的 ENTRYPOINT。services: app: image: my-app command: ["--port", "8080"]如果镜像里有:ENTRYPOINT ["node", "server.js"]CMD ["--port", "3000"]Compose 的 command 会把默认参数改成:node server.js --port 8080如果同时覆盖入口和参数: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。
服务端阅读 06月20日 00:02

Docker COPY 和 ADD 有什么区别?什么时候该用 ADD?

Dockerfile 里复制文件时,默认优先用 COPY。它的语义很单纯:把构建上下文里的文件或目录复制到镜像指定路径。ADD 也能复制文件,但它多了几个“隐式动作”,尤其是本地 tar 包自动解压和远程 URL 下载。正因为这些行为不够直观,日常构建里更推荐 COPY,只有明确需要 ADD 的特殊能力时再用它。COPY 做什么?COPY 只负责复制本地文件、目录,行为可预测:COPY package.json package-lock.json ./COPY src/ /app/src/注意目标路径的斜杠:COPY file.txt /appCOPY file.txt /app/如果 /app 不存在,第一种可能把文件复制成名为 /app 的文件;第二种明确表示复制到 /app/ 目录下。写 Dockerfile 时建议目录目标都带上 /,减少歧义。COPY 也支持通配符,例如:COPY *.json ./但通配符匹配的是构建上下文里的文件,不是容器里的路径。构建上下文会受 .dockerignore 影响,所以别把 node_modules、日志、临时文件、密钥文件一起送进镜像构建,否则不仅镜像变大,还可能泄露敏感信息。ADD 多了哪些能力?ADD 可以做 COPY 能做的事,还多了两类常见行为。第一,本地 tar 包会自动解压:ADD app.tar.gz /app/如果 app.tar.gz 是本地构建上下文里的 tar 归档,Docker 会把它解压到 /app/。这个功能适合你明确想把本地归档展开进镜像的场景。第二,ADD 可以从远程 URL 下载文件:ADD https://example.com/tool.tar.gz /tmp/tool.tar.gz但这通常不推荐。远程下载会让构建结果依赖网络、服务端响应和缓存规则,可复现性变差。使用 BuildKit 时,HTTP(S) URL 可以配合 --checksum 校验内容:ADD --checksum=sha256:<hash> https://example.com/tool.tar.gz /tmp/tool.tar.gz有校验比裸下载安全,但大多数场景仍然更适合用 RUN curl 或 wget,因为你可以在同一层里校验、解压、删除缓存文件。为什么下载文件更推荐 RUN curl 或 wget?比如安装一个二进制包,更推荐这样写: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 层通常都会失效。所以常见优化是先复制依赖描述文件,再安装依赖,最后复制业务代码:COPY package.json package-lock.json ./RUN npm ciCOPY src/ ./src/这样业务代码变了,不会轻易让依赖安装层重新执行。.dockerignore 也会影响缓存。忽略无关文件能减少构建上下文变化,避免因为日志、缓存目录、编辑器临时文件导致 Docker 反复失效。--chown、--chmod 和多阶段构建COPY 和 ADD 都可以配合权限参数使用:COPY --chown=node:node --chmod=755 app.sh /usr/local/bin/app.sh这比复制后再 RUN chown、RUN chmod 更干净,少一层,也更容易读懂。多阶段构建里还常用 COPY --from 从前一个阶段复制产物:FROM node:20 AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildFROM nginx:alpineCOPY --from=build /app/dist/ /usr/share/nginx/html/这也是 COPY 的高频用法:只把最终产物放进运行镜像,构建工具、源码缓存、依赖中间文件都不带进去。安全上怎么选?规则可以很简单:只是复制文件:用 COPY需要本地 tar 自动解压:可以用 ADD需要下载远程文件:优先 RUN curl 或 wget,并做 checksum 校验不要把密钥、.env、SSH 私钥放进构建上下文用 .dockerignore 控制复制范围目录目标路径尽量写成 /path/,避免歧义ADD 不是不能用,而是它会“顺手多做事”。Dockerfile 越显式,镜像构建越稳定,也越容易排查问题。
服务端阅读 06月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 状态,适合先准备配置、网络、挂载参数,再手动启动。docker create --name app nginxdocker run 更常用,它等价于“创建 + 启动”。如果加上 -d,容器会在后台运行。docker run -d --name app nginx简单说:create 是先把容器准备好,run 是准备好以后立刻跑起来。启动、停止和强制终止怎么用?启动已创建或已停止的容器,用 docker start:docker start app停止容器优先用 docker stop。它会先向容器内的 PID 1 进程发送 SIGTERM,给程序一个优雅退出的机会;如果超时仍未退出,再发送 SIGKILL 强制结束。docker stop appdocker stop -t 30 appdocker kill 则是直接发送强制终止信号,默认是 SIGKILL:docker kill app生产环境通常先用 stop,只有容器卡死、无法正常退出时才用 kill。尤其要注意,容器里的 PID 1 如果没有正确处理 SIGTERM,应用可能来不及关闭连接或刷盘,导致数据状态不一致。pause、unpause 和 restart 分别做什么?docker pause 会暂停容器中的所有进程,容器仍存在,但进程不会继续执行:docker pause appdocker unpause app它适合短时间冻结容器,比如临时排查资源占用。但它不是正常停机方式,不能替代 stop。docker restart 相当于先停止再启动:docker restart app如果应用需要平滑重启,要确认它能正确处理 SIGTERM,否则 restart 也可能变成一次粗暴中断。如何查看容器运行情况?生命周期管理不只是执行命令,还要能看懂容器发生了什么。docker logs appdocker inspect appdocker stats appdocker eventsdocker 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 重启后不会再自动拉起 |示例:docker run -d --restart unless-stopped --name app nginx服务型容器更常用 unless-stopped。它既能在异常退出后自动恢复,又不会和人工停机操作打架。删除容器前要注意什么?删除容器用 docker rm,但容器必须先停止:docker stop appdocker rm app如果确认不需要保留容器,也可以强制删除:docker rm -f app不过要分清“容器数据”和“卷数据”。删除容器不会自动删除 Docker volume 中的数据,挂载到 volume 或宿主机目录里的文件通常还在。这是好事,也是坑:好处是数据不会因为容器重建就丢;坑是不用的 volume 会越积越多,需要定期清理。常见清理命令包括:docker container prunedocker volume prunedocker system prune清理前一定确认资源不再使用,尤其是 volume,误删后数据库、上传文件这类持久化数据可能无法恢复。小结Docker 容器生命周期可以理解为:先创建,再启动运行;运行中可以暂停、恢复、停止或重启;退出后根据退出码和重启策略决定是否自动拉起;不再需要时再删除容器并清理无用资源。日常管理时,stop 比 kill 更安全,inspect 和 logs 是排查退出问题的入口,volume 数据是否保留则要单独确认。
服务端阅读 06月20日 00:02

Docker 环境变量怎么配置?ENV、ARG 和 Compose 怎么选?

Docker 环境变量到底能配置在哪?Docker 容器环境变量常见配置方式有四类:写在 Dockerfile 里、运行容器时传入、通过环境变量文件批量传入、在 Docker Compose 中配置。它们看起来都叫“环境变量”,但生效时机、覆盖规则和安全风险并不一样。一句话先说结论:固定默认值可以放 Dockerfile,环境差异参数用运行时传入,生产敏感信息不要直接当环境变量裸奔。Dockerfile 里的 ENV 和 ARG 有什么区别?Dockerfile 里最容易混淆的是 ENV 和 ARG。ENV:构建后仍然存在ENV 用来设置镜像默认环境变量,构建出的镜像和启动后的容器里都能看到。FROM node:20-alpineENV NODE_ENV=productionENV APP_PORT=3000CMD ["node", "server.js"]适合放默认运行参数,比如 NODE_ENV、默认端口、默认语言环境。它不适合放密码、Token、数据库连接串,因为这些值可能进入镜像层历史,也可能被 docker inspect 看到。ARG:只在构建阶段使用ARG 只在镜像构建时生效,默认不会保留到运行时环境里。FROM node:20-alpineARG BUILD_VERSIONRUN echo "build version: $BUILD_VERSION"构建时传入:docker build --build-arg BUILD_VERSION=2026.06.19 -t my-app .如果需要把 ARG 写入运行时环境,必须显式转成 ENV。这也意味着它会进入最终镜像环境。不要把“ARG 构建时用”误解成“ARG 一定安全”,构建日志、镜像历史和 CI 记录里仍可能留下痕迹。docker run 怎么传环境变量?运行容器时最直接的方式是 -e 或 --env。docker run -e NODE_ENV=production -e APP_PORT=3000 my-app也可以只写变量名,让 Docker 从当前 Shell 环境里取值:export API_BASE_URL=https://api.example.comdocker run --env API_BASE_URL my-app如果变量比较多,用 --env-file 更清爽。docker run --env-file .env.production my-app.env.production 示例:NODE_ENV=productionAPP_PORT=3000API_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 里的占位变量。IMAGE_TAG=1.2.0HOST_PORT=8080services: web: image: my-app:${IMAGE_TAG} ports: - "${HOST_PORT}:3000"这里 .env 的作用是让 Compose 在解析配置文件时,把 ${IMAGE_TAG}、${HOST_PORT} 替换掉。它不等于自动把所有变量都塞进容器。env_file:把文件里的变量传给容器services: web: image: my-app:1.2.0 env_file: - .env.production这会把 .env.production 里的变量注入容器运行环境。environment:直接在 compose 里声明容器变量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 来说,通常可以按这个理解:docker run -e KEY=value 显式传入的值优先级最高;docker run --env-file 中的值次之;镜像里 Dockerfile ENV 的默认值最后兜底。Compose 的优先级更细一些:environment 里直接声明的值通常会覆盖 env_file 同名变量;env_file 会覆盖镜像 ENV 默认值;.env 主要影响 ${VAR} 插值,本身不自动等于容器环境变量;命令行临时覆盖通常最高。如果变量来源很多,不要靠猜。可以用下面命令看 Compose 最终解析结果:docker compose config再进容器确认实际值:docker compose exec web env | grep NODE_ENV敏感信息能不能放环境变量?能用,但不推荐把它当成安全存储。环境变量很方便,也很容易泄漏:docker inspect 可能看到容器环境变量,应用启动日志可能把配置整体打印出来,CI/CD 日志中也可能出现明文。所以数据库密码、私钥、访问 Token 这类内容,生产环境更建议使用 Docker Swarm Secrets、Kubernetes Secrets、云厂商密钥管理服务,或者运行平台提供的 Secret 注入能力。Secrets 不是环境变量,那应用怎么读?很多运行平台会把 Secret 挂载成文件,而不是直接放进环境变量。Docker Swarm Secrets 常见路径类似:/run/secrets/db_password不少官方镜像支持 _FILE 约定,例如:MYSQL_PASSWORD_FILE=/run/secrets/mysql_passwordPOSTGRES_PASSWORD_FILE=/run/secrets/postgres_password这个约定不是 Docker 强制标准,而是很多镜像和框架采用的习惯。自己写应用时也可以照这个思路做:普通配置走环境变量,敏感值走文件或密钥服务。一个更接近生产的配置方式开发环境可以简单一点:services: web: build: . ports: - "3000:3000" env_file: - .env.development environment: NODE_ENV: development生产环境建议把默认值、环境差异和密钥分开: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_passwordsecrets: db_password: external: true应用启动时读取: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 渲染后的配置:docker compose config查看容器环境变量:docker exec <container> env | sort查看镜像默认环境变量:docker image inspect my-app --format '{{json .Config.Env}}'查看容器环境变量时要小心,不要在共享终端、CI 日志或工单截图里暴露敏感值。很多泄漏不是黑客攻破系统,而是排查问题时顺手贴了一段日志。生产环境建议怎么定规则?比较稳的做法是把变量按用途分层:镜像默认值放无敏感、跨环境都合理的默认配置;部署环境变量放不同环境会变化的普通配置;Secret 放密码、Token、私钥、证书;CI/CD 参数放构建版本、提交哈希、构建时间这类构建期信息。还要给变量命名留点规矩。比如统一使用 APP_、DB_、REDIS_ 前缀;布尔值固定用 true/false;端口统一写字符串,避免 YAML 把值解析成奇怪的类型。最后,给应用启动加一层配置校验。缺了关键变量就直接失败,不要带着默认空值跑起来。Docker 环境变量配置没有唯一答案,关键是边界清楚:默认配置归默认配置,环境差异归部署系统,敏感信息归 Secrets。
服务端阅读 06月19日 23:48

Docker 端口映射怎么配置?-p、Compose 和排查怎么做?

Docker 端口映射的作用,是把容器网络命名空间里的端口发布到宿主机上。最常见的写法是 docker run -p 8080:80 nginx:外部访问宿主机的 8080 端口,请求会被转发到容器里的 80 端口。这里最容易混淆的是:EXPOSE 只是镜像或 Dockerfile 里的声明,告诉别人“这个容器通常会监听哪些端口”;真正让宿主机能访问容器端口的,是 -p/--publish 或 Compose 里的 ports。-p 的完整写法是什么?-p 的通用格式是:docker run -p [hostIP:]hostPort:containerPort[/protocol] image几个常见例子:# 宿主机 8080 转发到容器 80docker run -p 8080:80 nginx# 只允许本机访问宿主机 8080docker run -p 127.0.0.1:8080:80 nginx# 绑定所有网卡的 8080docker 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 只绑定本机回环地址,通常只有宿主机自己能访问,适合数据库、管理后台、本地调试服务这类不该直接暴露的端口。所以生产环境里不要随手写:docker run -p 0.0.0.0:3306:3306 mysql这等于把数据库端口暴露在宿主机所有网卡上。更稳妥的方式是只绑定本地地址,再通过反向代理、堡垒机、VPN 或内网访问控制处理入口。为什么映射了端口还是访问不了?端口映射只负责把流量送到容器端口,但容器里的应用也要真的监听在正确地址上。如果应用在容器内只监听 127.0.0.1,外部流量通常到不了它。因为 Docker 转发过来的目标地址是容器的网络地址,不是容器里的 loopback。容器里的 Web 服务应监听:0.0.0.0:80而不是:127.0.0.1:80很多 Node、Python、Go 开发服务默认只监听 localhost,本地直接跑没问题,放进容器再做端口映射就会踩坑。EXPOSE、-P 和 -p 有什么区别?EXPOSE 不会自动发布端口,例如:EXPOSE 80它只是元数据。你运行容器时仍然需要:docker run -p 8080:80 image-P 是大写 P,会把镜像里 EXPOSE 声明的端口随机映射到宿主机端口:docker run -P nginx这种方式适合临时调试,不适合稳定对外服务,因为宿主机端口不固定。要知道实际映射到了哪个端口,可以查:docker port 容器名Docker Compose 里怎么写?Compose 里最常见的是 ports:services: web: image: nginx ports: - "8080:80" - "127.0.0.1:8443:443"ports 会把容器端口发布到宿主机。expose 不会发布到宿主机,只是让同一个 Docker 网络里的其他服务知道这个服务使用了哪些端口:services: api: image: my-api expose: - "3000"在同一个 Compose 网络里,服务之间通常不需要端口映射。比如 web 访问 api:3000 即可,Docker 内部 DNS 会把服务名解析到对应容器。只有当宿主机或外部用户需要访问容器时,才需要 ports。常用排查命令有哪些?先看 Docker 是否真的发布了端口:docker port 容器名再看容器是否在运行:docker ps检查宿主机端口是否监听:ss -lntp | grep 8080如果是 macOS 没有 ss,可以用:lsof -i :8080从宿主机访问测试:curl -v http://127.0.0.1:8080进容器里看应用是否监听正确地址:docker exec -it 容器名 shss -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、宿主机端口被占用、防火墙或安全组没放行。按这个顺序查,通常很快能定位。
服务端阅读 06月19日 23:48

Docker 容器时区配置怎么做才稳妥?

Docker 容器里的时间不对,最先影响的一般不是页面展示,而是日志、定时任务和排查问题的效率。比如宿主机已经是北京时间,容器日志却显示 UTC,凌晨任务提前 8 小时执行,排查起来很容易绕晕。Docker 容器时区配置常见有几种做法:设置 TZ 环境变量、安装 tzdata、挂载宿主机时区文件,或者在 Compose、Kubernetes 中统一声明。实际选哪一种,要看镜像基础系统、应用运行时和部署环境。用 TZ 环境变量设置时区最轻量的方式是在容器中设置 TZ 环境变量:ENV TZ=Asia/Shanghai运行容器时也可以临时传入:docker run -e TZ=Asia/Shanghai your-image这种方式优点是简单、可移植,不依赖宿主机的 /etc/localtime。如果同一个镜像要部署到不同地区,只需要改环境变量,不用重新改镜像逻辑。不过要注意,TZ 是否生效取决于镜像里是否有可用的时区数据。很多精简镜像没有完整的 timezone 数据库,只设置环境变量可能不够。Debian 和 Alpine 镜像要安装 tzdata如果基础镜像是 Debian 或 Ubuntu,可以在 Dockerfile 里安装 tzdata: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:RUN apk add --no-cache tzdataENV TZ=Asia/ShanghaiAlpine 镜像更精简,很多时候没有预装时区数据。只写 ENV TZ=Asia/Shanghai,但没有 tzdata,程序可能仍然按 UTC 或默认时区处理。挂载宿主机时区文件可以用,但别过度依赖另一种常见写法是挂载宿主机的时区配置: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 中通常直接配置环境变量:services: app: image: your-image environment: TZ: Asia/Shanghai如果你的镜像需要系统级时区文件,也可以挂载:services: app: image: your-image environment: TZ: Asia/Shanghai volumes: - /etc/localtime:/etc/localtime:ro更稳妥的做法是:镜像里安装好 tzdata,部署层只负责传 TZ。这样 Compose、Kubernetes、普通 docker run 都能复用同一套镜像。Kubernetes 中怎么配置Kubernetes 里一般通过环境变量设置: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 小时。可以先在容器里确认时间:docker exec -it container-name date也可以看时区文件:docker exec -it container-name sh -c 'date && ls -l /etc/localtime'如果安装了 tzdata,还可以检查:zdump Asia/Shanghai | head应用运行时也可能有自己的时区规则容器系统时区正确,不代表应用一定正确。Java、Node.js、Python 都可能有自己的处理方式。JavaJava 常见做法是设置 JVM 参数:-Duser.timezone=Asia/ShanghaiSpring Boot 项目还可能涉及 Jackson 序列化时区、数据库连接时区等配置。如果接口返回时间偏移,不能只看容器的 date。Node.jsNode.js 会受 TZ 环境变量影响,但不同运行环境和镜像差异较大。建议在启动前设置:TZ=Asia/Shanghai node server.js如果项目使用 dayjs、moment、luxon 这类库,还要确认是否启用了对应的 timezone 插件或配置。PythonPython 的 datetime.now()、time.localtime() 会受系统时区影响,但推荐业务代码使用明确的 timezone-aware datetime,避免混用 naive datetime。例如在 Python 3.9+ 中可以使用:from zoneinfo import ZoneInfofrom datetime import datetimenow = datetime.now(ZoneInfo("Asia/Shanghai"))这样代码表达更清楚,也不完全依赖容器系统配置。NTP 负责校准时间,不负责选择时区NTP 解决的是“时间准不准”,不是“显示哪个时区”。容器通常不需要单独跑 NTP 客户端,因为容器共享宿主机内核时间,宿主机时间同步正常,容器拿到的时间基准也正常。如果容器时间整体漂移,应该优先检查宿主机或节点的 NTP、chrony、systemd-timesyncd 配置。容器里单独跑 NTP 反而会增加权限和运维复杂度。推荐做法如果只是普通业务容器,推荐这样处理:镜像中安装 tzdata;使用 ENV TZ=Asia/Shanghai 或部署文件传入 TZ;不默认依赖宿主机 /etc/localtime;Java、Node.js、Python 等运行时单独确认时区行为;用 docker exec date、应用日志、cron 执行时间一起验证。容器时区配置看起来只是一个小参数,但它会影响日志排查、定时任务、数据审计和用户看到的时间。最怕的不是用 UTC 或北京时间,而是系统里同时混着几套时间规则。
服务端阅读 06月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、清理未使用镜像和删除停止容器确实能释放空间,但生产环境不能随手执行。更安全的做法是:docker system dfdocker 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 容器成本优化可以按这个顺序推进:先减小镜像和仓库存储,再校准资源申请,然后优化扩缩容和节点装箱率,最后处理日志、存储、网络出口和成本归因。这样改动风险比较低,也更容易看到真实账单变化。
服务端阅读 06月19日 19:34

Docker 容器灾难恢复计划要备份和演练什么?

Docker 容器灾难恢复计划,不能只写一句“定期备份镜像和数据卷”。真正出问题时,决定恢复速度的往往不是镜像在不在,而是配置能不能还原、数据是不是一致、依赖服务有没有顺序、账号密钥是否还能用,以及团队是否知道第一步该做什么。一个可执行的 Docker 灾备方案,至少要回答三个问题:丢了什么能恢复、多久能恢复、最多能接受丢多少数据。先定清楚 RTO 和 RPO灾难恢复计划先别急着写命令,先定两个指标:RTO(恢复时间目标):服务中断后,最多允许多久恢复。例如官网 30 分钟、内部报表 4 小时。RPO(恢复点目标):最多允许丢多少数据。例如订单库最多丢 5 分钟数据,日志系统可以丢 1 小时。这两个值会直接影响备份频率、存储成本和架构复杂度。RPO 要求越小,越不能只靠每天一次的文件备份,通常需要数据库主从、增量备份、对象存储版本控制或跨区域复制。RTO 要求越短,就越依赖自动化脚本、预热环境和清晰的恢复 runbook。Docker 灾备到底要备份什么很多事故恢复失败,是因为只备份了镜像,却漏掉了运行时配置和数据。Docker 环境至少要覆盖下面几类资产。镜像和镜像仓库镜像可以用 docker save 导出:docker save -o app-web.tar registry.example.com/app/web:2026-06-01docker 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 文件,可以用下面的命令把现有容器配置先导出来,作为整理依据:docker inspect app-web > app-web.inspect.jsondocker inspect 更像事故调查记录,真正可维护的恢复配置应该沉淀为 Compose、Helm Chart、Kustomize 或 Terraform 等可重复执行的文件。数据卷、挂载目录和上传文件容器本身应该尽量无状态,真正要命的是 volume、数据库和用户上传文件。Docker volume 可以用临时容器打包:docker run --rm -v app_data:/data -v /backup:/backup alpine tar czf /backup/app_data_$(date +%F).tar.gz -C /data .恢复时反向解包: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,按步骤执行:确认事故范围:单容器异常、宿主机损坏、机房故障,还是镜像仓库不可用。冻结现场信息:保留日志、容器 inspect、宿主机磁盘和网络状态。准备目标环境:确认 Docker 版本、内核参数、磁盘挂载、网络、防火墙、时区和系统依赖。恢复镜像:优先从镜像仓库拉取固定 tag 或 digest,必要时使用 docker load。恢复配置:应用 compose、Kubernetes manifests、环境变量、Secret、证书和网关配置。恢复数据:先恢复数据库,再恢复 volume、上传文件、对象存储索引等业务数据。按依赖顺序启动服务:数据库、缓存、队列、后端服务、前端网关、定时任务依次恢复。验证功能:检查健康接口、登录、下单、上传、异步任务、回调等关键路径。切换流量:通过 DNS、负载均衡、网关或 Kubernetes Service 将流量切到恢复环境。复盘记录:记录实际 RTO、实际 RPO、失败步骤和需要补齐的自动化脚本。容器状态是 running 不代表业务已经恢复。真正的验证应该来自业务探针,例如能否创建订单、能否写入数据库、队列是否消费、文件是否能访问。高可用设计不能等灾难后再补备份解决的是“坏了以后能不能回来”,高可用解决的是“坏的时候能不能少中断”。Docker 单机部署至少要配置重启策略、健康检查、磁盘和 Docker daemon 监控,并把日志采集到集中系统。如果业务要求更高,应该考虑多节点和跨区域:使用 Kubernetes、Docker Swarm 或云容器服务做多副本调度;数据库采用主从复制、跨可用区部署或托管数据库;镜像仓库做跨区域复制;对象存储开启版本控制和跨区域复制;流量入口支持 DNS Failover 或负载均衡故障转移。跨区域灾备要特别小心数据一致性。应用服务跨区域比较容易,数据库跨区域才是难点。同步复制延迟低但成本高,异步复制便宜但可能丢数据,这要回到 RPO 来取舍。备份是否可用,必须靠演练证明没有恢复演练的备份,只能算心理安慰。建议至少做三类测试:备份完整性检查、局部恢复演练、全链路灾备演练。演练时要记录实际 RTO、实际 RPO、失败步骤、人工操作和业务验证结果。如果每次演练都要靠某个老员工“凭经验操作”,这本身就是风险。灾备计划应该让新同事也能按文档恢复到可用状态。监控和告警也属于灾备的一部分灾备不是从服务挂掉才开始。建议监控容器重启次数、退出码、健康检查状态、宿主机 CPU/内存/磁盘/inode、Docker daemon 状态、镜像仓库拉取失败率、数据库复制延迟、备份任务成功率、队列积压和关键接口成功率。备份任务也要有告警。最糟糕的情况不是备份失败,而是备份失败了三个月没人知道。常见坑只备份容器,不备份数据卷:容器能启动,但业务数据没了。只备份数据,不备份配置:数据在,但服务跑不起来。镜像使用 latest 标签:恢复时拉到的可能不是事故前版本。数据库热备方式不对:直接压缩运行中的数据目录,恢复后数据损坏。Secret 没有备份或无法解密:服务启动后连不上数据库和第三方接口。没有依赖顺序:应用先启动,数据库和队列没恢复,导致大量报错。没有恢复验证:容器显示 running,但核心业务路径不可用。备份和生产在同一台机器:宿主机磁盘坏了,备份也一起没了。一份可落地的 Docker 灾备清单RTO、RPO 已按业务等级定义;镜像使用固定 tag 或 digest,并有可用镜像仓库;Compose、Kubernetes manifests、环境变量模板进入版本管理;Secret、证书、Token 有加密备份和恢复权限说明;volume、上传文件、对象存储有独立备份策略;数据库使用官方工具或可靠快照备份;依赖关系图清楚标出数据库、队列、缓存、外部接口;恢复 runbook 写明执行顺序、命令、负责人和验证方式;备份任务、复制延迟、容器健康状态都有监控告警;至少定期做一次局部恢复演练,核心系统做全链路演练。Docker 容器灾难恢复计划的重点,不是把所有命令都背下来,而是把“能不能恢复”变成一件可验证、可重复、可交接的事。镜像、配置、数据、密钥、依赖和演练缺一不可。
服务端阅读 06月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 命令大概是这样:trivy image --severity HIGH,CRITICAL --exit-code 1 your-image:tag但生产里不要只靠一条命令。更合理的是把策略写进 CI 模板:哪些严重级别失败、哪些目录跳过、哪些漏洞暂时忽略、忽略到什么时候失效。这样规则才不会散落在每个仓库里。如何处理误报和“看起来没法修”的漏洞?容器扫描会遇到误报,尤其是发行版 backport 补丁。比如 Debian 或 Ubuntu 可能已经把修复补丁回合到旧版本包里,但版本号没有升到上游公告里的新版本,工具如果只看版本号,就可能误判。处理方式不是简单把漏洞加入 ignore,而是先确认三件事:漏洞对应的包是否真的存在于最终镜像层里;漏洞代码路径是否会被应用调用;发行版安全公告是否已经说明该版本已修复。如果确实是误报,可以加入忽略文件,但要写清原因和过期时间。没有过期时间的忽略规则,最后往往会变成“永久免死金牌”。还有一种情况是漏洞暂时没有修复版本。这时可以做风险缓解:移除不需要的包,关闭相关功能,限制容器权限,缩小网络访问范围,或者换一个维护更及时的基础镜像。基础镜像更新和重建为什么重要?很多团队以为应用代码没变,就不用重新构建镜像。实际不是这样。基础镜像里的 openssl、glibc、ca-certificates 等包会持续更新,旧镜像即使昨天还是安全的,今天也可能因为新披露的 CVE 变成高危。建议把基础镜像治理纳入日常流程:优先使用官方、可信、仍在维护的基础镜像;尽量固定 digest,避免同一个 tag 在不同时间指向不同内容;定期刷新基础镜像并重新构建业务镜像;使用多阶段构建,最终镜像只保留运行所需文件;能用 slim、distroless、alpine 时再用,不要为了小而牺牲兼容性和可维护性;删除构建工具、包管理缓存和临时文件,减少攻击面。修复漏洞不一定是“在容器里 apt upgrade 一下”。更推荐修改 Dockerfile 或基础镜像版本,然后重新构建、扫描、签名和发布,保证过程可追踪。签名、准入控制和运行时扫描怎么配合?镜像扫描解决的是“镜像里有什么风险”,签名解决的是“这个镜像是不是可信流程产物”,准入控制解决的是“能不能进入集群”。三者最好连起来。常见链路是:CI 构建镜像;生成 SBOM;执行漏洞、Secret、IaC、License 扫描;达到策略阈值后用 cosign 签名;推送到镜像仓库;Kubernetes 准入控制检查签名、镜像来源、扫描结果和基础安全配置;不满足条件的镜像禁止部署。准入控制可以用 Kyverno、OPA Gatekeeper、Connaisseur 或云厂商自带策略能力。规则不必一开始写得很重,可以先从“禁止 latest 标签”“必须来自可信 Registry”“必须非 root 运行”“必须有签名”这些低争议规则开始。运行时扫描也有边界。镜像扫描看不到容器启动后的异常行为,例如进程被注入、容器逃逸尝试、异常网络连接、运行时挂载了危险目录。这部分要靠 Falco、eBPF Runtime Security、Kubernetes Audit、云安全产品或主机入侵检测来补齐。所以,容器安全不是某一个工具的事。镜像扫描负责提前发现已知风险,CI 策略负责把风险挡在发布前,签名和准入控制负责防止不可信镜像进入环境,运行时监控负责发现上线后的异常行为。把这几步串起来,才算真正把 Docker 容器安全扫描落到了工程流程里。
服务端阅读 06月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 来做适配。实际项目里怎么分工?常见流程是:开发者写 Dockerfile;用 Docker CLI 或 CI 中的 BuildKit 构建镜像;把镜像推到镜像仓库;Kubernetes 从仓库拉取镜像;节点上的 containerd 运行容器;Kubernetes 负责调度、扩容、重启和对外暴露服务。本地开发继续用 Docker 很正常,因为它简单、反馈快。生产环境通常把 Docker 的“构建镜像”能力留在开发机或 CI,把“运行和编排”交给 Kubernetes + containerd。这样分工更清楚,也更符合现在 Kubernetes 集群的主流做法。
服务端阅读 06月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 不会。部署到生产时最好写成: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 可以把镜像改回上一版后执行:docker compose pulldocker compose up -dKubernetes 更推荐保留 Deployment 修订记录:kubectl rollout undo deployment/apikubectl rollout status deployment/api前提是上一版镜像还在 registry 里。所以 registry retention 不能只按“保留最近 N 天”粗暴清理,至少要保留当前线上版本、上一稳定版、最近若干个发布版,以及审计要求范围内的镜像。安全和治理要补哪些?版本管理不只是回滚,还包括供应链安全。建议为镜像生成 SBOM,用 cosign 或类似工具签名,并在部署阶段校验签名。旧镜像要定期清理,但不能删掉仍被生产、回滚或审计依赖的版本。一句话,生产环境的 Docker 版本管理要做到:标签可读、digest 可验、构建可追踪、晋级不重构、回滚有旧包、清理有规则。latest 很省事,但它省掉的,往往是事故发生后最需要的证据。
服务端阅读 06月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 项目可以这样写一个多阶段测试镜像:FROM node:20-alpine AS depsWORKDIR /appCOPY package*.json ./RUN npm ciFROM deps AS testCOPY . .ENV NODE_ENV=testCMD npm test -- --runInBandFROM deps AS buildCOPY . .RUN npm run build多阶段构建的好处是,依赖安装、测试、构建可以复用同一套基础层。CI 里缓存 Docker layer 后,速度通常比每次从零安装依赖更稳。对 Java、Go、Python 项目也一样:测试阶段保留测试工具和调试符号,生产阶段只复制构建产物,避免把测试依赖带进运行镜像。单元测试容器里最好显式传入环境变量,例如: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,并让测试服务等待依赖健康后再跑。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 更灵活。它可以在测试代码里按需启动容器,拿到动态端口,再把连接信息注入测试。适合需要并行跑很多测试文件,或每个测试套件都要独立数据库的场景。import { PostgreSqlContainer } from "@testcontainers/postgresql";const db = await new PostgreSqlContainer("postgres:16-alpine").start();process.env.DATABASE_URL = db.getConnectionUri();// run integration testsawait db.stop();Compose 更像“固定一套测试环境”,Testcontainers 更像“测试自己声明需要什么依赖”。团队规模小、依赖固定时用 Compose 就够;测试隔离要求高、并行任务多时,Testcontainers 会更省心。E2E 和浏览器测试:别忘了浏览器也是依赖端到端测试经常因为浏览器版本、字体、系统库不同而误报。把 Playwright 或 Selenium 放进容器,可以把浏览器、驱动和系统依赖一起锁住。Playwright 官方镜像已经带好浏览器: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=htmlSelenium 也类似,可以用 selenium/standalone-chrome 或 Selenium Grid。重点是测试代码访问服务时不要写 localhost:3000。在 Compose 网络里,localhost 指的是当前容器自己,应该用服务名,比如 http://app:3000。E2E 报告建议至少保留三样东西:失败截图、视频或 trace、浏览器控制台日志。很多 UI 问题只看断言信息看不出来,trace 文件往往能省半小时。性能测试:容器化工具不等于随便压测JMeter、Locust、k6 都可以容器化运行,但性能测试要特别注意网络和资源隔离。压测容器和被测服务如果跑在同一台 CI 机器上,CPU 抢占会让结果变形;如果目标是外部测试环境,又要避免多个流水线同时压测同一套服务。Locust 的例子: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.htmlJMeter 也可以把 .jmx 脚本和结果目录挂进去,输出 JTL、HTML 报告。性能测试不要只看平均响应时间,至少要记录 p95、p99、错误率和吞吐量。CI 里可以设置门槛,例如错误率超过 1% 或 p95 超过 800ms 就失败。CI/CD 里怎么串起来一条实用的流水线通常是这样的:stages: - unit - integration - e2e - performanceunit: script: - docker build --target test -t myapp:test . - docker run --rm -v $PWD/reports:/app/reports myapp:testintegration: 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 后,收益通常很直接:环境一致:本地、CI、预发用同一份镜像,减少“只在某台机器失败”。依赖可复制:数据库、缓存、浏览器、压测工具都能随测试启动。清理成本低:容器、网络、临时卷可以在任务结束后统一销毁。并行更容易:每个任务一套隔离网络和临时数据库,互不抢状态。报告可沉淀:覆盖率、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 里长期稳定运行。
服务端阅读 06月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 节点# 在第一台服务器上初始化docker swarm init --advertise-addr 192.168.1.10# 输出加入命令# docker swarm join --token SWMTKN-xxx 192.168.1.10:2377Worker 节点# 在其他服务器上执行加入命令docker swarm join --token SWMTKN-xxx 192.168.1.10:2377查看集群状态# 查看所有节点docker node ls# 输出# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS# abc123 * node1 Ready Active Leader# def456 node2 Ready Active# ghi789 node3 Ready ActiveManager 节点负责调度和管理,Worker 节点只跑容器。Manager 也会跑容器——小集群不需要单独的管理节点。节点管理# 标记节点角色(让某些任务只跑在特定节点)docker node update --label-add role=backend node2docker node update --label-add role=database node3# 排空节点(维护时把容器迁移走)docker node update --availability drain node2# 恢复节点docker node update --availability active node2部署服务基本部署# 部署一个服务,3 个副本docker service create \ --name myapp \ --replicas 3 \ --publish 3000:3000 \ myapp:latestSwarm 自动把 3 个副本分散到不同节点,内置负载均衡——访问任意节点的 3000 端口都会被路由到健康的副本。使用 docker-compose 部署# docker-compose.ymlservices: 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:# 部署docker stack deploy -c docker-compose.yml myapp# 查看服务docker service ls# 查看某个服务的副本docker service ps myapp_api滚动更新# 更新镜像版本docker service update --image myapp:v2.0 myapp_api# 查看更新进度docker service ps myapp_apiupdate_config 控制更新策略:parallelism: 1 每次只更新 1 个副本delay: 10s 每个副本更新后等 10 秒再更新下一个failure_action: rollback 新版本启动失败时自动回滚到旧版本手动回滚# 回滚到上一版本docker service rollback myapp_apiOverlay 网络:跨主机通信Swarm 模式下的 Overlay 网络让不同主机上的容器直接通信:# 创建 Overlay 网络docker network create -d overlay my-netservices: api: networks: - my-net redis: networks: - my-netnetworks: my-net: external: trueapi 容器在 node1,redis 在 node2——通过 redis:6379 直接访问,和单机体验一样。配置和敏感信息Config(非敏感配置)# 创建配置echo "server.port=8080" | docker config create app_config -services: api: configs: - source: app_config target: /app/config.propertiesconfigs: app_config: external: trueSecret(敏感信息)# 创建 Secretecho "db_password_123" | docker secret create db_password -services: api: secrets: - db_passwordsecrets: db_password: external: trueSecret 在容器内挂载为 /run/secrets/db_password,只有容器内可读,不会出现在 docker inspect 里。常用运维命令# 查看服务日志docker service logs myapp_api# 扩缩容docker service scale myapp_api=5# 查看服务详情docker service inspect myapp_api# 删除服务docker service rm myapp_api# 删除整个 Stackdocker stack rm myapp# 查看集群事件docker events --filter type=service什么时候该从 Swarm 迁移到 K8s需要自动扩缩容(HPA)需要 CronJob(定时任务)需要 Ingress 控制器(7 层路由)需要 Pod 级别的健康检查集群超过 50 个节点在以上需求出现之前,Swarm 够用且省心。
服务端阅读 06月6日 20:29

Docker 怎么限制容器资源?CPU、内存和磁盘 IO 配置

不限制容器资源,一个失控的容器就能吃光宿主机内存,拖垮同一台机器上的所有服务。Docker 提供了 CPU、内存、磁盘 IO 的精细限制手段,核心是 docker run 的资源参数和 docker-compose 的 deploy.resources 配置。内存限制:最常用也最重要# 限制最大内存 4GBdocker run -d --name myapp --memory=4g myapp:latest# 限制内存 + 禁用 swapdocker run -d --name myapp --memory=4g --memory-swap=4g myapp:latest--memory-swap=4g 等于 --memory 的值意味着容器不能用 swap。如果 --memory-swap 比 --memory 大,差值就是允许的 swap 大小。docker-compose 配置services: app: image: myapp:latest deploy: resources: limits: memory: 4G # 硬上限,超过会被 OOM Kill reservations: memory: 1G # 软保底,调度时保证至少 1GOOM 时会发生什么容器内存超过 limits.memory 时,内核 OOM Killer 杀掉容器里内存占用最大的进程:# 检查容器是否被 OOM 杀掉docker inspect myapp --format '{{.State.OOMKilled}}'# true = 被 OOM 杀了# 查看 OOM 事件dmesg | grep -i oomOOM 优先级调整多个容器抢内存时,可以设 OOM 优先级:# 不容易被 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 核数# 最多用 2 核docker run -d --cpus=2.0 myapp# 最多用 0.5 核docker run -d --cpus=0.5 myappservices: app: deploy: resources: limits: cpus: '2.0' reservations: cpus: '0.5'--cpus=2.0 不是绑核——容器可以在任意 2 个核心上运行,只是总使用时间不超过 200%。绑核用 --cpuset-cpus:# 只在第 0 和第 2 个核心上运行docker run -d --cpuset-cpus=0,2 myapp# 只在第 1-3 个核心上运行docker run -d --cpuset-cpus=1-3 myapp绑核适合对 CPU 缓存一致性敏感的应用(如高性能计算),一般 Web 服务不需要。CPU 权重多个容器抢 CPU 时,按权重分配:# 默认权重 1024# 高权重 = 抢到更多 CPU 时间docker run -d --cpu-shares=2048 high-priority-appdocker run -d --cpu-shares=512 low-priority-app注意:--cpu-shares 只在 CPU 资源紧张时生效。CPU 空闲时低权重容器也能用满 CPU。磁盘 IO 限制限制容器读写磁盘的速率,防止一个容器把磁盘 IO 吃光:# 限制写速率 10MB/sdocker run -d \ --device-write-bps /dev/sda:10mb \ myapp# 限制读速率 20MB/sdocker 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 炸弹:# 最多 100 个进程docker run -d --pids-limit=100 myappservices: app: deploy: resources: limits: pids: 100没有这个限制,一个容器可以 fork 出几千个进程,耗尽宿主机的 PID 表。运行时修改资源限制不需要重建容器就能调整限制:# 动态调整内存限制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 观察实际用量再收紧
服务端阅读 06月6日 20:29

Docker 私有仓库怎么搭建?Registry 和 Harbor 选型

Docker Hub 是公开的,你的私有镜像不想让外人看到。企业内部需要一个私有 Registry 存放自己的镜像,CI/CD 推送镜像到私有仓库,生产服务器从私有仓库拉取。最简方案:Docker 官方 RegistryDocker 官方提供了一个极简的 Registry 镜像,几分钟就能跑起来:# 启动私有仓库docker run -d -p 5000:5000 --name registry registry:2# 推送镜像docker tag myapp:latest localhost:5000/myapp:latestdocker push localhost:5000/myapp:latest# 拉取镜像docker pull localhost:5000/myapp:latest这就够了——一个能推能拉的私有仓库。但生产环境需要持久化存储、认证、TLS。持久化存储services: registry: image: registry:2 ports: - "5000:5000" volumes: - registry_data:/var/lib/registryvolumes: registry_data:默认镜像存在 /var/lib/registry,用 Volume 持久化防止容器重启后镜像丢失。启用 TLSHTTP 模式下 Docker 客户端会拒绝推送(安全限制)。加 TLS: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# 自签名证书(测试用)mkdir certsopenssl req -newkey rsa:4096 -nodes -sha256 \ -keyout certs/domain.key \ -x509 -days 365 \ -out certs/domain.crt \ -subj "/CN=registry.example.com"所有拉取镜像的机器都要信任这个证书:# 把证书复制到 Docker 信任目录sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000sudo cp certs/domain.crt /etc/docker/certs.d/registry.example.com:5000/ca.crtsudo systemctl restart docker基本认证# 创建用户密码文件mkdir authdocker run --entrypoint htpasswd httpd:2 -Bbn admin password123 > auth/htpasswdservices: registry: image: registry:2 environment: REGISTRY_AUTH: htpassd REGISTRY_AUTH_HTPASSWD_REALM: "Registry Realm" REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd volumes: - ./auth:/auth:ro# 登录后才能推送docker login registry.example.com:5000# Username: admin# Password: password123Harbor:企业级私有仓库Docker 官方 Registry 功能太简陋——没有 Web 界面、没有镜像扫描、没有 RBAC。Harbor 是 VMware 开源的企业级 Registry,补全了这些能力。Docker Compose 部署 Harbor# 下载 Harborwget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgztar xzf harbor-offline-installer-v2.10.0.tgzcd harbor# 编辑配置cp harbor.yml.tmpl harbor.yml# harbor.yml 关键配置hostname: registry.example.comhttp: port: 80https: port: 443 certificate: /certs/domain.crt private_key: /certs/domain.keyharbor_admin_password: Harbor12345data_volume: /data/harbor# 安装./install.sh# 访问 Web 界面# https://registry.example.com# 用户名: admin 密码: Harbor12345Harbor 的企业级功能| 功能 | 说明 ||------|------|| 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 集成 || 阿里云 | 容器镜像服务 | 国内访问快 |# AWS ECR 示例aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.comdocker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latestdocker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest托管服务的优势:不需要维护服务器、自动 TLS、自动漏洞扫描。劣势:网络延迟(国内拉海外镜像慢)、费用随存储量增长。选择决策| 场景 | 推荐方案 ||------|---------|| 个人/小团队测试 | Docker 官方 Registry || 团队 5-20 人 | Harbor(Docker Compose 部署) || 企业生产环境 | Harbor(高可用部署) || 全部在云上 | 云厂商托管 Registry || 国内访问为主 | 阿里云容器镜像服务 |起步建议:先用官方 Registry 跑起来,等需要 Web 界面和权限管理时迁移到 Harbor。
服务端阅读 06月6日 20:29

Docker 容器日志怎么管理?轮转、结构化和聚合方案

容器日志管理不只是 docker logs ——那只能看单个容器的标准输出。生产环境需要日志轮转防止磁盘撑满、日志聚合实现集中查询、结构化日志方便检索。这篇从本地管理到集中式方案逐步展开。docker logs 的局限docker logs myapp # 查看日志docker logs -f myapp # 实时跟踪docker logs --tail 100 myapp # 最近 100 行问题:容器删了日志就没了多容器没法一起搜没有日志轮转,磁盘会被撑满没有结构化字段,搜索靠 grep本地日志轮转:防止磁盘撑满json-file 驱动的轮转配置services: app: image: myapp:latest logging: driver: json-file options: max-size: "10m" # 单个日志文件最大 10MB max-file: "3" # 最多保留 3 个文件这样每个容器最多占 30MB 日志(10MB × 3 个文件)。超过 10MB 自动轮转,超过 3 个文件自动删除最老的。local 驱动:更省磁盘services: app: logging: driver: local options: max-size: "10m" max-file: "5"local 驱动用压缩存储,同样内容比 json-file 省 50% 空间。而且日志格式更易读。全局配置不想每个容器都写 logging 配置?在 daemon.json 里设全局默认:// /etc/docker/daemon.json{ "log-driver": "local", "log-opts": { "max-size": "10m", "max-file": "3" }}重启 Docker 后所有容器都用这个配置。强烈建议加上——我见过太多服务器因为 Docker 日志占满磁盘而崩溃。结构化日志:让检索更高效非结构化日志只能全文搜索。结构化日志可以按字段过滤:# Python - 用 structlog 输出 JSONimport structloglogger = structlog.get_logger()logger.info("user_login", user_id=123, ip="1.2.3.4")// Node.js - 用 pino 输出 JSONconst pino = require('pino')()pino.info({ userId: 123, action: 'login' }, 'User logged in')输出示例:{"level":"info","time":1704067200,"userId":123,"action":"login","msg":"User logged in"}在日志聚合平台里可以按 userId=123 或 action=login 精确过滤,不用全文搜索。日志级别管理# 动态调整日志级别(不需要重启容器)# Spring Bootcurl -X POST http://localhost:8080/actuator/loggers/com.example \ -d '{"configuredLevel": "DEBUG"}'# Node.js(需要应用支持)# 通过环境变量控制LOG_LEVEL=debug node app.js生产环境默认 INFO 级别,排查问题时临时切 DEBUG,不需要重新部署。日志聚合:集中式管理轻量方案:Grafana LokiLoki 只索引标签不索引正文,存储成本是 ELK 的 1/10: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 查询:{container_name="myapp"} |= "error" | json | level="error"重量方案:EFK Stack需要全文搜索、复杂聚合时用 EFK(Elasticsearch + Fluentd + Kibana):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 日志驱动:services: app: logging: driver: fluentd options: fluentd-address: localhost:24224 tag: myappEFK 最少需要 4GB 内存。团队小于 10 人用 Loki 就够了。日志管理最佳实践| 检查项 | 建议 ||--------|------|| 日志轮转 | 全局配 max-size: 10m, max-file: 3 || 日志级别 | 生产用 INFO,排查切 DEBUG || 结构化 | 用 JSON 格式输出 || 敏感信息 | 不在日志里打印密码、token || 聚合 | 小团队用 Loki,大团队用 EFK || 持久化 | 关键日志用 Volume 存储,不依赖容器可写层 || 监控 | 对 ERROR 日志设置告警 |起步建议:先配好本地日志轮转(10 分钟的事),再按需加 Loki 聚合。
服务端阅读 06月6日 20:29

Docker 容器间怎么通信?同一项目、跨项目和跨主机方案

两个容器要互相访问,怎么连通?同一个 compose 项目的容器用服务名直接访问,不同项目的容器需要共享网络,跨主机通信就得用 Overlay 网络。这篇按场景从简单到复杂讲清楚。同一 docker-compose 项目:默认网络Docker Compose 自动为每个项目创建一个网络,项目内的容器可以互相用服务名访问:# docker-compose.ymlservices: api: image: myapp-api ports: - "3000:3000" redis: image: redis:7 postgres: image: postgres:16# api 容器内直接用服务名访问curl http://redis:6379 # 访问 Rediscurl http://postgres:5432 # 访问 PostgreSQL不需要 IP,不需要 --link——Docker 内置 DNS 自动把服务名解析为容器 IP。自定义网络名默认网络名是 项目名_default。如果要自定义:services: api: networks: - frontend - backend redis: networks: - backendnetworks: frontend: backend:api 同时在 frontend 和 backend 两个网络里——可以访问两边。redis 只在 backend 里——frontend 网络的容器访问不到 redis,实现网络隔离。不同 docker-compose 项目:外部网络两个独立的 compose 项目需要通信时,共享一个外部网络:# 创建共享网络docker network create shared-net# 项目 A: docker-compose.ymlservices: api: networks: - shared-netnetworks: shared-net: external: true# 项目 B: docker-compose.ymlservices: worker: networks: - shared-netnetworks: shared-net: external: true项目 A 的 api 和项目 B 的 worker 通过服务名互相访问。容器访问宿主机容器里需要访问宿主机上的服务(比如宿主机上的 MySQL):# 专用 DNS 名curl http://host.docker.internal:3306host.docker.internal 是 Docker Desktop 提供的特殊 DNS,自动解析为宿主机 IP。Linux 上需要手动添加:services: api: extra_hosts: - "host.docker.internal:host-gateway"容器间直接用 IP不推荐但有时需要:# 查看容器 IPdocker inspect myapp --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'# 查看所有容器的 IPdocker network inspect bridge --format '{{range .Containers}}{{.Name}}: {{.IPv4Address}}{{end}}'容器 IP 每次重启可能变化——硬编码 IP 是反模式,应该用服务名或 DNS。端口映射:容器对外暴露服务services: api: ports: - "3000:3000" # 宿主机 3000 → 容器 3000 - "8080:80" # 宿主机 8080 → 容器 80 - "127.0.0.1:3306:3306" # 只允许本机访问127.0.0.1:3306:3306 这种写法限制了只监听 loopback 接口——外部无法访问,只有宿主机本身可以连。适合数据库等不需要对外暴露的服务。端口冲突排查# 查看宿主机端口占用lsof -i :3000# 或ss -tlnp | grep 3000# Docker 占用的端口docker port myapp网络模式选择| 模式 | 说明 | 适用场景 ||------|------|---------|| bridge(默认) | 容器有独立 IP,通过 NAT 访问外部 | 大部分场景 || host | 容器直接用宿主机网络栈,无隔离 | 需要极致网络性能 || none | 无网络 | 离线计算任务 || overlay | 跨主机容器通信 | Docker Swarm / 多主机 |host 模式services: api: network_mode: hosthost 模式下容器没有独立 IP,直接用宿主机的端口和网络。好处是没有 NAT 性能损耗,坏处是端口冲突风险高(容器和宿主机共享端口空间)。不要在生产环境用 host 模式——失去了网络隔离,一个容器被攻破等于宿主机被攻破。跨主机通信:Overlay 网络Docker Swarm 多主机环境下,不同主机上的容器需要通信:# 创建 Overlay 网络docker network create -d overlay my-overlay# 在 Overlay 网络上启动服务docker service create --network my-overlay --name api myappdocker service create --network my-overlay --name worker myworkerOverlay 网络底层用 VXLAN 隧道——api 和 worker 即使跑在不同的物理机上,也能通过服务名直接通信,和单机体验一致。DNS 排查容器间访问不通时,先排查 DNS:# 进入容器测试 DNS 解析docker exec myapp nslookup redisdocker exec myapp ping redis# 查看 DNS 配置docker exec myapp cat /etc/resolv.conf# 临时指定 DNSdocker run --dns 8.8.8.8 myapp常见问题:自定义了 docker-compose.yml 的 networks 但忘了在服务里引用,或者两个服务不在同一个网络里。