
容器里 date 一看是 UTC 时间比北京时间慢 8 小时——这大概是每个国内 Docker 用户都会撞上的问题。表现五花八门日志时间戳对不上、定时任务凌晨跑成下午、数据库存的时间查出来差 8 小时、JWT 过期判断错乱。这篇把时区问题拆开讲为什么容器默认是 UTC、四种解法分别怎么用、各自的坑在哪、以及 Java / MySQL / Node 这些运行时各自的额外处理。为什么容器时间是 UTC容器共享宿主机内核但文件系统是隔离的。时区信息来自 /etc/localtime 和 /etc/timezone 这两个文件容器镜像里默认不带或者指向 UTC所以容器读到的就是 UTC跟宿主机设了什么时区无关。验证一下date # 宿主机2026-09-24 15:30:00 CST docker run --rm alpine date # 容器2026-09-24 07:30:00 UTC差了 8 小时就是这个原因。解法一环境变量 TZ最简单推荐首选启动容器时传 TZ 环境变量docker run -d --name app -e TZAsia/Shanghai myapp:1.0Compose 里services: app: image: myapp:1.0 environment: - TZAsia/Shanghai原理大多数基础镜像debian/ubuntu 系的 libc 会读 TZ 环境变量来决定时区。坑Alpine 镜像不认 TZ因为它用的是 musl libc默认不带时区数据库。Alpine 上要额外装 tzdataFROM alpine:3.20 RUN apk add --no-cache tzdata ENV TZAsia/Shanghai或者在 docker run 时挂载见解法二。如果你用 alpine 又发现 TZ 不生效十有八九是缺 tzdata。解法二挂载宿主机时区文件对 alpine 也有效把宿主机的 /etc/localtime 只读挂载进容器docker run -d --name app \ -v /etc/localtime:/etc/localtime:ro \ myapp:1.0Composeservices: app: image: myapp:1.0 volumes: - /etc/localtime:/etc/localtime:ro优点不依赖镜像里有没有 tzdataalpine 也生效宿主机改时区容器跟着变。坑Windows / macOS 的 Docker Desktop 里宿主机 /etc/localtime 不一定存在或行为不同这种环境优先用解法一的 TZ。另外 :ro 只读一定要加防止容器误改宿主机时区。解法三Dockerfile 里固化镜像层面统一如果整个团队镜像都想统一时区直接在 Dockerfile 里写死避免每个 run 都记得传参数Debian/Ubuntu 系FROM eclipse-temurin:17-jre-jammy ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezoneAlpine 系FROM alpine:3.20 RUN apk add --no-cache tzdata ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone优点镜像自带正确时区谁拉起来都对不依赖运行时参数。坑时区烧进镜像后如果将来要部署到其他时区比如出海业务得重新构建。多时区部署场景反而推荐解法一/二在运行时注入。解法四daemon.json 全局默认一劳永逸但有局限有人想给所有容器统一设时区。Docker 本身没有全局时区配置项但可以通过给所有容器注入环境变量的方式间接实现——实际上更常见的做法是在 Compose 或 K8s 层面统一加 TZ。如果一定要 daemon 级别可以用 docker run 的默认环境变量文件部分发行版支持但可移植性差不推荐。团队场景建议在 Compose 模板或 K8s 的 pod spec 里统一加 TZ比 daemon 配置更可控。各运行时的额外处理设了系统时区不等于万事大吉有些运行时/组件有自己的时区逻辑JavaJVM 默认读系统时区一般设了 TZ 就够。但如果代码里显式用了 TimeZone.getDefault() 且启动参数带了 -Duser.timezone以参数为准。保险起见可以加java -Duser.timezoneAsia/Shanghai -jar app.jarMySQLMySQL 有自己的时区变量跟系统时区独立。容器里设了 TZ 后MySQL 的 system_time_zone 会跟随但连接时区time_zone 变量默认是 SYSTEM通常没问题。如果存的时间还是差 8 小时检查 JDBC 连接串有没有加 serverTimezonejdbc:mysql://host:3306/db?serverTimezoneAsia/ShanghaiMySQL 8 的 JDBC 驱动如果检测不到时区会报错或回退 UTC显式写 serverTimezone 最稳。也可以在 MySQL 配置里设[mysqld] default-time-zone 08:00RedisRedis 本身不存时区存的是时间戳或字符串不受影响。但 RDB/AOF 里的时间戳是 UTC epoch展示层转换时才涉及时区。Node.jsNode 读系统时区设 TZ 即可。但注意 Node 的 TZ 需要在进程启动前设置容器里通过 environment 传入是 OK 的。日志框架Logback / Log4j2 输出时间戳用 JVM 时区JVM 时区对了日志就对。如果用了 JSON 日志且带 timestamp确认采集端如 Filebeat/ES的时区转换配置避免二次偏移。验证时区是否生效改完一定要验证别只看 date# 系统层 docker exec app date # 期望... CST或 0800 # 看 TZ 是否注入 docker exec app env | grep TZ # alpine 确认 tzdata 在不在 docker exec app ls /usr/share/zoneinfo/Asia/Shanghai # Java 应用层 docker exec app java -XshowSettings:properties -version 21 | grep timezone # 期望user.timezone Asia/Shanghai # MySQL 层 docker exec mysql mysql -uroot -p -e SELECT global.time_zone, session.time_zone, NOW();四层都对上时区问题才算真正解决。经常遇到的情况是系统层对了但 MySQL 连接层还是 UTC查的时候逐层排查。定时任务跑错点的排查时区问题最坑的表现是 cron / 定时任务跑错时间。比如你设了每天 8 点跑结果下午 4 点才跑或凌晨跑。排查思路确认任务调度器跑在哪个时区。如果 cron 跑在容器里看容器时区如果是宿主机 crontab 调 docker exec看宿主机时区确认应用内定时器如 Quartz、Spring Scheduled用的时区。Spring 默认用 JVM 时区JVM 时区对了就对确认表达式解析时区。某些调度框架如 Quartz 的 cron可以单独指定时区检查配置一个真实案例容器没设 TZUTCSpring Scheduled(cron 0 0 8 * * ?) 按 UTC 8 点触发换算成北京时间是下午 4 点。给容器加 -e TZAsia/Shanghai 后重建容器恢复正常。小结容器时区问题根源是镜像不带时区文件默认 UTC。四种解法按场景选单个容器 / Compose-e TZAsia/Shanghaialpine 记得装 tzdata需要跟宿主机一致或 alpine 不想装 tzdata挂载 /etc/localtime:ro团队镜像统一Dockerfile 里 ENV TZ 软链 zoneinfo多时区部署运行时注入别烧进镜像设完系统时区还要检查 Java 的 user.timezone、MySQL 的 serverTimezone / default-time-zone、日志采集端的时区转换逐层验证才算闭环。定时任务跑错点优先怀疑时区。