先说总结
设置docker时区正确姿势。将下面内容写在Dockerfile里面。(有些alpine镜像可能需要手动安装tzdata)
ENV TZ='Asia/Shanghai'
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo $TZ > /etc/timezone
问题产生
开发这边需要一个tomcat9的镜像,我从官方拉了一个最新的tomcat:9-jdk8
,并修改了时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
但是开发在使用的时候发现date
命令显示的时间是对的,但是tomcat日志里面时间还是错的:

于是问题来了,需要查询原因。
排查过程
首先TZ环境变量
因为网上搜索docker时区,大部分人都会修改这个,于是我测试了下
~]# docker run --rm -it -e TZ='Asia/Shanghai' tomcat:9-jdk8 bash
然后就发现时区正常了

那么为什么是TZ环境变量
通过百度,我查询到了TZ环境变量和/etc/timezone有关,通过man手册,发现:
TZ环境变量优先级高于系统配置文件/etc/timezone
,如果TZ变量设置且生效,那么/etc/timezone就不会加载
~]$ man timezone | cat
...
TZ If this variable is set its value takes precedence over the system configured timezone.
所以问题就变成了 /etc/localtime
和/etc/timezone
有什么区别?
localtime和timezone
关于它俩的区别,网上大佬已经写的很清楚了。我通过google查得的答案也是一样的。
# 参考:https://blog.csdn.net/kq1983/article/details/89913861
/etc/localtime是用来描述本机时间,而 /etc/timezone是用来描述本机所属的时区
在linux中,有一些程序会自己计算时间,不会直接采用带有时区的本机时间格式,会根据UTC时间和本机所属的时区等计算出当前的时间。
比如jdk应用,时区为“Etc/UTC”,本机时间改为北京时间,通过java代码中new 出来的时间还是utc时间,所以必须得修正本机的时区。
java与时区
有大佬研究了java获取时区这块的源码:http://kaiwangchen.github.io/2018/09/30/java-timezone-revisited.html
java.base/unix/native/libjava/TimeZone_md.c
可以看到,java程序会先读取TZ环境变量,如果变量为空,则去读取timezone文件
/*
* findJavaTZ_md() maps platform time zone ID to Java time zone ID
* using <java_home>/lib/tzmappings. If the TZ value is not found, it
* trys some libc implementation dependent mappings. If it still
* can't map to a Java time zone ID, it falls back to the GMT+/-hh:mm
* form.
*/
/*ARGSUSED1*/
char *
findJavaTZ_md(const char *java_home_dir)
{
char *tz;
char *javatz = NULL;
char *freetz = NULL;
tz = getenv("TZ");
if (tz == NULL || *tz == '\0') {
tz = getPlatformTimeZoneID();
freetz = tz;
}
// snipped
}
#if defined(__linux__) || defined(MACOSX)
/*
* Performs Linux specific mapping and returns a zone ID
* if found. Otherwise, NULL is returned.
*/
static char *
getPlatformTimeZoneID()
{
struct stat statbuf;
char *tz = NULL;
FILE *fp;
int fd;
char *buf;
size_t size;
int res;
#if defined(__linux__)
/*
* Try reading the /etc/timezone file for Debian distros. There's
* no spec of the file format available. This parsing assumes that
* there's one line of an Olson tzid followed by a '\n', no
* leading or trailing spaces, no comments.
*/
if ((fp = fopen(ETC_TIMEZONE_FILE, "r")) != NULL) {
char line[256];
if (fgets(line, sizeof(line), fp) != NULL) {
char *p = strchr(line, '\n');
if (p != NULL) {
*p = '\0';
}
if (strlen(line) > 0) {
tz = strdup(line);
}
}
(void) fclose(fp);
if (tz != NULL) {
return tz;
}
}
#endif /* defined(__linux__) */
/*
* Next, try /etc/localtime to find the zone ID.
*/
RESTARTABLE(lstat(DEFAULT_ZONEINFO_FILE, &statbuf), res);
if (res == -1) {
return NULL;
}
/*
* If it's a symlink, get the link name and its zone ID part. (The
* older versions of timeconfig created a symlink as described in
* the Red Hat man page. It was changed in 1999 to create a copy
* of a zoneinfo file. It's no longer possible to get the zone ID
* from /etc/localtime.)
*/
if (S_ISLNK(statbuf.st_mode)) {
char linkbuf[PATH_MAX+1];
int len;
if ((len = readlink(DEFAULT_ZONEINFO_FILE, linkbuf, sizeof(linkbuf)-1)) == -1) {
jio_fprintf(stderr, (const char *) "can't get a symlink of %s\n",
DEFAULT_ZONEINFO_FILE);
return NULL;
}
linkbuf[len] = '\0';
tz = getZoneName(linkbuf);
if (tz != NULL) {
tz = strdup(tz);
return tz;
}
}
/*
* If it's a regular file, we need to find out the same zoneinfo file
* that has been copied as /etc/localtime.
* If initial symbolic link resolution failed, we should treat target
* file as a regular file.
*/
RESTARTABLE(open(DEFAULT_ZONEINFO_FILE, O_RDONLY), fd);
if (fd == -1) {
return NULL;
}
RESTARTABLE(fstat(fd, &statbuf), res);
if (res == -1) {
(void) close(fd);
return NULL;
}
size = (size_t) statbuf.st_size;
buf = (char *) malloc(size);
if (buf == NULL) {
(void) close(fd);
return NULL;
}
RESTARTABLE(read(fd, buf, size), res);
if (res != (ssize_t) size) {
(void) close(fd);
free((void *) buf);
return NULL;
}
(void) close(fd);
tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
free((void *) buf);
return tz;
}
#elif defined(__solaris__)
总结
/etc/localtime
控制系统所在位置的时间,/etc/timezone
控制系统所在的位置
修改localtime不会影响timezone,但是修改timezone却会修改localtime
~]# docker run --rm -it -e TZ='Asia/Shanghai' tomcat:9-jdk8 bash
root@1d8059a42062:/usr/local/tomcat# date
Thu Oct 21 19:03:31 CST 2021
修改docker镜像时区时候既要修改localtime,又要修改timezone,保证万无一失
ENV TZ='Asia/Shanghai'
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo $TZ > /etc/timezone