In this post, we’re going to explore the cause of a TimeZoneNotFoundException in a .NET Core application, running on .NET Core 3.0 in an Alpine Linux Docker container.
The default Alpine Docker image does not include time zone data by default. This causes code which depends on this data to throw exceptions. If you need this data you must add the package to your image or use a non-alpine image.
Exploring the Problem
My problems began whilst working on a multi-tenant event processing microservice. The service processes some messages from an AWS SQS queue, triggering the creation and population of a data model which is then sent onto another queue for further processing.
One of the inputs on the original message is a UTC DateTime which indicates when an event occurred. My service needs to translate that to a local DateTime, based on the configuration for the tenant to which the event belongs.
The way this works is that using the tenant identifier on the incoming message, the service queries a configuration API to get the tenant’s configured time zone identifier (stored as a Windows ID). Using that ID, my code then converts the UTC DateTime into the local DateTime.
One extra complication is that the service is developed on my Windows PC and we get a Windows ID for the time zone from the configuration API. However, when deployed, the service runs in a Linux Docker container. Since Linux and Windows use a different time zone database, converting dates in a cross-platform way needs some care.
To work around the platform differences, I had chosen to use the TimeZoneConverter NuGet package in my solution. You can read more about this approach on the Microsoft .NET Core Blog in, “Cross-platform Time Zones with .NET Core“.
When running my application on my Windows PC, everything worked as expected. However, upon testing the code running inside a Docker container, it failed with an exception.
This surprised me as I thought that by using the TimeZoneConverter library I was protected from such errors. I spent a little while trying to isolate the issue in my code, a little longer than I care to admit!
Reproducing the Exception
After getting nowhere I decided it was best to produce a minimal reproduction of the code, to rule out as many potential causes as possible. I ended up created a simple .NET Core 3.0 console application. I included the dependency to the TimeZoneConverter library.
I then added some basic code which exercised the static GetTimeZoneInfo method I was calling on TZConvert. Here is the code:
I ran this code on Windows and the output was as expected:
(UTC+00:00) Dublin, Edinburgh, Lisbon, London
(UTC+00:00) Dublin, Edinburgh, Lisbon, London
I then created a Dockerfile for this project, based on the structure used for our .NET Core microservices:
I built a Docker image and then ran it which resulted in the TimeZoneNotFoundException as I’d been seeing in my original service.
I was honestly stumped for a few minutes and resorted to some random Googling of time zone issues with Linux. Ultimately, I stumbled upon a few pages which referred to issues in Alpine Linux specifically. I was getting warmer! I narrowed my searched to Alpine specifically and hit the problem. By default, Alpine is a very slim, lightweight Linux distribution. One of the things it lacks is the time zone database. That would explain the exceptions.
Note: We can do better than this but I’m including it for completeness.
The first and most obvious solution was to depend on a non-Alpine Linux image as my base. I could achieve this by switching to the mcr.microsoft.com/dotnet/core/aspnet runtime image with the tag 3.0 in my Dockerfile.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 AS base
After this change and rebuilding the image, I was able to run the Console1 code without exception. Hurrah!
This works because this tag is based on a Debian 10 distribution which includes the time zone database my code requires.
While the above solution works fine, it left me frustrated that I was unable to use the lighter Alpine images. I prefer these as we build images often as part of CI/CD and reducing the size has a fairly large effect on our image repository.
I set about finding a better solution and in the end, realised I could simply add the time zone database package to my Alpine image as part of my Dockerfile. I simply needed to add one command to my base runtime image in my Dockerfile:
The final line here is the key, it uses the Alpine package management tool to install the tzdata package.
With this single run command, my Docker image now included the required time zone data and my application was able to execute correctly, without exception.
This was the first time I’d run into any limitations of the Alpine runtime image so it took me a while to realise that it’s slim nature meant I was lacking a key dependency. Problems like this crop up for developers fairly often and when one has never seen them before, solutions can be tricky. I hope this post explains why the troubleshooting steps of producing a minimal reproduction helped set me on the path to success, narrowing down the problem to a very small set of possible causes.