上一篇 下一篇 分享链接 返回 返回顶部

Docker 上生产后才懂的 20 个坑:从镜像、日志到数据安全

发布人:慈云数据-客服中心 发布时间:19小时前 阅读量:3

Docker 使用避坑指南|生产环境实测

Docker 早已成为后端服务部署、CI/CD、微服务治理中的基础设施之一。它的优势非常明显:环境一致、部署快速、依赖隔离、扩缩容方便。但在生产环境中,Docker 并不是“装上就万事大吉”的银弹。很多问题在开发环境中不明显,一旦进入生产,就会演变成性能瓶颈、磁盘爆满、容器异常退出、日志失控、安全风险甚至数据丢失。

本文结合生产环境中的实际使用经验,系统梳理 Docker 在镜像构建、容器运行、数据持久化、网络配置、日志管理、资源限制、安全加固、故障排查等方面的常见坑点,并给出对应的规避建议。


一、不要把 Docker 当成虚拟机使用

很多刚接触 Docker 的团队,容易把容器当成一台“小型虚拟机”来使用:进入容器手动安装软件、修改配置、重启服务,甚至在容器里运行多个进程。这种做法在生产环境中非常危险。

Docker 的核心理念是:镜像不可变,容器可替换

也就是说,容器应该是由镜像启动出来的临时运行实例,而不是一个需要长期维护的“服务器”。如果你进入容器手动修改配置,那么一旦容器重启、迁移或重新部署,这些修改很可能全部丢失。

常见错误做法

docker exec -it app-container bash
apt update
apt install vim
vim /etc/app/config.yml
systemctl restart app

这种方式看起来很方便,但问题很多:

  • 修改过程无法追溯;
  • 配置无法版本化管理;
  • 容器重建后修改丢失;
  • 多环境难以保持一致;
  • 不利于自动化部署。

推荐做法

生产环境中应坚持以下原则:

  1. 所有依赖写入 Dockerfile
  2. 所有配置通过环境变量、配置文件挂载或配置中心管理;
  3. 所有变更通过 CI/CD 构建新镜像;
  4. 容器异常时直接重建,而不是进入容器“修机器”。

例如:

FROM openjdk:17-jdk-slim

WORKDIR /app

COPY target/app.jar /app/app.jar

ENV JAVA_OPTS="-Xms512m -Xmx512m"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

这种方式虽然前期规范成本更高,但长期维护成本会明显降低。


二、镜像不要越做越大

生产环境中非常常见的一个问题是镜像体积过大。一个普通 Java 服务镜像动辄 1GB,Python 服务镜像几百 MB,Node.js 镜像包含完整源码、开发依赖、缓存文件。这不仅会拖慢构建速度,还会影响部署效率,占用仓库和节点磁盘空间。

镜像过大的危害

  • 拉取镜像慢,影响发布速度;
  • 节点磁盘占用大;
  • 镜像仓库存储成本高;
  • 安全扫描范围扩大;
  • 包含不必要工具,增加攻击面。

避坑建议

1. 使用精简基础镜像

尽量避免使用完整系统镜像,例如:

FROM ubuntu:latest

如果没有必要,不建议直接使用这种基础镜像。可以选择:

FROM alpine

或者:

FROM debian:bookworm-slim

Java 应用可以选择:

FROM eclipse-temurin:17-jre

而不是 JDK 完整环境。

2. 使用多阶段构建

以 Go 应用为例:

FROM golang:1.22 AS builder

WORKDIR /src
COPY . .
RUN go build -o app main.go

FROM alpine:3.19

WORKDIR /app
COPY --from=builder /src/app /app/app

ENTRYPOINT ["/app/app"]

构建阶段使用完整编译环境,运行阶段只保留最终产物。

3. 清理缓存文件

对于 Debian/Ubuntu 系镜像:

RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

对于 Node.js:

RUN npm ci --omit=dev && npm cache clean --force

4. 正确使用 .dockerignore

很多团队忽略了 .dockerignore,导致 .git、日志、临时文件、测试数据全部被打进镜像上下文。

推荐示例:

.git
node_modules
target
logs
*.log
.env
.DS_Store
.idea
.vscode

.dockerignore 对构建速度和镜像安全都有明显帮助。


三、不要使用 latest 标签部署生产环境

latest 是 Docker 中最容易被误用的标签之一。很多团队为了方便,在 docker-compose.yml 或 Kubernetes YAML 中直接使用:

image: my-app:latest

看起来简单,但在生产环境中风险很大。

为什么不建议使用 latest

latest 并不代表“最新稳定版本”,它只是一个普通标签。不同时间拉取同一个 latest,得到的镜像内容可能完全不同。这会带来几个严重问题:

  • 无法准确回滚;
  • 发布版本不可追踪;
  • 多节点镜像可能不一致;
  • 排查问题时无法确认运行版本;
  • 容易出现“我本地没问题,线上镜像不一样”的情况。

推荐做法

生产环境镜像标签应明确绑定版本,例如:

my-app:v1.8.3
my-app:20240609-1530
my-app:git-a8f3c21

更推荐使用 Git Commit ID 或语义化版本加构建号:

my-app:1.8.3-a8f3c21

CI/CD 流程中也应将版本号写入应用启动日志,方便定位:

App Version: 1.8.3
Git Commit: a8f3c21
Build Time: 2024-06-09 15:30:00

生产环境部署时,尽量使用不可变标签,必要时甚至可以使用镜像摘要:

my-app@sha256:xxxxxxxx

四、容器内不要保存关键数据

Docker 容器的文件系统是临时性的。容器删除后,容器内写入的数据也会随之消失。生产环境中,最危险的误区之一就是把数据库、上传文件、业务日志等关键数据直接写在容器内部。

错误示例

docker run -d --name mysql mysql:8

这个命令可以启动 MySQL,但如果没有挂载数据卷,一旦容器删除,数据就可能无法恢复。

正确方式:使用数据卷

docker run -d \
  --name mysql \
  -v mysql_data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=yourpassword \
  mysql:8

或者挂载宿主机目录:

docker run -d \
  --name app \
  -v /data/app/uploads:/app/uploads \
  my-app:1.0.0

数据持久化注意事项

  1. 数据目录必须纳入备份策略;
  2. 数据卷权限要提前规划;
  3. 不要随意删除 volume;
  4. 数据库容器升级前必须备份;
  5. 宿主机目录应使用绝对路径;
  6. 多容器共享目录时注意并发写入问题。

查看数据卷:

docker volume ls

查看未使用的数据卷:

docker volume ls -f dangling=true

清理数据卷时一定要谨慎:

docker volume prune

这个命令可能删除未被容器使用但仍然有价值的数据卷,生产环境不建议随意执行。


五、日志不限制,磁盘迟早爆

生产环境中 Docker 最常见的事故之一就是:容器日志撑爆磁盘

默认情况下,Docker 使用 json-file 日志驱动。如果没有配置日志大小限制,容器标准输出和标准错误会持续写入宿主机磁盘。高并发服务、异常堆栈刷屏、调试日志未关闭,都可能导致磁盘迅速耗尽。

查看容器日志文件位置

docker inspect 容器ID | grep LogPath

通常路径类似:

/var/lib/docker/containers//-json.log

推荐配置日志轮转

可以在 /etc/docker/daemon.json 中配置:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}

修改后重启 Docker:

systemctl restart docker

注意:该配置通常只对新创建的容器生效,已有容器需要重新创建。

docker run 单独配置

docker run -d \
  --log-driver=json-file \
  --log-opt max-size=100m \
  --log-opt max-file=3 \
  my-app:1.0.0

生产建议

  • 应用日志优先输出到 stdout/stderr;
  • 使用日志采集系统统一收集,例如 ELK、Loki、Filebeat;
  • 禁止生产环境长期开启 DEBUG 日志;
  • 对异常堆栈刷屏设置告警;
  • 监控 /var/lib/docker 所在磁盘容量。

六、不要忽略资源限制

容器默认并不会自动限制 CPU 和内存。如果不设置资源限制,一个异常容器可能吃满整台机器资源,导致其他服务被拖垮。

常见问题

  • Java 服务内存泄漏,宿主机 OOM;
  • 批处理任务占满 CPU;
  • 容器疯狂创建线程;
  • 单个服务拖垮整台节点;
  • 数据库容器与应用容器抢资源。

设置内存限制

docker run -d \
  --memory=1g \
  --memory-swap=1g \
  my-app:1.0.0

设置 CPU 限制

docker run -d \
  --cpus=2 \
  my-app:1.0.0

或者:

docker run -d \
  --cpu-shares=512 \
  my-app:1.0.0

Java 容器特别注意

Java 应用在容器环境中一定要正确设置 JVM 参数。虽然新版本 JDK 已经能识别容器限制,但生产环境仍建议明确配置:

JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"

或者固定堆大小:

JAVA_OPTS="-Xms512m -Xmx512m"

如果容器限制 1GB,但 JVM 堆设置为 1GB,再加上 Metaspace、线程栈、Direct Memory,很容易被 OOM Kill。

排查 OOM

查看容器是否被 OOM:

docker inspect 容器ID | grep -i oom

查看系统日志:

dmesg | grep -i "killed process"

七、容器时间和时区要统一

生产环境中,时区问题很容易引发日志混乱、定时任务异常、数据统计偏差。很多基础镜像默认使用 UTC,而业务系统可能使用 Asia/Shanghai。

常见现象

  • 应用日志时间比实际时间少 8 小时;
  • 定时任务执行时间不对;
  • 数据库写入时间与业务系统不一致;
  • 排查问题时无法对齐日志。

推荐方式

方式一:设置环境变量

docker run -d \
  -e TZ=Asia/Shanghai \
  my-app:1.0.0

方式二:挂载宿主机时区文件

docker run -d \
  -v /etc/localtime:/etc/localtime:ro \
  -v /etc/timezone:/etc/timezone:ro \
  my-app:1.0.0

方式三:镜像内安装 tzdata

RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai

生产环境建议统一规范:所有服务到底使用 UTC 还是本地时区,应提前约定。大型系统更推荐内部存储使用 UTC,展示层按用户时区转换。


八、容器网络不要随便使用 host 模式

Docker 支持多种网络模式,包括 bridge、host、none、自定义网络等。有些团队为了省事,直接使用 --network host。虽然 host 模式性能略好、端口访问简单,但它会降低隔离性,增加端口冲突和安全风险。

host 模式的问题

  • 容器直接使用宿主机网络栈;
  • 端口冲突更难管理;
  • 服务暴露范围扩大;
  • 网络隔离能力下降;
  • 多容器部署不灵活。

推荐使用自定义 bridge 网络

docker network create app-net

启动容器:

docker run -d --name redis --network app-net redis:7
docker run -d --name app --network app-net my-app:1.0.0

此时应用可以通过容器名访问 Redis:

redis:6379

这比使用宿主机 IP 更稳定。

端口暴露要最小化

不要把所有端口都映射出来:

docker run -p 0.0.0.0:6379:6379 redis

如果 Redis、MySQL 等只供本机或内网访问,应限制绑定地址:

docker run -p 127.0.0.1:6379:6379 redis

对外服务建议前面增加网关、负载均衡或反向代理。


九、不要在容器里运行 SSH 服务

有些团队喜欢在容器中安装 SSH,方便登录维护。生产环境中不推荐这样做。

为什么不建议

  • 增加攻击面;
  • 违背单进程模型;
  • 用户权限难以管理;
  • 容器应该通过镜像构建和日志排查维护;
  • SSH 登录后的手工修改不可追踪。

推荐方式

临时进入容器调试:

docker exec -it container-name sh

或:

docker exec -it container-name bash

如果需要查看日志:

docker logs -f container-name

如果需要拷贝文件:

docker cp container-name:/app/logs/error.log ./error.log

但要注意,docker exec 也不应成为常规运维手段,只适合临时排查。


十、健康检查不能省

容器进程存在,不代表服务可用。例如 Java 进程还在,但线程池耗尽;Nginx 还在,但后端连接失败;应用启动了,但数据库连接池初始化失败。

因此生产环境需要配置健康检查。

Dockerfile 中配置 HEALTHCHECK

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

docker-compose 示例

services:
  app:
    image: my-app:1.0.0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

健康检查接口建议

健康检查接口不要只返回固定字符串,而应至少检查:

  • 应用是否完成启动;
  • 数据库连接是否正常;
  • Redis 等关键依赖是否可用;
  • 队列消费是否正常;
  • 磁盘空间是否充足。

当然,也要避免健康检查过重。检查逻辑太复杂,反而可能给系统带来额外压力。


十一、重启策略要配置,但不能依赖它掩盖问题

Docker 支持容器自动重启策略。如果不配置,服务异常退出后可能不会自动恢复。

常见重启策略

docker run -d --restart=always my-app:1.0.0

或:

docker run -d --restart=unless-stopped my-app:1.0.0

区别:

  • always:Docker 启动时总是尝试拉起容器;
  • unless-stopped:除非用户手动停止,否则自动拉起;
  • on-failure:只有非 0 退出码时重启。

生产环境比较常用:

--restart=unless-stopped

注意事项

重启策略只能提高可用性,不能替代故障排查。如果容器反复崩溃重启,必须查看根因:

docker ps -a
docker logs --tail=200 container-name
docker inspect container-name

对于频繁重启的服务,应接入告警,而不是让它在后台无限循环。


十二、权限问题:不要长期使用 root 运行应用

默认情况下,很多镜像中的进程以 root 用户运行。这样虽然方便,但一旦应用被攻击,攻击者可能获得更高权限,带来安全风险。

Dockerfile 中创建普通用户

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

CMD ["node", "server.js"]

文件权限要匹配

如果挂载宿主机目录,注意容器用户是否有读写权限。常见错误:

Permission denied

可以通过提前设置 UID/GID 解决:

chown -R 1000:1000 /data/app

安全建议

  • 尽量使用非 root 用户运行;
  • 限制容器 capabilities;
  • 文件系统尽量只读;
  • 不挂载敏感宿主机目录;
  • 不使用 --privileged,除非非常明确需要。

例如:

docker run -d \
  --read-only \
  --cap-drop=ALL \
  my-app:1.0.0

实际生产中可以根据应用需求逐步收紧权限。


十三、慎用 --privileged

--privileged 会赋予容器几乎等同于宿主机的权限。它会绕过很多隔离机制,让容器拥有访问设备、修改内核参数等能力。

危险示例

docker run -d --privileged some-app

除非你非常清楚为什么需要它,否则不要在生产环境使用。

替代方案

如果只是需要某个 Linux capability,可以单独添加:

docker run --cap-add=NET_ADMIN my-app

如果只是需要访问某个设备,可以使用:

docker run --device=/dev/snd my-app

遵循最小权限原则:需要什么给什么,不要一把梭。


十四、宿主机磁盘规划非常重要

Docker 默认数据目录通常在:

/var/lib/docker

如果系统盘空间较小,而镜像、容器、日志、volume 都放在这里,生产环境很容易出现磁盘爆满。

查看 Docker 占用

docker system df

查看详细信息:

docker system df -v

清理无用资源

docker image prune
docker container prune
docker network prune

慎用:

docker system prune -a

它会删除未使用的镜像、容器、网络等资源,可能影响回滚。

修改 Docker 数据目录

可以在 /etc/docker/daemon.json 中配置:

{
  "data-root": "/data/docker"
}

然后迁移数据并重启 Docker。生产环境迁移前一定要停机评估和备份。

建议

  • Docker 数据目录使用独立磁盘;
  • 日志目录纳入监控;
  • 镜像定期清理;
  • 不要让业务数据和系统盘混放;
  • 设置磁盘使用率告警,例如超过 80% 告警。

十五、Docker Compose 生产可用,但要规范

很多中小型项目会使用 Docker Compose 部署生产环境。Compose 本身并非不能用于生产,但必须规范使用。

推荐配置示例

services:
  app:
    image: my-app:1.0.0
    container_name: my-app
    restart: unless-stopped
    environment:
      TZ: Asia/Shanghai
      SPRING_PROFILES_ACTIVE: prod
    ports:
      - "8080:8080"
    volumes:
      - /data/app/logs:/app/logs
    networks:
      - app-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 1G

networks:
  app-net:
    driver: bridge

注意

在非 Swarm 模式下,deploy.resources 对普通 docker compose up 不一定完全生效。可以使用 Compose 支持的字段或通过运行参数控制资源限制。不同 Compose 版本行为可能有差异,生产前必须验证。


十六、环境变量不要泄露敏感信息

很多团队喜欢把数据库密码、Token、密钥直接写在 docker-compose.yml 里:

environment:
  DB_PASSWORD: 123456

这样做非常危险,因为配置文件可能进入 Git 仓库,也可能被日志系统、CI/CD 平台或运维人员无意暴露。

更好的方式

  • 使用 .env 文件,并确保不提交到 Git;
  • 使用 Docker secrets;
  • 使用配置中心;
  • 使用云厂商 KMS;
  • CI/CD 注入敏感变量。

.gitignore 中应包含:

.env
*.secret

同时,生产环境不要使用弱密码,也不要将密码写入镜像。


十七、镜像安全扫描不能忽略

镜像中可能包含有漏洞的系统库、语言运行时、第三方依赖。如果长期不更新基础镜像,很容易积累高危漏洞。

推荐工具

  • Trivy;
  • Grype;
  • Docker Scout;
  • Clair;
  • Harbor 镜像扫描。

Trivy 示例:

trivy image my-app:1.0.0

建议

  • CI 阶段加入镜像扫描;
  • 高危漏洞阻断发布;
  • 定期更新基础镜像;
  • 不使用来源不明的镜像;
  • 尽量选择官方镜像或可信镜像;
  • 固定基础镜像版本,不盲目使用 latest。

十八、发布前必须验证镜像内容

生产事故中有一种很常见:镜像构建成功了,但配置错了、文件漏了、启动命令不对,部署后服务直接挂掉。

发布前检查清单

docker run --rm my-app:1.0.0 --version

检查镜像元信息:

docker inspect my-app:1.0.0

进入临时容器检查文件:

docker run --rm -it my-app:1.0.0 sh

本地或测试环境先运行:

docker run --rm -p 8080:8080 my-app:1.0.0

必查内容

  • 启动命令是否正确;
  • 配置文件是否存在;
  • 端口是否正确暴露;
  • 依赖文件是否完整;
  • 健康检查是否通过;
  • 应用版本号是否正确;
  • 镜像标签是否符合发布规范。

十九、容器监控不能只看 docker ps

docker ps 只能告诉你容器是否在运行,不能说明服务是否健康,也无法反映资源趋势。

基础命令

查看实时资源:

docker stats

查看容器状态:

docker inspect container-name

查看事件:

docker events

生产监控建议

至少监控以下指标:

  • CPU 使用率;
  • 内存使用率;
  • 网络流量;
  • 磁盘 IO;
  • 容器重启次数;
  • 容器健康状态;
  • 日志错误率;
  • Docker daemon 状态;
  • 宿主机磁盘容量;
  • 镜像拉取失败次数。

常见方案:

  • Prometheus + cAdvisor + Grafana;
  • Node Exporter;
  • Loki + Promtail;
  • ELK;
  • 云厂商容器监控服务。

二十、生产环境 Docker 使用检查清单

最后给出一份可直接参考的生产检查清单。

镜像构建

  • [ ] 使用固定基础镜像版本;
  • [ ] 不使用 latest 部署;
  • [ ] 配置 .dockerignore
  • [ ] 使用多阶段构建;
  • [ ] 清理构建缓存;
  • [ ] 镜像体积可控;
  • [ ] 镜像通过安全扫描;
  • [ ] 镜像标签包含版本或 Commit ID。

容器运行

  • [ ] 配置重启策略;
  • [ ] 设置 CPU 和内存限制;
  • [ ] 配置健康检查;
  • [ ] 设置正确时区;
  • [ ] 使用非 root 用户运行;
  • [ ] 不使用 --privileged
  • [ ] 不在容器中运行 SSH;
  • [ ] 不在容器内保存关键数据。

存储和日志

  • [ ] 业务数据挂载到 volume 或宿主机目录;
  • [ ] 数据目录纳入备份;
  • [ ] 配置日志轮转;
  • [ ] 监控 Docker 数据目录磁盘;
  • [ ] 定期清理无用镜像;
  • [ ] 谨慎执行 prune 命令。

网络和安全

  • [ ] 使用自定义网络;
  • [ ] 最小化端口暴露;
  • [ ] 敏感服务不直接暴露公网;
  • [ ] 密码和密钥不写入镜像;
  • [ ] 敏感环境变量安全管理;
  • [ ] 定期更新基础镜像;
  • [ ] 限制容器权限。

总结

Docker 的价值不在于“把服务装进容器”这么简单,而在于通过标准化镜像、自动化部署、环境一致性和资源隔离,让系统交付更加稳定可靠。但生产环境中的 Docker 使用,必须从一开始就建立规范。

真正稳定的 Docker 生产实践,通常具备以下特点:

  • 镜像可追踪;
  • 配置可管理;
  • 数据可持久;
  • 日志可控;
  • 资源有限制;
  • 故障可观测;
  • 权限最小化;
  • 发布可回滚。

如果只是把传统部署方式搬进容器,而不改变运维和交付思路,那么 Docker 反而可能制造新的复杂度。建议团队在引入 Docker 时,不仅关注“怎么启动容器”,更要关注“如何长期稳定运行、如何排查问题、如何安全升级、如何快速回滚”。

生产环境没有小问题。Docker 用得好,是效率工具;用得粗糙,就是事故放大器。希望这份避坑指南能帮助你在实际项目中少踩坑、少熬夜、多稳定上线。

目录结构
全文