Timezones in Docker containers

Timezones in Docker containers

While reviewing logs from various Docker containers I ran into an issue: some containers were running with the host's local timezone (CET), others with UTC. While debugging I even spotted a third timezone in another container. The reason: every developer can pin an arbitrary timezone in their Dockerfile in 1–2 lines — definitely not a best practice, but it happened in one of the containers I was using. The default for most containers is UTC:

$ docker run --rm debian date
Tue Mar 23 19:13:37 UTC 2021

To normalise all containers (and make logs easier to correlate), I settled on a combination of two measures.

Set the timezone via /etc/localtime

On all common Linux distributions, the /etc/localtime symlink drives the timezone. It points to the matching file under /usr/share/zoneinfo:

$ ls -lah /etc/localtime
lrwxrwxrwx 1 root root 33 Feb  4 20:23 /etc/localtime -> /usr/share/zoneinfo/Europe/Berlin

The simplest way to get the right timezone into a Docker container is to mount the host's /etc/localtime. Since the container shouldn't be able to change the file, we mount it read-only:

$ docker run -v /etc/localtime:/etc/localtime:ro --rm debian date
Tue Mar 23 20:13:37 CET 2021

Unfortunately the timezone set this way doesn't always take effect inside the container for every application:

$ docker run -v /etc/localtime:/etc/localtime:ro openjdk /bin/sh -c 'curl -s https://gist.githubusercontent.com/tldev-de/8a1c0247b59042dd4faa5c50b65d4754/raw/241697e9602ef79aac67a84b4594ef69290836c7/GetTimeZone.java > GetTimeZone.java && java GetTimeZone.java'

sun.util.calendar.ZoneInfo[id=“GMT”,offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]

In this minimal example we download a Java class via curl and run it inside the openjdk container. Java reports GMT as the timezone instead of CET.

Set the timezone via environment variable

There's a second knob: the TZ environment variable. Depending on the container, setting TZ can even replace the /etc/localtime mount: if the tzdata package is installed, the environment variable is enough:

$ docker run -e TZ=Europe/Berlin openjdk /bin/sh -c 'curl -s https://gist.githubusercontent.com/tldev-de/8a1c0247b59042dd4faa5c50b65d4754/raw/241697e9602ef79aac67a84b4594ef69290836c7/GetTimeZone.java > GetTimeZone.java && java GetTimeZone.java'

sun.util.calendar.ZoneInfo[id=“Europe/Berlin”,offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]

Conclusion

Personally, I went with belt-and-braces and combine both approaches. All of my currently running containers have the same timezone configured:

$ docker run -e TZ=Europe/Berlin -v /etc/localtime:/etc/localtime:ro --rm debian date
Tue Mar 23 20:13:37 CET 2021

$ docker run -v /etc/localtime:/etc/localtime:ro -e TZ=Europe/Berlin openjdk /bin/sh -c ‘curl -s https://gist.githubusercontent.com/tldev-de/8a1c0247b59042dd4faa5c50b65d4754/raw/241697e9602ef79aac67a84b4594ef69290836c7/GetTimeZone.java > GetTimeZone.java && java GetTimeZone.java’

sun.util.calendar.ZoneInfo[id=“Europe/Berlin”,offset=3600000,dstSavings=3600000,useDaylight=true,transitions=143,lastRule=java.util.SimpleTimeZone[id=Europe/Berlin,offset=3600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=2,startMonth=2,startDay=-1,startDayOfWeek=1,startTime=3600000,startTimeMode=2,endMode=2,endMonth=9,endDay=-1,endDayOfWeek=1,endTime=3600000,endTimeMode=2]]