.NET Core Tip 1: Beware of TimeZones across platforms like Windows OS and Linux Docker Containers

.NET Core Tip 1: Beware of TimeZones across platforms like Windows OS and Linux Docker Containers

TL/DR: Make sure you use the same timezones in each OS running the different services when you need to use/insert datetimes

Prerequisites

  • Windows 10
  • .NET Core 3.1
  • Visual Studio 2022
  • Docker Desktop with WSL 2
  • Target: Linux Docker Container

Demo project

[GitHub - nicoclau/TimeZoneLinuxContainerDateTime: Set proper timezone on Linux from Dockerfile
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com](https://github.com/nicoclau/TimeZoneLinuxContainerDateTime "github.com/nicoclau/TimeZoneLinuxContainerD..")

Introduction

Context

We had two applications :

- application A running on Windows OS with timezone of Paris
- application B developped with ASP.NET CORE 3.1 running on Linux within a Docker container within Kubernetes cluster

The application A sends a message with a new datetime to the application B. The application B needs to store the new datetime in one SQL Server table.

Unfortunately I had a problem.

Problem

I had the following problem

Wrong Datetime in SQL Server Table

We noticed that the datetime on the SQL Server table was 2 hours behind.
For example we went from 19:00 to 17:00.

Analysis

Before we can solve the problem, we need to know which Linux distro is used by application B running on ASP.NET Core 3.1.

Why? So we will know what commands are possible to resolve the problem on the Linux OS.

For developing our application B, we used the architecture x86–64 dev machine. We used ASP.NET CORE 3.1 because application B was a REST Web API.

In the demo later on we will see when we use Visual Studio 2022 for adding the Dockerfile, it chooses the base docker image:

FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-bu.. AS base

We can find the ASP.NET CORE 3.1 Dockerfile for the x86–64 architecture:

[dotnet-docker/Dockerfile at main · dotnet/dotnet-docker
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com](https://github.com/dotnet/dotnet-docker/blob/main/src/aspnet/3.1/buster-slim/amd64/Dockerfile "github.com/dotnet/dotnet-docker/blob/main/s..")

We can see that Dockerfile uses:

mcr.microsoft.com/dotnetruntime:3.1.27-buster-slim

Dockerfile then downloads and installs within the filesystem ASP.NET CORE 3.1.27 for x64:

https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/3.1.27/aspnetcore-runtime-3.1.27-linux-x64.tar.gz

Finally, let’s see what Linux distro is used in mcr.microsoft.com/dotnet/runtime:3.1.27-bus..

It depends on mcr.microsoft.com/dotnet/runtime-deps:3.1.2.. which depends itself on arm64v8/debian:buster-slim

So mcr.microsoft.com/dotnet/core/aspnet:3.1-bu.. uses the Linux Debian distro.

We can check in our running container:

root@xxx:/app# cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 10 (buster)"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"

So now we are sure we are using the Debian GNU Linux version 10 (buster).
Now, what can we do to solve our problem?

Now we know Debian GNU Linux already has installed tzdata of https://www.iana.org/time-zones

We can check it with the presence of /etc/localtime and /etc/timezone

It would be a different story if the Linux distro was Linux Alpine for example which would require further steps to install tzdata.

So all we have to do is to set the right timezone directly.
Let’s see the solution.

Solution

We need to use the same timezone in Windows and in Debian Linux.
We will see how.

In our case, Windows Server used “W.Europe Standard Time” timezone in the .net core program.
So within the Linux os (the Debian Linux used by the container running our application B), we will need to set the timezone to “Europe/Paris”.

We will see there are many levels where we can do that.

We will have the following situation and we will see the problem solved:

Same Timezone but different names between the different OS

We can change the timezone of the Linux Debian OS on:

  • the OS environment variables: the most flexible solution we will use here
  • the OS filesystem
  • the codebase C# .NET Core

Be careful, the timezone names are different on Windows and Linux
Like you can see on the graph.

We will use the OS environment variables solution.
Why? Because we can change the environment variables outside the OS and the codebase. So it is very flexible.

We can change environment variables in three different possible locations:

  • dockerfile
  • docker runtime (options when docker is run)
  • kubernetes deployment (yaml file)

We will use here the dockerfile option on our demo.

DEMO

We just created a console application with .NET Core 3.1

It will just display the date.

using System;
using System.Text.Json;

namespace TimeZoneLinuxContainerDateTime
{

internal class Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now);
}
}
}

We add the docker support to our console project.

Click on Docker Support and choose Linux as Target OS:

Click on OK.

Visual Studio 2022 will run Docker behind the scenes:

\========== Checking for Container Prerequisites ==========

Verifying that Docker Desktop is installed...

Docker Desktop is installed.

\========== Verifying that Docker Desktop is running... ==========

Verifying that Docker Desktop is running...

Docker Desktop is running.

\========== Verifying Docker OS ==========

Verifying that Docker Desktop's operating system mode matches the project's target operating system...

Docker Desktop's operating system mode matches the project's target operating system.

\========== Pulling Required Images ==========

Checking for missing Docker images...

Pulling Docker images. To cancel this download, close the command prompt window.

docker pull mcr.microsoft.com/dotnet/runtime:3.1

Docker images are ready.

\========== Warming up container(s) for TimeZoneLinuxContainerDateTime ==========

Starting up container(s)...

docker build -f "C:\Dev\TimeZoneLinuxContainerDateTime\TimeZoneLinuxContainerDateTime\Dockerfile" --force-rm -t timezonelinuxcontainerdatetime:dev --target base --label "com.microsoft.created-by=visual-studio" --label "com.microsoft.visual-studio.project-name=TimeZoneLinuxContainerDateTime" "C:\Dev\TimeZoneLinuxContainerDateTime"

#1 [internal] load build definition from Dockerfile

#1 sha256:a3f449de26c280f82facd0cfe113e19009b34882c0092ca5f2b6f2b8a6a662a5

#1 transferring dockerfile: 893B 0.0s done

#1 DONE 0.1s

#2 [internal] load .dockerignore

#2 sha256:0c0d7ec41ea95c8f3d3d0c411c7a88079bdfcc88396cb7ebc856b651e82a345d

#2 transferring context: 382B 0.0s done

#2 DONE 0.1s

#3 [internal] load metadata for mcr.microsoft.com/dotnet/runtime:3.1

#3 sha256:0553f26b35189d38deb0143a81e5c83c941e9b39a74cfa379650145f26f6db1f

#3 DONE 0.0s

#4 [base 1/2] FROM mcr.microsoft.com/dotnet/runtime:3.1

#4 sha256:84cc03a55bd4a33aba1714c34d872646c9ef15055bd99dc6df9422de3007a647

#4 DONE 0.0s

#5 [base 2/2] WORKDIR /app

#5 sha256:5127c3c72b2387bd931efa010eae7503fb3d72051e78617116669bc0d87685dc

#5 CACHED

#6 exporting to image

#6 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00

#6 exporting layers done

#6 writing image sha256:e090c4f7d0a7473c836c0200b7414636e4e3fdd9057a07456815add40f91b4ea done

#6 naming to docker.io/library/timezonelinuxcontainerdat.. done

#6 DONE 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

docker run -dt -v "C:\Users\xxx\vsdbg\vs2017u5:/remote_debugger:rw" -v "C:\Dev\TimeZoneLinuxContainerDateTime\TimeZoneLinuxContainerDateTime:/app" -v "C:\Dev\TimeZoneLinuxContainerDateTime:/src/" -v "C:\Users\xxx\.nuget\packages\:/root/.nuget/fallbackpackages3" -v "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages:/root/.nuget/fallbackpackages" -v "C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages2" -e "DOTNET_USE_POLLING_FILE_WATCHER=1" -e "NUGET_PACKAGES=/root/.nuget/fallbackpackages3" -e "NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages;/root/.nuget/fallbackpackages2;/root/.nuget/fallbackpackages3" --name TimeZoneLinuxContainerDateTime --entrypoint tail timezonelinuxcontainerdatetime:dev -f /dev/null

fdf2a2d45bbcbb806679b06ffbd163386ae7021e5f1e2bf178120d0ae20b100f

Container started successfully.

\========== Finished ==========

What happened? VS built and run our application within a Docker container and attached the debugger to it. Moreover, it linked our project code to it with the use of Volumes.

Below the complete command:

docker run -dt -v "C:\Users\xxx\vsdbg\vs2017u5:/remote_debugger:rw" -v "C:\Dev\TimeZoneLinuxContainerDateTime\TimeZoneLinuxContainerDateTime:/app" -v "C:\Dev\TimeZoneLinuxContainerDateTime:/src/" -v "C:\Users\xxx\.nuget\packages\:/root/.nuget/fallbackpackages3" -v "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages:/root/.nuget/fallbackpackages" -v "C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages2" -e "DOTNET_USE_POLLING_FILE_WATCHER=1" -e "NUGET_PACKAGES=/root/.nuget/fallbackpackages3" -e "NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages;/root/.nuget/fallbackpackages2;/root/.nuget/fallbackpackages3" --name TimeZoneLinuxContainerDateTime --entrypoint tail timezonelinuxcontainerdatetime:dev -f /dev/null

We can check on the Docker desktop the container:

In the Docker images section:

We can see it is in use, it means we have a running container.

Let’s go to the shell (red arrow):

Let’s check the timezone used by running the command

# date

We can see the date is on the UTC timezone.

It will cause problem for our program let’s see.

Let’s add a breaking point in our program.

Let’s run the program from VS by clicking on “Docker”:

The debug pauses and when we click on our container we can see the Environment, volumes etc..

Let’s see on the “Logs” tab what our program displays for the datetime:

07/14/2022 13:28:33

But we are expected to be 15:28:33 at the CEST timezone.

So now let’s try our solution:

we add the environment variable TZ = “Europe/Paris” within our dockerfile and rebuild and rerun our application:

#See aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:3.1 AS base

WORKDIR /app

ENV TZ="Europe/Paris"

FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build

WORKDIR /src

COPY ["TimeZoneLinuxContainerDateTime/TimeZoneLinuxContainerDateTime.csproj", "TimeZoneLinuxContainerDateTime/"]

RUN dotnet restore "TimeZoneLinuxContainerDateTime/TimeZoneLinuxContainerDateTime.csproj"

COPY . .

WORKDIR "/src/TimeZoneLinuxContainerDateTime"

RUN dotnet build "TimeZoneLinuxContainerDateTime.csproj" -c Release -o /app/build

FROM build AS publish

RUN dotnet publish "TimeZoneLinuxContainerDateTime.csproj" -c Release -o /app/publish

FROM base AS final

WORKDIR /app

COPY --from=publish /app/publish .

ENTRYPOINT ["dotnet", "TimeZoneLinuxContainerDateTime.dll"]

We get again the program paused by the breakpoint but we notice a difference in the environment:

So now the linux running our application should display the expected current datetime (not 2 hours behind):

We get :

07/14/2022 15:35:28

At last on the shell we can check the timezone:

We are indeed in the CEST timezone.

Please find the Demo project:

[GitHub - nicoclau/TimeZoneLinuxContainerDateTime: Set proper timezone on Linux from Dockerfile
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or…github.com](https://github.com/nicoclau/TimeZoneLinuxContainerDateTime "github.com/nicoclau/TimeZoneLinuxContainerD..")

Conclusion:

When we work with different OS, we need to be aware of the timezones.
It is even more true when we work with Linux Docker containers running our workload.

References:

Some articles about the problem:

[Cross-platform Time Zones with .NET Core
Developing applications that span multiple operating systems in .NET Core while working with Time Zone information can…devblogs.microsoft.com](https://devblogs.microsoft.com/dotnet/cross-platform-time-zones-with-net-core/ "devblogs.microsoft.com/dotnet/cross-platfor..")

[Differences in Time Zone Formats in .NET Core on Windows and Linux host
Recently I was working on converting DateTime value to a different time zone because the servers are running in a…dejanstojanovic.net](https://dejanstojanovic.net/aspnet/2018/july/differences-in-time-zones-in-net-core-on-windows-and-linux-host-os/ "dejanstojanovic.net/aspnet/2018/july/differ..")

[How to Handle Timezones in Docker Containers
Timezones are a common source of confusion when containerizing an application. Will your cron tasks run at the right…howtogeek.com](https://www.howtogeek.com/devops/how-to-handle-timezones-in-docker-containers/ "howtogeek.com/devops/how-to-handle-timezone..")

The best article, it is in french though:

[Guide : corriger les problèmes d'heure dans Docker
Régler l’heure et la timezone sur Linux Avant d’attaquer la partie Docker, il est nécessaire de rappeler…hoa.ro](https://hoa.ro/fr/blog/2020-12-08-draft-docker-time-timezone/ "hoa.ro/fr/blog/2020-12-08-draft-docker-time..")