Timezone pitfall in Go and Docker

Published May 23, 2022

While working on a project I received a bug report that there is a 1 hour difference between dates at one part of my service and after an hour of trying to reproduce to issue, we came to the conclusion that the reason why it was so hard to track down was that our GKE cluster operates in the UTC timezone, while my local environment (and even our production cluster) is in the Europe/Budapest timezone.

I did what probably everyones first idea would be, add an environment variable to the service and configure the timezone of the service through that and that is exactly what I did.

// Get the timezone from the environment variable
timezone := os.Getenv("TIMEZONE")

// Get the *time.Location object corresponding to the timezone
tz, err := time.LoadLocation(timezone)
if err != nil {
	fmt.Printf("Error loading timezone from env variable. err={%s}", err.Error())
	return
}

// Set the global timezone variable to our *time.Location object
time.Local = tz

Problem

Thinking I just fixed the issue I pushed the changes and laid back in my chair, but right after the new release rolled out on our GKE cluster and the Pod started, it went straight down with the error message:

Error loading timezone from env variable. err={unknown time zone Europe/Budapest}

Whipping out the good old Go documentation I quickly notice this piece of information in the docs of time.LoadLocation:

... the name is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York".

LoadLocation looks for the IANA Time Zone database in the following locations in order:

- the directory or uncompressed zip file named by the ZONEINFO environment variable - on a Unix system, the system standard installation location - $GOROOT/lib/time/zoneinfo.zip - the time/tzdata package, if it was imported 

Note that the Dockerfile used to build the Docker image was similar to the following:

FROM golang:alpine as builder
WORKDIR /app 
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o svc

FROM scratch
ENV TIMEZONE=Europe/Budapest
COPY --from=builder /app/svc .
ENTRYPOINT ["./svc"]

Who doesn’t like multistage Dockerfile, am I right?

Solution

There are 3 ways of fixing this issue as described in time.LoadLocation’s docs:

  • Using an alpine image instead of the scratch image for running the app and adding the RUN apk add --no-cache tzdata line to our second stage

  • Or if you are hellbent on minimizing your Docker image size as much as possible, copy the zoneinfo zip file or directory to the scratch environment by hand in the Dockerfile and setting the path to it in the ZONEINFO environment variable

  • Importing the “time/tzdata” package with the blank identifier