目录

Docker容器时间如何与宿主机同步问题解决方案

容器与宿主机相差 8 小时的根因分析与五种时区同步方案

容器默认时区是 UTC(Coordinated Universal Time,协调世界时),而宿主机在国内一般配置为 Asia/Shanghai,两者正好相差 8 小时——这是大多数 Java/Python/Go 应用日志时间、定时任务(cron)触发时间、数据库时间戳对不上的根本原因。本文先讲清楚根因,再给出五种常见的同步方案及取舍。

常见的现象包括:

  • 容器内 date 输出比宿主机慢 8 小时
  • 应用日志的时间戳整体错位 8 小时
  • MySQL/PostgreSQL 写入的 CURRENT_TIMESTAMP 与业务预期不符
  • Spring @Scheduled、Celery beat 等定时任务在错误的时间点触发

根因在于: Docker 容器与宿主机共享同一个内核,所以系统时钟(wall clock,即"墙上时间")是一致的——宿主机和容器里执行 date -u(UTC 时间)输出完全相同。真正的差异不在时钟,而在时区文件不在容器的 rootfs 里:容器的基础镜像(alpine、debian、ubuntu 等)默认未安装或未配置 tzdata,因此容器内的时区回退到 UTC,而宿主机在国内通常配置为 Asia/Shanghai

所以严格来说,“时间不同步” 这个说法并不准确——真正的差异是时区(timezone)不一致,而不是时钟漂移。这也意味着 ntpchrony 这类校时工具在此场景下不会解决问题。

在动手之前,先理清三个容易混淆的文件和变量。

/usr/share/zoneinfo 是 tzdata 包提供的时区数据库目录,里面是 TZif(Time Zone Information Format,RFC 9636)格式的二进制文件,例如 /usr/share/zoneinfo/Asia/Shanghai

/etc/localtime系统当前生效的时区文件,通常是一个指向 /usr/share/zoneinfo/<Region>/<City> 的软链接,也可以是直接拷贝的二进制文件。C 库的 localtime(3) 函数读取它来把 UTC 时间换算成本地时间。

/etc/timezone 是一个纯文本文件,里面只有一行时区名(如 Asia/Shanghai)。它在 Debian/Ubuntu 系发行版中由 dpkg-reconfigure tzdata 维护,少数老程序会读取它;Alpine 和 CentOS 一般不依赖它。

TZ 是 POSIX 标准定义的环境变量,glibc、Java、Python、Go 等运行时都会读取它。只要 TZ 被设置且系统安装了 tzdata,它就优先于 /etc/localtime 生效TZ=Asia/Shanghai 是跨发行版最通用的设置方式。

最简单、改动最小的方式,适合不希望修改镜像的场景:

docker run -d \
  --name mysql \
  -p 3306:3306 \
  -v /etc/localtime:/etc/localtime:ro \
  mysql:8

注意几点:

  • :ro(只读) 是安全实践——防止容器内进程意外或被攻破后篡改宿主机时区文件。
  • 如果应用依赖 /etc/timezone(少数 Debian 老程序),可以一并挂载: -v /etc/timezone:/etc/timezone:ro
  • 这种方式把容器和宿主机绑定,换一台时区不同的宿主机(比如部署到海外云)行为会变化,可移植性略差。

最干净、最可移植的方式,前提是镜像里已经装了 tzdata:

docker run -d --name app -e TZ=Asia/Shanghai my-image:latest

对于没有安装 tzdata 的最小镜像(如 alpinedebian:slim),单设 TZ 可能不生效——因为 C 库找不到对应的时区数据。此时要么换用方案三在镜像里预装 tzdata,要么同时挂载 /etc/localtime

把时区固化进镜像,部署时无需任何额外参数,最适合生产环境。

ENV TZ=Asia/Shanghai
RUN apt-get update \
 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata \
 && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
 && echo $TZ > /etc/timezone \
 && rm -rf /var/lib/apt/lists/*

DEBIAN_FRONTEND=noninteractive 是必须的——否则 apt-get install tzdata 会进入交互式地区选择界面,导致构建卡住。建议把它放在单条 RUN 里临时生效,不要 ENV 全局持久化,以免掩盖后续排错时的其他交互提示。

ENV TZ=Asia/Shanghai
RUN apk add --no-cache tzdata \
 && cp /usr/share/zoneinfo/$TZ /etc/localtime \
 && echo $TZ > /etc/timezone

如果对镜像体积敏感,可以在拷贝 /etc/localtimeapk del tzdata,节省约 1.5 MB;但要留意 Go 的 time 包等运行时需要完整 zoneinfo 数据库才能解析所有时区,删除前确认应用不依赖。

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
 && echo $TZ > /etc/timezone

CentOS 系基础镜像默认已包含 tzdata,无需额外安装。

最推荐用 TZ 环境变量,简洁且跨平台:

services:
  mysql:
    image: mysql:8
    environment:
      TZ: Asia/Shanghai
    ports:
      - "3306:3306"

若镜像未预置 tzdata 又不想改镜像,可改用挂载方式:

services:
  app:
    image: my-app:latest
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

历史上也有人写过 SET_CONTAINER_TIMEZONE=true / CONTAINER_TIMEZONE=Asia/Shanghai 这类变量——但这些是某些早期镜像(如旧版 atlassian、jenkins 镜像)的自定义约定,并不是通用标准。如果用的是这些镜像,按其官方文档设置即可;其它情况一律优先用 TZ

容器已经跑起来、又不想重启时,可以临时同步——但这只是救急手段,容器重建后会丢失,事后务必落到镜像或 compose 配置:

# 方法 1: 从宿主机拷贝时区文件到容器(-L 解引用软链接)
docker cp -L /usr/share/zoneinfo/Asia/Shanghai <容器名或ID>:/etc/localtime
docker cp /etc/timezone <容器名或ID>:/etc/timezone

# 方法 2: 进入容器手动建立软链
docker exec -it <容器名或ID> sh -c \
  'ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone'

docker cp -L-L 会跟随软链接拷贝真实内容——宿主机上 /etc/localtime 本身就是软链,不加 -L 会拷一个失效的链接进去。

早期资料常把宿主机时间简称为 “CST(China Shanghai Time,东八区时间)"——这个说法不准确。CST 是一个有歧义的三字母缩写,根据上下文可能是:

  • China Standard Time,中国标准时间,UTC+8
  • (U.S.) Central Standard Time,北美中部标准时间,UTC−6
  • Cuba Standard Time,古巴标准时间,UTC−4

更麻烦的是,Java 的 TimeZone.getTimeZone("CST") 默认解析为美国中部时间,且三字母 ID 在 JDK 中已 @Deprecated——所以配置文件、代码、环境变量里千万不要用 CST 作为时区标识,统一使用 IANA 完整 ID Asia/Shanghai

顺带一提: IANA 时区数据库里中国标准时间的标识是 Asia/Shanghai 而不是 Asia/Beijing——数据库的命名约定是选取该时区内人口最多的城市,而上海是命名时(约 1980 年代末)该时区人口最多的城市。

按优先级排序:

  1. 镜像层面: 在 Dockerfile 里 ENV TZ=Asia/Shanghai 并安装 tzdata,固化到镜像里。最干净,跨主机部署一致。
  2. 编排层面: docker-compose / Kubernetes Deployment 通过 TZ 环境变量覆盖,灵活、便于按环境差异化配置。
  3. 临时修复: 用 docker cp 拷贝时区文件救急,事后务必落到镜像或 compose 配置。
  4. 挂载方式: -v /etc/localtime:/etc/localtime:ro 简单但与宿主机耦合,慎用于需要跨地域迁移的服务。

值得一提的是行业最佳实践: 容器内一律存 UTC,展示层(前端 / 接口)按用户时区换算。容器里强制设 Asia/Shanghai 是国内单地域小团队为减少心智负担的折中——规模一上来、业务跨时区后,还是要回归 UTC。

相关内容