日志文件如果只进不出,再大的磁盘也会被慢慢耗尽。容器化部署让应用日志的落盘位置变得分散,传统在宿主机上配置 logrotate 的方式很难跟随服务一起迁移。我们可以借助 Docker 的卷共享能力,把日志归档做成一个独立的容器:应用容器只负责把日志写入指定目录,归档容器则定时扫描该目录,按天压缩打包,并清理过期文件。这样归档策略和业务容器解耦,也更容易在不同环境中复现。

一、容器化日志归档的整体设计
这套方案的核心是让多个容器共享同一个日志卷。假设我们有一个 Nginx 服务,它的访问日志和错误日志统一写到 /var/log/app 目录。我们在宿主机上创建一个命名卷,或者直接绑定挂载一个宿主目录,例如 ./logs:/var/log/app,然后将同一个目录再挂载到归档容器的 /logs 路径。这样归档容器就能看到应用容器刚写出的日志文件,而不需要通过网络传输或额外配置。
归档容器内部运行一个轻量级 Linux 发行版,比如 Alpine,并安装 cron 和 tar。cron 定时触发归档脚本,脚本会遍历日志目录,找到前一天产生的日志文件,将其打包成带日期的 tar.gz 文件,移动到归档目录。随后脚本会删除超过保留期限的归档包,避免归档目录无限增长。整个过程都在容器内完成,宿主机只需要保留一个数据目录。
这种设计有两个明显优点。第一,归档逻辑和业务镜像完全分离,更新归档策略时不需要重新构建业务镜像。第二,容器化后的归档服务可以随 Docker Compose 一起启动,新环境只需要一份 Compose 文件就能恢复完整的日志归档能力。它比在宿主机上写 crontab 更容易做版本管理和团队协作。
二、编写归档脚本与 Dockerfile
归档脚本是整套服务的执行核心。我们使用 Shell 编写一个简单脚本,逻辑分为三步:定位日志目录、按日期打包、清理过期归档。为了确保脚本在容器中稳定运行,需要显式指定路径和 shell。下面是一个可直接使用的示例,脚本中避免了复杂的条件判断,重点展示归档动作。
#!/bin/sh
set -e
LOG_DIR="/logs"
ARCHIVE_DIR="/archive"
DATE_STR=$(date -d "yesterday" +%Y%m%d)
KEEP_DAYS=30
mkdir -p "$ARCHIVE_DIR"
for file in "$LOG_DIR"/*.log; do
if [ -f "$file" ]; then
base=$(basename "$file")
tar -czf "$ARCHIVE_DIR/${base%.log}-${DATE_STR}.tar.gz" -C "$LOG_DIR" "$base"
fi
done
find "$ARCHIVE_DIR" -type f -mtime +${KEEP_DAYS} -delete
脚本中 date -d "yesterday" 是为了按自然日归档前一天的日志,避免在日志文件正在写入时直接打包导致截断。打包完成后,find 命令会删除修改时间超过 30 天的归档文件。这里使用 mtime 判断文件年龄,如果归档目录中还有其他类型文件,可以再用 -name "*.tar.gz" 进一步限定。需要注意的是,Alpine 镜像内置的 date 命令来自 BusyBox,它支持 -d 参数,但部分精简系统可能不支持,因此选择镜像时要确认。
接下来编写 Dockerfile。基础镜像使用 Alpine 是为了体积小,同时通过 apk 安装 bash 和 dcron。Alpine 默认没有 cron 服务,dcron 是一个轻量级替代方案。我们把归档脚本复制到镜像中,并配置 crontab 文件。
FROM alpine:3.20 RUN apk add --no-cache bash dcron COPY archive.sh /usr/local/bin/archive.sh RUN chmod +x /usr/local/bin/archive.sh COPY crontab.txt /etc/crontabs/root CMD ["crond", "-f", "-l", "2"]
crond 是 dcron 提供的守护进程,-f 表示前台运行,这样容器启动后不会立即退出。归档脚本被放在 /usr/local/bin 目录下,并赋予可执行权限。crontab 文件内容极简单,只需要一行:
0 2 * * * /usr/local/bin/archive.sh
这行配置表示每天凌晨 2 点执行归档脚本。容器内默认使用 UTC 时间,如果业务需要按本地时区归档,可以在 Dockerfile 中安装 tzdata 并设置 TZ 环境变量,或者接受 UTC 时间并在脚本中调整日期计算方式。
三、用 Docker Compose 编排日志归档服务
有了归档镜像后,下一步就是通过 Docker Compose 把应用容器和归档容器组合起来。Compose 文件需要定义两个服务:一个是模拟业务服务的 Nginx,另一个是归档服务。关键点在于两个服务挂载同一个卷,且归档容器需要挂载归档输出目录,以便在宿主机上查看归档结果。
version: "3.8"
services:
web:
image: nginx:1.27
volumes:
- ./logs:/var/log/app
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "8080:80"
archiver:
build: ./archiver
volumes:
- ./logs:/logs
- ./archive:/archive
restart: unless-stopped
在这个配置中,宿主机目录 ./logs 同时挂载到 Nginx 容器的 /var/log/app 和归档容器的 /logs。Nginx 需要调整配置,让 access_log 和 error_log 写到该目录。归档容器额外挂载 ./archive 到 /archive,用于保存压缩后的归档文件。这样做的好处是宿主机上可以直接查看 ./archive 目录内容,不需要进入容器。
需要说明的是,如果使用 Docker 命名卷而不是绑定挂载,共享方式类似,但宿主机路径由 Docker 管理。对于生产环境,建议使用绑定挂载并纳入备份策略,因为归档文件本身就是需要长期保存的数据。另外,如果多个应用容器都写日志到同一个目录,归档脚本需要根据文件名区分来源,本文示例只处理 *.log 文件,如果有子目录结构,可以进一步扩展。
归档服务的构建上下文是 ./archiver,需要在该目录下放置 Dockerfile、archive.sh 和 crontab.txt。启动时直接执行 docker compose up -d --build,归档容器会以守护进程方式运行。若想验证 cron 是否生效,可以查看容器日志。
四、验证归档效果与常见问题
服务启动后,可以先手动向 Nginx 发送几个请求,确保日志文件产生。然后进入归档容器,手动执行一次归档脚本,检查 /archive 目录中是否出现了当天或前一天的 tar.gz 文件。例如使用 docker compose exec archiver sh -c 'ls -lh /archive' 查看文件列表。如果脚本没有执行成功,优先检查挂载目录权限以及脚本中的换行符是否为 LF 格式。
docker compose exec web sh -c 'echo "test log line" >> /var/log/app/access.log' docker compose exec archiver sh -c '/usr/local/bin/archive.sh' docker compose exec archiver sh -c 'ls -lh /archive'
常见的坑包括:归档容器内 cron 没有启动,通常是因为基础镜像缺少 cron 或启动命令写错;脚本没有可执行权限,导致 cron 无法调用;挂载目录在 Windows 或 macOS 上存在性能差异,大量小文件时归档速度会下降。对于 Linux 服务器,这些问题较少出现,但仍建议在 CI 中增加一次手动触发归档的测试。
另一个需要关注的是归档文件的保留策略。本方案使用 find -mtime +30 清理文件,但归档文件的修改时间在创建后几乎不会再变化,因此用修改时间判断是可靠的。如果归档目录中还有其他用途的文件,建议加上 -name "*.tar.gz" 参数。若日志量很大,也可以把 tar -czf 改为 tar -cf 配合外部压缩工具,但 gzip 对文本日志的压缩率已经很高,大多数场景下足够使用。
最后,这套方案虽然轻量,但它的定位是中小规模的日志归档。对于分布式系统或日志量达到每天数百 GB 的场景,更适合引入 Loki、Elasticsearch 等集中式日志平台,配合对象存储做冷数据归档。不过对于单体应用、内部系统或边缘节点,本文的 Docker 归档方案已经可以稳定运行,并且维护成本非常低。