Docker Container Time Drift: Syncing Container Clock with Host RTC

Root Cause Diagnosis

Containers do not have their own clock. They share the host kernel’s system clock (CLOCK_REALTIME), so a container cannot drift independently on a native Linux host. If container time is wrong, either the host clock is wrong (no NTP sync, or RTC and system time disagree), or the container runs inside a VM (Docker Desktop, WSL2, Colima, Hyper-V, VirtualBox) whose guest clock stops advancing during host sleep or suspend and is not corrected on resume. Only the timezone, which comes from /etc/localtime and TZ inside the container, is per-container, so many “drift” reports are actually timezone mismatches.

Quick Fix

Compare host and container time first:

date -u; docker run --rm alpine date -u

If the two UTC outputs differ by more than a second, the host or the Docker VM clock is wrong. Fix it at that layer.

# Native Linux host: force an NTP sync
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd    # or: sudo systemctl restart chronyd

# Docker Desktop / WSL2: resync the VM clock from the Windows host
sudo hwclock -s            # inside the WSL2 distro
# or, from PowerShell:  wsl --shutdown   (then restart Docker Desktop)

If the UTC outputs match but local times differ, the problem is timezone only. Run the container with -e TZ=Europe/Berlin (substitute your zone).

Step-by-Step Resolution

1. Determine whether it is drift or a timezone difference

docker exec <container> date -u
date -u
docker exec <container> sh -c 'echo "TZ=$TZ"; readlink -f /etc/localtime'

Matching UTC with differing local output means the timezone is wrong. Differing UTC means the kernel clock is wrong.

See also  Linux out of memory killer (oom-killer) picking wrong high-priority daemon: Tuning oom_score_adj

2. Check the host clock and RTC (native Linux)

timedatectl
sudo hwclock --show --utc

In the timedatectl output, System clock synchronized: yes and NTP service: active are required. If RTC in local TZ: yes, the RTC holds local time, which commonly happens on dual-boot machines with Windows. Set it to UTC:

sudo timedatectl set-local-rtc 0 --adjust-system-clock

3. Enable a time synchronization daemon on the host

Debian/Ubuntu:

sudo timedatectl set-ntp true
sudo systemctl enable --now systemd-timesyncd
timedatectl timesync-status

RHEL/Fedora/Rocky/Alma, or hosts that need fast stepping after long offsets:

sudo dnf install -y chrony
sudo systemctl enable --now chronyd
chronyc tracking
sudo chronyc makestep

Do not run NTP clients inside containers. Without CAP_SYS_TIME they cannot set the clock, and with it they would modify the host clock for every container.

4. Write the RTC from the system clock after a correction

sudo hwclock --systohc --utc

Chrony does this automatically if /etc/chrony/chrony.conf (or /etc/chrony.conf) contains:

rtcsync

5. Set the container timezone correctly

Preferred, via environment variable (works for images that include tzdata):

docker run -e TZ=America/New_York <image>

Compose:

services:
  app:
    image: <image>
    environment:
      - TZ=America/New_York
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /etc/timezone:/etc/timezone:ro

Mounting /etc/localtime works only on hosts that have it as a valid zoneinfo file or symlink. /etc/timezone is Debian-specific and does not exist on RHEL-family hosts, so omit that mount there. Minimal images (alpine, scratch, distroless) need tzdata installed for TZ to take effect:

RUN apk add --no-cache tzdata
# Debian/Ubuntu: RUN apt-get update && apt-get install -y --no-install-recommends tzdata

6. Docker Desktop (macOS/Windows) and WSL2: resync the VM clock

The Docker VM’s clock freezes during host sleep and hibernation, then resumes with a stale value.

See also  Resolving Docker DNS lookup failure inside container on custom bridge network

WSL2 (inside the distro):

sudo hwclock -s

Automate on WSL2 by adding to /etc/wsl.conf:

[boot]
systemd=true
command="hwclock -s"

Restart with wsl --shutdown from PowerShell. Docker Desktop on macOS: restart Docker Desktop to force a resync, or update to a current release, which resyncs on wake.

Colima/Lima:

colima ssh -- sudo chronyc makestep
# or restart the VM
colima stop && colima start

7. Verify

for i in 1 2 3; do date -u +%s; docker run --rm alpine date -u +%s; sleep 5; done

Timestamp pairs should differ by at most 1 second consistently across iterations.

Why This Fails for Some Users

  • Sleep and suspend on Docker Desktop, WSL2, or laptops: The VM clock halts during host sleep. Correction happens only on resume or on a manual resync. Time is wrong until then.
  • Drift measured as a timezone offset: An offset of exact whole hours or half-hours is a timezone problem, not drift. Fix with TZ in Step 5.
  • TZ set but ignored: The image lacks tzdata, or the application reads /etc/localtime directly and ignores the environment variable. Mount /etc/localtime or install tzdata.
  • Dual-boot RTC conflict: Windows stores local time in the RTC while Linux assumes UTC, producing a constant offset after each boot switch. Use set-local-rtc 0 on Linux, or set the Windows registry value RealTimeIsUniversal (DWORD 1) under HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation.
  • Container cannot change the clock: date -s or ntpdate inside a container fails with Operation not permitted. CAP_SYS_TIME is dropped by default, and granting it alters the host clock globally. Fix the host instead.
  • Firewall blocks NTP: UDP port 123 outbound is blocked, so timedatectl timesync-status shows no sync. Point chrony or timesyncd at an internal server via NTP= in /etc/systemd/timesyncd.conf or server/pool lines in the chrony config.
  • Host is a cloud VM with a failing time source: Use the provider’s local time source (for example 169.254.169.123 on AWS, metadata.google.internal on GCP) in the chrony config.
  • Hypervisor guest clock disagreement: VMware, VirtualBox, and Hyper-V integration services can step the guest clock independently of NTP. Disable host-guest time sync, or disable the competing NTP daemon, so only one authority sets the clock.
  • Time namespaces: Linux 5.6+ supports time namespaces, but they offset only CLOCK_MONOTONIC and CLOCK_BOOTTIME, not wall-clock time. They do not cause date drift, and Docker does not enable them by default.
  • Application-level misbehavior: JVM, Python, and Node processes cache the timezone at startup. Restart the process after changing TZ, and for Java also set -Duser.timezone=<zone>.
  • Timestamps in logs differ from date: The logging driver or collector (for example json-file, Fluentd, Loki) timestamps entries in UTC on the host side. Compare with docker logs --timestamps.
  • Rootless Docker and Podman: The clock is still the host’s, but the timezone mount path may be unreadable under user namespaces. Verify with docker exec <container> ls -l /etc/localtime.

Leave a Comment