Configuring Timezone and Locale Correctly in Docker Containers
AI generated
FROM
RUN
Docker · Configuration · Internationalization
Setting Timezone and Locale Correctly in Containers
Understanding the UTC default and configuring date formats reliably

Containers run in UTC by default without installed timezone data, which quickly leads to wrong output in PHP and Node applications with localized date formats unless TZ, tzdata, and locale are configured deliberately.

16 min read Timezone Locale Internationalization

1. Why Containers Default to UTC

Most official base images, especially lean variants like alpine or slim images on a Debian base, ship without the tzdata package and without /etc/localtime configuration for size reasons. Without that data, the container automatically falls back to UTC, because the operating system cannot apply any other timezone without a timezone database, even when the host machine runs in a different timezone.

This is a deliberate choice, because UTC as a reference timezone has no daylight saving transitions and is therefore the most reliable choice for server-side processes like logging, scheduling, and database timestamps. It only becomes a problem when an application itself needs to display localized times, for example on an invoice or a user interface for end customers in a specific region, without the conversion happening explicitly in application code.

2. Using the TZ Environment Variable Correctly

The TZ environment variable, for example TZ=Europe/Berlin, tells many programming languages and libraries which timezone to use for time functions, without changing the operating system itself. PHP, Node.js, Python, and many other runtimes read this variable and apply it to date and time functions, provided the underlying timezone database is present in the image.

This is exactly where a common source of errors lies: TZ alone is not enough if the tzdata package is missing, because the runtime then knows the timezone's name but cannot look up offset and daylight saving rules. The result is a silent fallback to UTC or an error, depending on the language and library, which often goes unnoticed in tests because they frequently work with UTC anyway.


# Set TZ as an environment variable for the container
docker run -e TZ=Europe/Berlin myapp

# Check which timezone the container currently uses
docker exec myapp date
docker exec myapp cat /etc/timezone

3. Installing the tzdata Package in Lean Images

For TZ to actually take effect, the timezone database must be present as a package in the image. On Debian- and Ubuntu-based images the package is called tzdata, on Alpine images also tzdata, but via the apk package manager. After installation, the full IANA timezone database resides under /usr/share/zoneinfo, which most language runtimes reference once TZ is set.

On Debian-based images extra care is needed with interactive installation: without DEBIAN_FRONTEND=noninteractive, the tzdata package interactively prompts for the timezone during installation and blocks the build, because no input is possible in a non-interactive Docker build context. This environment variable should therefore be set before the installation.


# Dockerfile snippet: installing tzdata correctly on a Debian base
FROM debian:bookworm-slim
ENV TZ=Europe/Berlin
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y tzdata \
    && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \
    && dpkg-reconfigure -f noninteractive tzdata \
    && rm -rf /var/lib/apt/lists/*

# Dockerfile snippet: tzdata on an Alpine base
FROM node:20-alpine
ENV TZ=Europe/Berlin
RUN apk add --no-cache tzdata \
    && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone

4. A Complete Dockerfile Example for PHP Applications

In a typical PHP application, setting TZ at the operating system level alone is not enough, because PHP has its own timezone configuration via php.ini, independent of the system timezone. If date.timezone is missing there, PHP often falls back to the system timezone, but depending on php.ini settings raises an E_WARNING when calling date functions, which creates unnecessary log noise in production.

A complete setup therefore combines three layers: the TZ environment variable for the operating system, the tzdata package for the underlying timezone database, and a dedicated php.ini directive for date.timezone, so PHP always uses the same timezone regardless of the calling environment, even if TZ happens to be unset for some reason.


FROM php:8.4-fpm
ENV TZ=Europe/Berlin
RUN apt-get update && apt-get install -y tzdata \
    && ln -fs /usr/share/zoneinfo/$TZ /etc/localtime \
    && echo $TZ > /etc/timezone \
    && printf '[Date]\ndate.timezone = ${TZ}\n' \
       > /usr/local/etc/php/conf.d/timezone.ini \
    && rm -rf /var/lib/apt/lists/*

5. Locale Problems in PHP and Node Applications

While TZ and tzdata shift the point in time correctly, the locale controls how date, time, currency amounts, and numbers are formatted linguistically. A PHP application that uses strftime or IntlDateFormatter to output German month names or correctly formatted decimal numbers with a comma instead of a period needs an installed and activated de_DE locale in the container for that.

Lean base images often contain only the C or POSIX locale, where all output falls back to a neutral, usually English-language format. In practice this leads to symptoms like English weekday names in an otherwise German-language interface, or warnings when an application explicitly calls setlocale(LC_TIME, 'de_DE.UTF-8') and the locale simply does not exist on the system.

6. The PHP intl Extension and Node.js Full-ICU

For robust internationalization, the classic setlocale function in PHP is often not enough, because its behavior strongly depends on the operating system and the locales installed on it. The intl extension, based on ICU (International Components for Unicode), avoids this problem by bringing its own locale and timezone database embedded in PHP, delivering consistent formatting independently of the system locales installed in the container.

Node.js behaves similarly: the official Node images ship with Full-ICU by default, so Intl.DateTimeFormat and related APIs already support all locales without any additional system configuration. Anyone using a particularly lean, self-built Node image without Full-ICU, however, must either swap the small-icu data package for full-icu or explicitly include the data via NODE_ICU_DATA, otherwise non-English locales are silently ignored.


# Install the PHP intl extension (Debian base)
docker exec myapp php -m | grep intl
apt-get install -y php8.4-intl

# Check whether Full-ICU is active in Node.js
docker exec myapp node -e \
  "console.log(new Intl.DateTimeFormat('de-DE', {month:'long'}).format(new Date()))"

7. Generating Locales on Debian-Based Images

On Debian- and Ubuntu-based images, required locales must be explicitly generated via the locales package, since by default only C.UTF-8 is present. The locale-gen command builds the compiled locale file from the line activated in /etc/locale.gen, which is then made active for all processes in the container via LANG and LC_ALL as environment variables.

On Alpine-based images the situation is more complicated, because Alpine's musl libc, unlike glibc, does not ship a full locale system and simply does not support many locale names. For applications with serious localization needs, a glibc-based image like debian-slim, or a special Alpine image with a glibc compatibility layer installed afterwards, is often the more practical choice than trying to force locales under musl.


FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y locales \
    && sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen \
    && locale-gen \
    && rm -rf /var/lib/apt/lists/*
ENV LANG=de_DE.UTF-8
ENV LC_ALL=de_DE.UTF-8

8. Consistent Configuration Across Multiple Services in Compose

In a typical compose stack with PHP-FPM, Nginx, MySQL, and a cron container, it is important to set TZ, LANG, and LC_ALL identically for all involved services, otherwise inconsistent timestamps arise between application logic and the database. MySQL, for example, also defaults to UTC but can use a different timezone via its own TZ configuration or via CONVERT_TZ functions in queries, which leads to hard-to-trace time shifts if the configuration is inconsistent.

The most maintainable approach is to keep TZ, LANG, and LC_ALL centrally in a .env file and reference them via environment in every service block of the compose file, instead of hardcoding the values individually in every Dockerfile. Changes to the timezone, for example when switching target markets, are then possible in a single place without rebuilding several images.


# .env
TZ=Europe/Berlin
LANG=de_DE.UTF-8
LC_ALL=de_DE.UTF-8

# docker-compose.yml
services:
  php-fpm:
    environment:
      TZ: ${TZ}
      LANG: ${LANG}
      LC_ALL: ${LC_ALL}
  mysql:
    environment:
      TZ: ${TZ}

9. Testing and Debugging TZ and Locale Issues

A common pitfall is that timezone and locale problems go unnoticed in automated tests, because test environments often work with UTC and the C locale anyway, and the actual root cause only becomes visible in production with real TZ and LANG values. A simple smoke test that prints date, locale, and php -i | grep date.timezone on container startup and checks against expected values catches such discrepancies before deployment.

For manual debugging in a running container, docker exec container date gives a quick overview of the effective system time, while locale -a lists all locales actually installed in the image. If an expected locale is missing from that list, the installation or locale-gen usually did not run correctly, regardless of what was set in LANG or LC_ALL.

Base image tzdata installation Locale support Recommendation
debian:bookworm-slim apt-get install tzdata Full support via the locales package Best choice with localization needs
alpine (musl) apk add tzdata Limited, many locales missing Only for pure UTC operation without locale needs
ubuntu:24.04 apt-get install tzdata Full support via the locales package Equivalent to Debian
distroless Cannot be added after the fact Not present Only if the application tolerates UTC/C locale

Mironsoft

Container infrastructure, CI pipelines and deployment automation

Docker setups that hold up across the team and in production?

We review existing Dockerfiles and Compose stacks for security gaps, bloated images and fragile build pipelines, then build a container infrastructure that builds fast, runs securely and stays understandable across the team.

Dockerfile Review

Systematically optimizing multi-stage builds, layer caching and image size.

Security Audit

Hardening container isolation, secrets handling and image scanning against real attack surfaces.

CI/CD Integration

Building build pipelines, registries and deployment strategies for reproducible releases.

10. Summary

Timezone and Locale: The Essentials at a Glance

Starting point

Lean base images run in UTC by default without tzdata.

Timezone

TZ variable plus installed tzdata package for correct time shifting.

Locale

locale-gen and LANG/LC_ALL for linguistically correct date formats.

Consistency

Central .env values for all compose services avoid time drift.

11. FAQ: Timezone and Locale: The Essentials at a Glance

1Why do Docker containers default to UTC?
Because lean base images lack the tzdata package for size reasons, and the operating system cannot apply any other timezone without a timezone database, it automatically falls back to UTC.
2Is setting the TZ variable alone enough?
No, without the tzdata package installed, the runtime knows the timezone's name but cannot look up offset and daylight saving rules, which can lead to a silent fallback to UTC.
3How do I install tzdata on an Alpine image?
Via the apk package manager with apk add --no-cache tzdata, after which /etc/localtime should be symlinked to the desired timezone under /usr/share/zoneinfo.
4Why does the tzdata installation sometimes block the Docker build?
On Debian-based images, the tzdata package interactively prompts for the timezone without DEBIAN_FRONTEND=noninteractive, which causes the build to hang in a non-interactive build context.
5Does PHP need extra configuration beyond the TZ variable?
Yes, PHP uses its own date.timezone directive in php.ini, which should be set independently of the system timezone to avoid warnings and inconsistent behavior.
6What is the difference between timezone and locale?
The timezone determines the correct point in time including the offset to UTC, while the locale determines the linguistic formatting of date, time, currency, and numbers; both concepts must be configured independently.
7Why do some locales not work on Alpine?
Alpine uses musl libc instead of glibc, and musl does not ship a full locale system, so many locale names are simply unsupported even if the locale package is installed.
8How do I generate a German locale on Debian images?
Via the locales package, activating the desired line in /etc/locale.gen, and then running locale-gen, followed by setting LANG and LC_ALL as environment variables.
9How do I ensure consistent timezones across multiple compose services?
Best done via central values in a .env file referenced through environment in every service block, instead of hardcoding TZ and LANG individually in every Dockerfile.
10How do I check which locales are actually available in a container?
With locale -a inside the container, which lists all installed locales. If the expected locale is missing from the output, the installation or locale-gen usually did not complete correctly.