Docker面试题手册

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

服务端阅读 02026年6月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 越显式,镜像构建越稳定,也越容易排查问题。
服务端阅读 02026年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 状态,适合先准备配置、网络、挂载参数,再手动启动。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 数据是否保留则要单独确认。
服务端阅读 02026年6月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 但忘了在服务里引用,或者两个服务不在同一个网络里。
服务端阅读 06月6日 20:23

Docker 容器文件系统怎么工作?分层和 Copy-on-Write 机制

容器里写了一个文件,它到底存在哪里?删掉容器后文件去哪了?为什么镜像层是只读的但容器可以写入?理解 Docker 文件系统的工作原理,这些问题就都清楚了。从镜像到容器:分层文件系统Docker 镜像不是一个大文件——它是多层只读文件系统的堆叠。每个 Dockerfile 指令产生一层:FROM ubuntu:22.04 # 层 1:操作系统基础(~70MB)RUN apt-get update # 层 2:包索引更新RUN apt-get install -y python3 # 层 3:Python 运行时COPY . /app # 层 4:应用代码CMD ["python3", "/app/main.py"] # 不产生新层(只是元数据)┌────────────────────────┐│ 可写层(容器运行时添加) │ ← 容器启动后才有├────────────────────────┤│ 层 4:应用代码 │ ← COPY 产生├────────────────────────┤│ 层 3:Python 运行时 │ ← RUN 产生├────────────────────────┤│ 层 2:包索引 │ ← RUN 产生├────────────────────────┤│ 层 1:Ubuntu 基础 │ ← FROM 产生└────────────────────────┘关键:每个只读层都是不可变的。删除了上一层的文件,并不会真的删除——只是在新层里标记为"已删除"(whiteout 文件)。Copy-on-Write:容器写入的核心机制容器启动时,Docker 在所有只读层上面加一层可写层。容器内的写操作都走 CoW:修改已有文件1. 容器要修改 /etc/config.yml(存在于镜像层 2)2. Docker 从层 2 把 config.yml 复制到可写层3. 修改发生在可写层的副本上4. 后续读取这个文件时,可写层的版本"遮盖"了镜像层的原始版本新建文件直接写在可写层,不涉及复制。删除文件在可写层创建一个 whiteout 文件(标记为已删除),不真的从只读层删除。overlay2 的实现overlay2 是 Docker 默认的存储驱动,基于 Linux 内核的 OverlayFS:容器看到的是 merged 视图:┌──────────────┐│ merged │ = upperdir + lowerdir 合并后的视图├──────────────┤│ upperdir │ = 可写层(容器修改的文件)├──────────────┤│ lowerdir │ = 镜像的所有只读层└──────────────┘在宿主机上的实际位置# Docker 的存储根目录ls /var/lib/docker/overlay2/# 每个镜像层一个目录/var/lib/docker/overlay2/<layer-id>/ diff/ # 这一层的文件内容 link # 指向层的短链接 lower # 指向下层层的链接 merged/ # 合并视图(容器运行时才出现) work/ # OverlayFS 工作目录查看容器的 overlay2 层# 找到容器的 overlay2 路径docker inspect myapp --format '{{.GraphDriver.Data.MergedDir}}'# /var/lib/docker/overlay2/abc123/merged# 查看可写层的内容ls /var/lib/docker/overlay2/abc123/diff/diff/ 目录里就是容器内所有修改过的文件——和 docker diff 命令看到的一致。为什么容器删除后数据会丢1. docker run → 创建可写层 + 启动容器2. 容器运行中 → 数据写在可写层3. docker rm → 删除可写层 + 容器元数据可写层随容器一起删除,里面的数据也消失了。这就是为什么需要 Volume:services: postgres: image: postgres:16 volumes: - pg_data:/var/lib/postgresql/data # 数据存在 Volume,不在可写层Volume 的数据存在 /var/lib/docker/volumes/pg_data/,容器删除不影响它。镜像层的共享和节省空间多个镜像共享相同的基础层:镜像 A(Node.js 应用) 镜像 B(Python 应用)┌──────────────┐ ┌──────────────┐│ 层 3:App A │ │ 层 3:App B │ ← 不同├──────────────┤ ├──────────────┤│ 层 2:Node.js│ │ 层 2:Python │ ← 不同├──────────────┤ ├──────────────┤│ 层 1:Ubuntu │ │ 层 1:Ubuntu │ ← 共享!只存一份└──────────────┘ └──────────────┘# 查看 Docker 磁盘占用docker system df -v# SHARED SIZE 列显示被多个镜像共享的大小# UNIQUE SIZE 列显示该镜像独有的层大小镜像构建优化:减少层和大小合并 RUN 指令# 差:每条 RUN 一层RUN apt-get updateRUN apt-get install -y python3RUN apt-get install -y pip# 好:合并成一层RUN apt-get update && \ apt-get install -y python3 pip && \ rm -rf /var/lib/apt/lists/*合并的好处:层数减少rm -rf /var/lib/apt/lists/ 在同一层执行——删除操作真的释放了空间,而不是在下一层标记为已删除多阶段构建FROM node:20 AS builderWORKDIR /appCOPY . .RUN npm ci && npm run buildFROM node:20-alpineCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCMD ["node", "dist/index.js"]最终镜像只有 dist/ 和 node_modules/——没有源码、没有构建工具,体积缩小 60-80%。.dockerignore 减少构建上下文# .dockerignore.gitnode_modules.env*.mdtest/.dockerignore 排除的文件不会进入构建上下文——既加快构建,又防止敏感文件进镜像。常见文件系统问题镜像越来越大# 查看每层大小docker history myapp:latest# 找出最大的层docker history --format "{{.Size}}\t{{.CreatedBy}}" myapp:latest常见的膨胀原因:apt-get update 的缓存、npm 缓存、临时文件。在同一层里清理掉。容器写入慢可写层写大量小文件时性能差——特别是 Mac/Windows 上 Docker Desktop 的文件共享。解决方案:大量写入用 Volume。inode 耗尽大量小文件会消耗 inode。df -i 查看剩余 inode。overlay2 比 overlay 的 inode 使用更少——这也是推荐 overlay2 的原因之一。
服务端阅读 06月6日 20:23

Docker 容器出问题了怎么排查?分层排查流程

容器启动失败、运行中崩溃、网络不通、数据丢失——Docker 故障排查有一套固定流程,按层级从外到内逐步缩小范围。排查流程总览容器状态异常?├── 容器没启动 → 查 docker logs├── 容器运行但行为异常 → 查 docker logs + docker exec├── 容器网络不通 → 查 docker network + 端口映射├── 容器数据问题 → 查 Volume 挂载└── 宿主机资源不足 → 查 docker stats + 系统资源第一步:确认容器状态# 查看所有容器(包括停止的)docker ps -a# 关注 STATUS 列# Up 2 hours → 正常运行# Exited (0) 5 mins → 正常退出# Exited (1) 5 mins → 错误退出# Exited (137) 1 min → 被 SIGKILL 杀掉(通常是 OOM)# Restarting → 不断重启(崩溃循环)退出码含义:| 退出码 | 含义 ||--------|------|| 0 | 正常退出 || 1 | 应用错误 || 137 | OOM Killed(内存不足) || 139 | Segmentation Fault || 143 | SIGTERM(正常停止) |第二步:查日志docker logs# 查看容器日志docker logs myapp# 实时跟踪docker logs -f myapp# 最近 100 行docker logs --tail 100 myapp# 带时间戳docker logs -t myapp# 查看某个时间段的日志docker logs --since "2024-01-01T00:00:00" --until "2024-01-01T12:00:00" myapp容器已经退出了?# 已退出的容器仍然可以查日志docker logs myapp # 只要容器没被 docker rm 删掉# 如果容器被删了,日志文件还在(json-file 驱动)ls /var/lib/docker/containers/<container-id>/# <container-id>-json.log日志里什么都没有?应用可能把日志写到了文件而不是 stdout/stderr。Docker 只捕获标准输出/错误流:# 进入容器查看日志文件docker exec -it myapp shcat /app/logs/app.log最佳实践:让应用把日志输出到 stdout/stderr,这样 docker logs 直接能看到。不要写到容器内的文件——容器删除后日志也丢了。第三步:进入容器排查# 进入容器执行命令docker exec -it myapp sh# 如果容器没有 sh,用 bashdocker exec -it myapp bash# 如果都没有(alpine 镜像可能只有 sh)docker exec -it myapp /bin/sh# 不进入容器,直接执行命令docker exec myapp ps auxdocker exec myapp cat /etc/config.yml容器里能做的事:ps aux 看进程top 看 CPU/内存cat /etc/hosts 看网络配置curl localhost:3000 测试内部端口env 看环境变量容器一启动就退出了怎么办docker exec 需要容器在运行状态。容器一启动就退出的情况,用覆盖入口点的方式:# 覆盖 CMD,保持容器运行docker run -it --entrypoint sh myapp:latest# 或者在 Dockerfile 末尾临时加# CMD ["sleep", "3600"]进入容器后手动执行原来的启动命令,观察报错。第四步:排查网络问题容器端口没暴露# 检查端口映射docker port myapp# 3000/tcp -> 0.0.0.0:3000# 如果没有输出,说明没做端口映射# 重新启动时加 -pdocker run -d -p 3000:3000 myapp容器间网络不通# 查看容器的网络docker inspect myapp --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'# 查看所有 Docker 网络docker network ls# 同一网络的容器可以互相通过容器名访问# 不同网络的容器互相隔离# 测试容器间连通性docker exec myapp ping redisdocker exec myapp curl http://api-service:8080/healthDNS 解析失败# 查看容器的 DNS 配置docker exec myapp cat /etc/resolv.conf# 自定义 DNSdocker run --dns 8.8.8.8 myapp第五步:排查 Volume 问题文件没有出现在容器内# 检查 Volume 挂载docker inspect myapp --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}'# /host/path -> /container/path常见原因:宿主机路径写错了(相对路径问题)文件权限不匹配(容器内用户没读权限)SELinux 阻止访问(加 :z 或 :Z 后缀)# SELinux 环境docker run -v /host/path:/container/path:z myapp # 多容器共享docker run -v /host/path:/container/path:Z myapp # 单容器专用容器内修改没有反映到宿主机检查是不是挂载方向反了,或者挂载的是文件而不是目录:# 错误:挂载不存在的文件,Docker 会创建一个目录docker run -v ./config.yml:/app/config.yml myapp# 如果 ./config.yml 不存在,Docker 会创建 /app/config.yml 目录# 正确:确保文件先存在touch ./config.ymldocker run -v ./config.yml:/app/config.yml myapp常见故障速查| 症状 | 排查命令 | 常见原因 ||------|---------|---------|| 容器启动就退出 | docker logs | CMD 执行失败、配置错误 || 容器被杀 | docker inspect --format '{{.State.OOMKilled}}' | 内存不足 || 端口访问不到 | docker port + curl | 没映射端口、防火墙 || 容器间不通 | docker exec ping | 不在同一网络 || 磁盘满 | df -h + docker system df | 日志太多、镜像太多 || 构建慢 | docker build --progress=plain | 没用缓存、上下文太大 || 权限错误 | docker exec ls -la | UID 不匹配、SELinux || 容器不断重启 | docker logs --tail 50 | 启动脚本报错、依赖服务未就绪 |调试技巧用 debug 镜像替换生产镜像可能是 distroless 或 alpine,没有 curl、ping 等工具。临时用 debug 镜像排查:# 把镜像临时换成有工具的版本docker run -d --name myapp-debug \ --entrypoint sh \ -p 3000:3000 \ myapp:latest -c "sleep 3600"docker diff 看文件变更# 查看容器内哪些文件被修改了docker diff myapp# A /app/newfile.txt → Added# C /etc/config.yml → Changed# D /tmp/old.log → Deleteddocker events 实时监控# 监控 Docker 事件(容器启停、OOM、健康检查等)docker events --filter container=myapp
服务端阅读 06月6日 20:23

Docker 容器怎么接入 CI/CD?GitHub Actions 和 GitLab CI 集成

代码提交后自动构建镜像、跑测试、部署——这就是 CI/CD 和 Docker 的结合。Docker 让构建环境一致、部署产物标准化,CI/CD 让这一切自动运行。这篇讲 Docker 在主流 CI/CD 平台中的集成方式。Docker 在 CI/CD 中的三个角色构建环境:CI Runner 本身跑在 Docker 容器里,保证每次构建环境一致构建产物:docker build 产出镜像,推到 Registry,部署时直接拉镜像部署目标:生产环境拉镜像启动容器,不需要在服务器上装运行时GitHub Actions + Docker基本构建和推送# .github/workflows/deploy.ymlname: Build and Push Docker Imageon: push: branches: [main]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | myorg/myapp:latest myorg/myapp:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=maxcache-from: type=gha 用 GitHub Actions 的缓存加速构建——没有变化的层直接复用,不用重新构建。首次构建可能要 5 分钟,后续只要 1-2 分钟。多阶段构建减小镜像# 构建阶段FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build# 运行阶段——只有产物,没有源码和 devDependenciesFROM node:20-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCMD ["node", "dist/index.js"]多阶段构建让最终镜像只有运行时需要的文件——从 1GB+ 缩小到 200MB 以下。GitLab CI + DockerDocker-in-Docker 构建# .gitlab-ci.ymlbuild: image: docker:24 services: - docker:24-dind variables: DOCKER_TLS_CERTDIR: "/certs" before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHAdocker:24-dind 服务启动一个 Docker 守护进程,让 CI 容器里可以执行 docker build。注意要加 DOCKER_TLS_CERTDIR 确保通信安全。使用 GitLab Container RegistryGitLab 自带 Container Registry,不用额外配 Docker Hub:script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA$CI_REGISTRY_IMAGE 是 GitLab 预设变量,指向当前项目的 Registry 地址。镜像标签策略# 不推荐:只用 latestmyapp:latest# 推荐:语义化版本 + Git SHA 双标签myapp:1.2.3myapp:sha-abc1234# 回滚时指定版本docker pull myapp:1.2.2| 标签 | 用途 | 示例 ||------|------|------|| latest | 默认最新版 | 开发环境用 || 语义化版本 | 生产部署 | 1.2.3 || Git SHA | 精确追溯 | sha-abc1234 || 分支名 | 预览环境 | main, staging |生产环境永远用精确版本号或 SHA,不用 latest——latest 指向的镜像随时可能变,出了问题无法回滚。部署阶段SSH 部署到服务器# GitHub Actions- name: Deploy to server uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: deploy key: ${{ secrets.SSH_PRIVATE_KEY }} script: | docker pull myorg/myapp:${{ github.sha }} docker stop myapp || true docker rm myapp || true docker run -d --name myapp \ --restart unless-stopped \ -p 3000:3000 \ myorg/myapp:${{ github.sha }}docker-compose 部署服务器上放一个 docker-compose.yml,CI 只需要触发 docker compose pull && docker compose up -d:# 服务器上的 docker-compose.ymlservices: app: image: myorg/myapp:latest ports: - "3000:3000" restart: unless-stopped# CI 部署脚本ssh deploy@server "cd /app && docker compose pull && docker compose up -d"零停机部署上面的方式会短暂中断服务。用双容器切换实现零停机:# 1. 启动新容器在不同端口docker run -d --name myapp-new -p 3001:3000 myorg/myapp:new-version# 2. 健康检查until curl -f http://localhost:3001/health; do sleep 1; done# 3. 切换 Nginx 上游# 更新 nginx upstream 指向 3001docker exec nginx nginx -s reload# 4. 停掉旧容器docker stop myapp-old && docker rm myapp-old# 5. 重命名新容器docker rename myapp-new myapp-old安全最佳实践不要用 root 跑容器:Dockerfile 里加 USER app扫描镜像漏洞:docker scout cves myapp:latest 或 Trivy最小基础镜像:用 alpine 或 distroless 代替 ubuntuSecret 不进镜像:数据库密码等通过环境变量或 Secret 注入,不写进 Dockerfile.dockerignore 排除无关文件:.git、node_modules、.env 不应该进构建上下文# .dockerignore.gitnode_modules.env*.md.dockerignore
服务端阅读 06月6日 20:23

Docker 存储驱动该选哪个?overlay2 和其他驱动的区别

Docker 镜像的每一层怎么存、容器怎么在只读层上写入、不同存储驱动有什么性能差异——理解存储驱动的工作原理,能帮你解决容器写入慢、镜像占磁盘、数据丢失这些问题。容器文件系统的分层结构Docker 镜像由多个只读层组成,容器启动时在最上面加一层可写层:┌─────────────────────┐│ 可写层(容器层) │ ← docker run 产生的可写层├─────────────────────┤│ 镜像层 3(App) │ ← COPY / RUN 指令产生的层├─────────────────────┤│ 镜像层 2(Runtime)│├─────────────────────┤│ 镜像层 1(OS 基础) │└─────────────────────┘只读层:镜像的每一层都是只读的,多个容器可以共享同一组只读层可写层:容器的所有修改(新建文件、修改配置、写日志)都写在这层容器删除后,可写层一起消失——这就是为什么容器内写的数据会丢Copy-on-Write 机制容器修改镜像中的文件时,不是直接改只读层——而是把文件复制到可写层再修改:# 镜像里有个 /etc/config.yml# 容器内修改它:echo "new value" >> /etc/config.yml# 发生了什么:# 1. 从只读层复制 config.yml 到可写层# 2. 在可写层修改# 3. 后续读这个文件时读可写层的版本(遮盖了只读层的原始文件)CoW 的性能影响:第一次修改大文件时会比较慢(需要完整复制)。频繁修改大文件(比如数据库的数据文件)不适合放在容器可写层——应该用 Volume。主流存储驱动对比| 驱动 | 文件系统 | 适用场景 | 性能 ||------|---------|---------|------|| overlay2 | OverlayFS | 默认推荐,所有 Linux 发行版 | 好 || devicemapper | devicemapper | 老系统、RHEL/CentOS 6 | 一般 || btrfs | Btrfs | 需要快照、子卷管理 | 写入好,读取一般 || zfs | ZFS | 需要数据完整性校验 | 功能强但内存需求大 |overlay2:默认且最优overlay2 是当前 Docker 的默认存储驱动,也是性能最好的:# 查看当前存储驱动docker info --format '{{.Driver}}'# overlay2overlay2 的工作方式:lowerdir(只读层)= 镜像的所有层upperdir(可写层)= 容器的修改merged = 合并视图(容器看到的文件系统)优势:只有一层 lowerdir 目录,不像老版 overlay 有多层,inode 消耗少页缓存共享——多个容器读同一个文件只占一份内存build 性能好——Docker Build 时层操作都是 O(1)99% 的场景用 overlay2 就对了,不需要换。devicemapper:老系统的选择RHEL/CentOS 6 时代内核不支持 OverlayFS,只能用 devicemapper。有两种模式:loopback(默认):用文件模拟块设备,性能差,生产环境不能用direct-lvm:直接用 LVM 块设备,性能可以// /etc/docker/daemon.json - direct-lvm 配置{ "storage-driver": "devicemapper", "storage-opts": [ "dm.directlvm_device=/dev/sdb", "dm.thinp_percent=95", "dm.thinp_metapercent=1" ]}现在大部分系统已经支持 overlay2,不需要再折腾 devicemapper。存储驱动和磁盘占用的关系每个存储驱动管理镜像层的方式不同,磁盘占用差异很大:# 查看 Docker 占用的磁盘docker system df# 详细信息docker system df -vTYPE TOTAL ACTIVE SIZE RECLAIMABLEImages 15 5 4.2GB 2.1GB (50%)Containers 8 3 120MB 80MB (66%)Local Volumes 5 3 800MB 200MB (25%)Build Cache 50 0 1.5GB 1.5GB (100%)RECLAIMABLE 是可以回收的空间。清理命令:# 清理悬空镜像(没有标签的)docker image prune# 清理所有未使用的镜像docker image prune -a# 清理构建缓存docker builder prune# 一键全清docker system prune -aVolume:数据持久化的正确方式容器可写层的数据会随容器删除而丢失。需要持久化的数据(数据库、日志、配置)用 Volume:services: postgres: image: postgres:16 volumes: - pg_data:/var/lib/postgresql/data # 命名 Volume - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 绑定挂载 app: image: myapp:latest volumes: - ./config:/app/config:ro # 只读绑定挂载 - app_logs:/app/logs # 命名 Volumevolumes: pg_data: app_logs:命名 Volume vs 绑定挂载| | 命名 Volume | 绑定挂载 ||---|---|---|| 存储位置 | Docker 管理(/var/lib/docker/volumes/) | 宿主机任意目录 || 性能 | 好(Docker 优化) | Mac/Win 上可能慢 || 可移植性 | 好 | 依赖宿主机路径 || 备份 | docker volume inspect 找路径 | 直接访问宿主机目录 || 适用场景 | 数据库、应用数据 | 配置文件、代码挂载 |存储驱动选择决策| 你的情况 | 推荐驱动 ||---------|---------|| Linux 内核 4.0+ | overlay2(默认) || RHEL/CentOS 7+ | overlay2 || 需要 ZFS 快照和校验 | zfs || 需要 Btrfs 子卷管理 | btrfs || 老系统无法升级内核 | devicemapper (direct-lvm) |不确定就用 overlay2——它已经是默认选项了。除非有明确的特殊需求,否则不需要改存储驱动。