Synchronizing Timezones between Java Application and Docker Container

I made a Spring Boot application that uses the java.time class a lot. Time-related classes are used, everywhere: from persistence layer to the HTML templates.

A problem that I saw early on, is that the widget that shows the "Today's Report" shows the report for yesterday. Since I did not specify the timezone, it used the UTC timezone and made "today" 8 hours late. The that has been implemented is to control the timezone by creating a Clock bean.

@Configuration
public class ClockConfiguration {

  @Bean
  public Clock systemClock() {
    return Clock.system(ZoneId.of("UTC+8"));
  }
}

Then using that bean to get the current time:

@Service
@RequiredArgsConstructor // lombok constructor
public class AggregatorService {
  
  // autowired in the generated constructor
  private final Clock systemClock;

  public List<DailyAggregate> aggregateDaily() {
    var today = LocalDate.now(systemClock);
    // service logic ....
  }
}

That solved the problem! Now the time is presented with the correct timezone.

However...

I noticed that the timezone used is different when the app is run locally using Gradle compared when running the app inside a Docker container.

The application instance that is run outside a Docker container uses the correct timezone, and the app inside the Docker container uses UTC.

Setting Timezone Data in Docker Container

The goal here is to add timezone data to the Docker container.

One of the simpliest way is to add a TZ environment variable to the Dockerfile:

ENV TZ="Asia/Manila"

But that approach is static. It requires knowledge on what the timezone value actually is. What I did in my container is I mounted the host's /etc/timezone to the container's volumes.

Mounting timezone file to the container

In Docker, you need to use the -v flag for the docker run command to mount a volume from the host to the container:

docker run --detach \
  --name=springboot-container \
  -v "/etc/timezone:/etc/timezone:ro" \
  -v "/etc/localtime:/etc/localtime:ro" \
  springapp:latest

The usage of -v flags above goes like this:

-v "$HOST_PATH:$CONTAINER_PATH:ro"
$HOST_PATH is the volume being mounted to the container. The $CONTAINER_PATH is where the $HOST_PATH will be refered in the container context. The :ro in the end is a read-only flag.

Since the /etc/timezone and /etc/localtime are system files, the path should be as is. Additionally, since the container needs to look at the file, not modify it, the volumes should be mounted as read-only.

Mounting using docker-compose

Since the project is already using Docker Compose, it would be better to set the volume mounting in the docker-compose.yml file rather than the docker run command. This makes the volume mounting command set in a file, and probably saved to your version control.

This is done by listing the time and timezone volumes in the volume list in your service in the docker-compose.yml.

version: "3.3"
services:
  web_app:
    image: "springapp:latest"
    container_name: "springboot-container"
    # other web_app service configs here
    volumes:
      - "/etc/timezone:/etc/timezone:ro"
      - "/etc/localtime:/etc/localtime:ro"
      # other volumes to mount

And that's it! When you docker compose up web_app the time and timezone from the host will be synced with the Spring Boot app in the "springboot-container" container.

This is the approach I did to synchronize timezone in my Docker container.