Docker 容器日志管理实战:日志驱动、轮转与集中收集

很多用 Docker 部署网站的个人站长都会遇到同一个问题:服务器磁盘空间突然告警,一查发现 /var/lib/docker/containers 目录占了几十个 G,全是容器的 JSON 日志文件。Docker 容器默认会把应用输出到标准输出和标准错误的日志全部记录下来,如果不做任何限制,日志文件会无限增长,最终撑爆磁盘。本文从 Docker 日志的原理讲起,完整介绍日志驱动、日志轮转、日志查看和集中收集的实战方法。

一、Docker 日志是怎么产生的

Docker 容器内部运行的应用,只要把日志写入标准输出(stdout)和标准错误(stderr),Docker 就会自动捕获这些输出并交给日志驱动处理。注意这和传统方式不同:传统部署下应用把日志写到文件里,而容器化的最佳实践是应用只输出到标准输出,由 Docker 负责收集。这样做的好处是日志的采集和存储与应用解耦,换日志方案时不需要改应用代码。

默认情况下,Docker 使用 json-file 日志驱动,每个容器的日志会以 JSON 格式保存在宿主机上,路径是 /var/lib/docker/containers/容器ID/容器ID-json.log。每条日志是一行 JSON,包含 log 字段(实际内容)、stream 字段(stdout 还是 stderr)和时间戳。查看默认驱动可以执行 docker info 命令,在 Logging Driver 一栏可以看到当前配置。

二、日志轮转配置:max-size 与 max-file

json-file 驱动支持两个重要的日志轮转参数:max-size 控制单个日志文件的最大大小,max-file 控制日志文件的最大数量。当日志达到 max-size 时,Docker 会自动把当前文件改名并新建一个日志文件,最多保留 max-file 个文件,超过的旧文件会被自动删除。

推荐的配置方式是在 daemon.json 中全局设置,这样所有新创建的容器都会自动生效:

# /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

修改后执行 systemctl restart docker 重启 Docker 服务。这里要注意两点:第一,daemon.json 的修改只对之后创建的容器生效,已经存在的容器需要删除重建或者使用 docker update 更新;第二,docker update 可以修改运行中容器的日志参数,但只对之后新产生的日志文件生效:

# 修改运行中容器的日志轮转参数(需要 Docker 20.10+)
docker update --log-opt max-size=10m --log-opt max-file=5 mycontainer

如果没有提前配置轮转,日志文件已经很大了,可以先手动清空当前日志再让配置生效:

# 危险操作,先确认容器名字
truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log

注意不要直接 rm 删除日志文件,Docker 进程还持有文件句柄,直接删除会留下空洞导致磁盘空间无法释放,truncate 清空才是正确做法。

三、docker logs 命令实战

docker logs 是查看容器日志最常用的命令,常用参数如下:

# 查看最近 100 行日志
docker logs --tail 100 mycontainer

# 实时跟踪日志输出,类似 tail -f
docker logs -f mycontainer

# 查看最近 30 分钟的日志
docker logs --since 30m mycontainer

# 查看某个时间点之后的日志
docker logs --since "2026-08-01T10:00:00" mycontainer

# 带时间戳查看
docker logs -t mycontainer

# 只查看错误输出
docker logs 2>&1 mycontainer | grep ERROR

docker logs 有一个容易踩的坑:默认的 json-file 驱动下,docker logs 需要把整个日志文件读出来再过滤,当日志文件达到几个 G 时,命令会非常慢甚至卡死。解决办法有两个:一是配置好日志轮转,把单个文件控制在 10m 以内;二是使用 --since 和 --tail 参数限制读取范围,避免全量读取。

四、常用的日志驱动对比

除了默认的 json-file,Docker 还支持多种日志驱动,适合不同的场景:

json-file:默认驱动,无需额外配置,适合单机小规模部署,配合 max-size 和 max-file 使用即可满足个人站长的需求。

journald:把日志交给 systemd 的 journal 管理,可以通过 journalctl -u 容器名 查看,好处是日志和系统日志统一管理,但 journal 本身也需要限制大小,否则一样会占满磁盘。

syslog:把日志发送到本机或者远程的 syslog 服务,适合有统一日志服务器的场景。

gelf:发送到 Graylog 日志平台,适合需要图形化搜索和分析日志的场景。

fluentd:把日志发送给 Fluentd 收集器,再由 Fluentd 转发到 Elasticsearch、Loki 等存储后端,适合中型以上的日志架构。

切换驱动的配置同样写在 daemon.json 或者容器的启动参数里:

docker run -d \
  --log-driver=journald \
  --name myapp myimage:latest

这里有一个重要的限制要提醒:只有 json-file 和 journald 驱动支持 docker logs 命令,切换到 syslog、gelf 或者 fluentd 之后,docker logs 会提示不支持。所以个人站长如果还要用 docker logs 排障,建议保持 json-file 驱动,只把轮转配置做好。

五、集中式日志收集方案

当服务器上的容器越来越多,一台一台执行 docker logs 就太累了。此时可以引入集中式日志收集,比较流行的组合是 Loki 加 Promtail,或者 Elasticsearch 加 Filebeat。对于个人站长来说,Loki 方案更轻量,内存占用远小于 ELK,这里以 Promtail 为例说明收集思路:

# promtail 配置文件片段:收集所有容器的 json 日志
scrape_configs:
  - job_name: docker
    static_configs:
      - targets: [localhost]
        labels:
          job: docker
    pipeline_stages:
      - docker: {}
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s

Promtail 可以直接读取 Docker 的 json 日志文件并解析出容器名、镜像名等标签,配合 Loki 的 LogQL 查询语法,可以在 Web 界面里按容器、按时间范围快速检索日志,排查问题效率提升非常明显。如果暂时不想引入这么重的方案,也可以先用一个简单的定时任务把容器日志打包压缩归档,保证磁盘不爆:

# 每天凌晨 3 点把超过 50M 的容器日志压缩归档
0 3 * * * find /var/lib/docker/containers -name "*-json.log" -size +50M -exec gzip {} \;

六、应用日志的最佳实践

日志管理做得好不好,一半取决于 Docker 配置,另一半取决于应用怎么写日志。这里给出几条容器化应用日志的实战建议:

1. 日志只写标准输出。应用内部不要自己把日志写到文件里,直接输出到 stdout,让 Docker 统一收集。如果应用必须写文件(比如某些框架的框架日志),也要写到挂载的卷目录,并自行处理轮转,否则容器重建后日志丢失。

2. 区分日志级别。错误、警告、信息分别用不同的级别输出,方便后续用 grep 或者日志平台过滤。生产环境建议把日志级别设为 info,调试阶段再临时调到 debug。

3. 日志要结构化。输出的日志尽量是 key=value 或者 JSON 格式,包含时间、级别、请求 ID、耗时等字段,这样集中收集后检索和统计都很方便。

4. 定时清理无用的容器和镜像。docker system prune 可以清理停止的容器、悬空镜像和未使用的网络,但默认不会清日志,所以日志轮转配置始终是必须的。

七、日志占满磁盘的排查与清理

如果磁盘告警已经发生,按下面的步骤排查和清理:

# 1. 查看磁盘占用排行
du -sh /var/lib/docker/* | sort -rh

# 2. 找出最大的容器日志文件
find /var/lib/docker/containers -name "*-json.log" -size +100M -exec ls -lh {} \;

# 3. 确认是哪个容器
docker ps -a --no-trunc | grep 容器ID前12位

# 4. 清空日志文件(不要 rm)
truncate -s 0 /var/lib/docker/containers/容器ID/容器ID-json.log

# 5. 修复配置,防止再次发生
# 编辑 /etc/docker/daemon.json 加上 max-size 和 max-file 后重启 Docker

还可以用 docker system df 查看 Docker 整体的磁盘占用情况,包括镜像、容器、卷和构建缓存各自的占用。构建缓存(Build Cache)经常被忽略,积累久了能占好几个 G,可以用 docker builder prune 清理。

七、Docker Compose 中的日志配置

现在用 Docker Compose 部署网站是主流方式,日志配置同样可以写在 compose 文件里,和代码一起版本化管理。每个服务都可以单独指定日志驱动和轮转参数:

# docker-compose.yml 片段
services:
  web:
    image: nginx:stable
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
  app:
    image: myapp:latest
    logging:
      driver: json-file
      options:
        max-size: "5m"
        max-file: "2"

这样每个服务的日志策略一目了然:静态资源服务日志量大但价值低,可以保留少一点;业务应用日志要排障用,可以多留几份。执行 docker compose up -d 重建容器后配置即生效。如果 compose 文件里没有写 logging 配置,就会继承 daemon.json 的全局设置,所以两种方式至少要配一种,否则就是裸奔状态。

八、常见问题与踩坑记录

1. docker logs 输出乱码或者中文变成问号。这通常是容器内的应用没有设置 UTF-8 语言环境导致的,在 Dockerfile 里加上 ENV LANG=C.UTF-8 可以解决。另外如果日志里出现大量转义符,说明应用输出的是 ANSI 彩色日志,在 docker logs 里会看到一堆控制字符,可以关闭应用的颜色输出。

2. 健康检查产生大量日志。很多镜像默认带有健康检查,比如每 30 秒检查一次,每次检查都会输出一行日志,一天下来就是几千条无用记录。可以在 compose 里用 logging 的 driver 为 none 把健康检查容器的日志直接丢弃,或者调整健康检查的频率。

3. 容器日志时间不是本地时间。Docker 日志的时间戳默认是 UTC 时区,如果应用本身输出的是本地时间,两者对不上会干扰排障。建议统一约定:应用输出的日志自带北京时间,Docker 的时间戳只作为参考,或者在部署时把容器时区设置为 Asia/Shanghai。

4. 误删日志文件导致空间不释放。前面提过,不要用 rm 删除正在被 Docker 持有的日志文件,否则磁盘空间不会释放,而且 Docker 还会继续往已删除的文件里写数据。正确做法是 truncate -s 0 清空,或者干脆重建容器。如果已经误删了,重启 Docker 服务可以释放空间,但会影响所有容器,尽量安排在低峰期。

5. 日志量极大时 docker logs 卡死。某些高并发应用一天能产生几个 G 的日志,此时 docker logs 全量读取会非常慢。除了配置轮转,还可以把日志级别调高减少输出量,或者改用 tail 加 since 组合查询,避免一次读取整个文件。

九、总结

Docker 日志管理可以概括成四句话:应用只管往标准输出写日志,Docker 负责收集;daemon.json 里配置 max-size 和 max-file 做好轮转,从源头防止日志无限增长;日常排查用 docker logs 加 --tail 和 --since 参数;容器多了以后引入 Promtail 加 Loki 做集中收集。把这四步做扎实,个人站长的服务器就不会再被日志文件撑爆磁盘,排障效率也能提升一个档次。日志是运维里最不起眼却最容易出问题的环节,建议新服务器部署 Docker 的第一件事就是写好 daemon.json 的轮转配置,而不是等磁盘告警了再补救。

Last modification:September 1st, 2026 at 08:10 am

Leave a Comment