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.
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.
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
TZin Step 5. TZset but ignored: The image lackstzdata, or the application reads /etc/localtime directly and ignores the environment variable. Mount /etc/localtime or installtzdata.- 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 0on Linux, or set the Windows registry valueRealTimeIsUniversal(DWORD1) underHKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation. - Container cannot change the clock:
date -sorntpdateinside a container fails withOperation not permitted.CAP_SYS_TIMEis 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-statusshows no sync. Point chrony or timesyncd at an internal server viaNTP=in /etc/systemd/timesyncd.conf orserver/poollines 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.123on AWS,metadata.google.internalon 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_MONOTONICandCLOCK_BOOTTIME, not wall-clock time. They do not causedatedrift, 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 examplejson-file, Fluentd, Loki) timestamps entries in UTC on the host side. Compare withdocker 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.