Docker 上生产后才懂的 20 个坑:从镜像、日志到数据安全
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
这种方式看起来很方便,但问题很多:
- 修改过程无法追溯;
- 配置无法版本化管理;
- 容器重建后修改丢失;
- 多环境难以保持一致;
- 不利于自动化部署。
推荐做法
生产环境中应坚持以下原则:
- 所有依赖写入
Dockerfile; - 所有配置通过环境变量、配置文件挂载或配置中心管理;
- 所有变更通过 CI/CD 构建新镜像;
- 容器异常时直接重建,而不是进入容器“修机器”。
例如:
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
数据持久化注意事项
- 数据目录必须纳入备份策略;
- 数据卷权限要提前规划;
- 不要随意删除 volume;
- 数据库容器升级前必须备份;
- 宿主机目录应使用绝对路径;
- 多容器共享目录时注意并发写入问题。
查看数据卷:
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 用得好,是效率工具;用得粗糙,就是事故放大器。希望这份避坑指南能帮助你在实际项目中少踩坑、少熬夜、多稳定上线。