Ваш пейджер замигає о 02:07. Панель показує, що пакетне завдання «не вклалося в SLA», у БД — мітки часу з «майбутнього», а аудит стверджує,
що працівники заходили в систему до найму. Код ніхто не чіпав. Єдина реальна зміна була… перезапуск контейнера.
Зсув часового поясу в контейнерах — це така несправність, що змушує розумних людей говорити дурниці на кшталт «але час — це ж час». У продакшні час — це залежність.
Він має конфігурацію, крайові випадки й політичні межі. І коли він неправий, ви втрачаєте гроші найбільш бюрократичним способом.
Що насправді означає «зсув часового поясу» в контейнерах
Розділимо три проблеми, які зводять людей до одного сердитого повідомлення в Slack:
- Зсув системного годинника: системний годинник неправильний (хвилини/години), бо NTP/chrony не працює або гіпервізор/віртуалізація неналежно синхронізують час.
- Невідповідність часового поясу: годинник правильний (UTC-секунди), але контейнер інтерпретує локальний час у невірній зоні (наприклад, UTC проти America/New_York).
- Невідповідність бази правил часових поясів: назва зони правильна, але правила (перехід на DST) застарілі, бо tzdata старий або відсутній.
Контейнери не мають власного годинника ядра. Вони читають час із ядра хоста. Якщо час неправильний — це проблема хоста. Якщо час у порядку, але
відображуваний локальний час неправильний — зазвичай це конфігурація контейнера: /etc/localtime, /etc/timezone, поведінка libc,
кешовані правила Java або додаток, який перевизначає обробку часових поясів.
Складність у тому, що «виправити часовий пояс» має кілька шарів. Debian-подібний контейнер може покладатися на те, що glibc читає
/etc/localtime як бінарний zoneinfo-файл. Alpine (musl) очікує трохи інакше. Java іноді постачає свої правила зон і кешує їх до перезапуску.
Бази даних можуть мати власні параметри часових поясів. І ваш застосунок може робити класичну помилку «локальний час скрізь».
Якщо ви запам’ятаєте лише одне: не пересобирайте образ лише щоб виправити поведінку часових поясів. Це операційний борг, замаскований під охайність.
Є безпечні рантайм-виправлення — обирайте їх.
Факти та історія, які пояснюють, чому це повторюється
Ось кілька маленьких, конкретних фактів, які роблять дивність менш особистою:
- UTC — це не «без часової зони». Це часовий пояс з правилами і врахуванням секунд-єдначків; багато додатків все одно трактують його як магічну константу.
- Більшість контейнерів постачаються без tzdata. Мінімальні образи вирізають його заради місця; тоді локальний час стає тим, що обирає libc як запасний варіант.
- База зон IANA постійно змінюється. Уряди змінюють зсуви й правила DST без великого попередження; оновлення tzdata — це нормальна операція.
- Скорочення назв зон неоднозначні. «CST» може означати China Standard Time або Central Standard Time; користуйтеся регіональними назвами на кшталт
America/Chicago. - Історично Unix використовував
/etc/localtimeяк канонічний перемикач. Багато дистрибутивів і досі трактують його як єдине джерело істини для «локального часу». - Docker за замовчуванням не віртуалізує часові пояси. Контейнери можуть мати відмінні файли часових поясів від хоста, але вони все одно ділять годинник хоста.
- Переходи на літній/зимовий час — це подарунок, що бере назад. Ваш інцидент відбудеться під час переходу DST, бо, звісно, так зазвичай буває.
- Java історично кешувала правила часових поясів. Навіть якщо ви виправите файли на диску, деякі JVM тримають старі правила в пам’яті до перезапуску.
Цитата, яка варта нотатки біля вашого ноутбука під час он-колу:
Усе ламається, весь час.
— Werner Vogels
Швидка діагностика (перевірка перша/друга/третя)
Це цикл «перестаньте сперечатися, почніть вимірювати». Запускайте його, коли підозрюєте проблему з часовим поясом і потрібно швидко знайти корінь.
Перше: чи правильний годинник хоста?
- Якщо годинник хоста неправильний — контейнери теж будуть неправильні. Виправте NTP/chrony, синхронізацію гіпервізора або джерело часу хоста.
- Якщо на хості все в порядку — не звинувачуйте NTP. Рухаємось далі.
Друге: показує контейнер UTC чи локальний час через відсутню конфігурацію?
- Перевірте
dateвсередині контейнера і порівняйте з UTC-виводом. - Перевірте наявність і тип
/etc/localtime. - Перевірте змінну оточення
TZвсередині контейнера і чи застосунок її враховує.
Третє: чи це саме правила tzdata (DST) або обробка часового поясу на рівні застосунку?
- Шукайте «зсув рівно на одну годину» біля меж DST.
- Перевірте версію пакета tzdata (якщо є) і порівняйте між хостами/контейнерами.
- Для JVM перевірте
user.timezoneі чи потрібен перезапуск JVM, щоб підхопити нові правила.
Жарт №1: Часові пояси — єдина функція, де «працює на моїй машині» — це політичне рішення від парламенту.
Практичні завдання: команди, виводи, рішення (12+)
Це реальні завдання, які можна виконати під час інциденту або як профілактичний аудит. Кожне містить: команду(и), приклад виводу та рішення.
Запускайте команди на хості Docker. Запускайте в контейнері через docker exec.
Завдання 1: Перевірте синхронізацію часу хоста та джерело часу
cr0x@server:~$ timedatectl status
Local time: Sat 2026-01-03 11:40:12 UTC
Universal time: Sat 2026-01-03 11:40:12 UTC
RTC time: Sat 2026-01-03 11:40:12
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Що це означає: Якщо System clock synchronized — no або NTP неактивний, у вас проблема на хості.
Рішення: Спершу виправте синхронізацію часу хоста. Рантайм-виправлення контейнера не допоможуть, якщо годинник ядра неправильний.
Завдання 2: Перевірте хост vs контейнер «зараз» (UTC і локальний) в один крок
cr0x@server:~$ date -u; date
Sat Jan 3 11:40:20 UTC 2026
Sat Jan 3 11:40:20 UTC 2026
cr0x@server:~$ docker exec web-1 date -u; docker exec web-1 date
Sat Jan 3 11:40:21 UTC 2026
Sat Jan 3 06:40:21 EST 2026
Що це означає: На хості UTC; контейнер показує EST. Це не зсув годинника; це різниця в конфігурації.
Рішення: Підтвердіть, що це заплановано. Якщо ні — уніфікуйте: або все в UTC, або все в локальному, але послідовно.
Завдання 3: Перевірте середовище контейнера на перевизначення TZ
cr0x@server:~$ docker exec web-1 env | grep -E '^TZ='
TZ=America/New_York
Що це означає: Хтось явно встановив TZ; базовий образ може інакше бути в UTC.
Рішення: Вирішіть, чи TZ — ваш офіційний інтерфейс. Якщо так — примусьте його по всіх деплойментах. Якщо ні — видаліть і змонтуйте /etc/localtime.
Завдання 4: Перевірте, чи присутній /etc/localtime і що це таке
cr0x@server:~$ docker exec web-1 ls -l /etc/localtime
lrwxrwxrwx 1 root root 36 Jan 3 10:00 /etc/localtime -> /usr/share/zoneinfo/Etc/UTC
Що це означає: Це симлінк на UTC zoneinfo.
Рішення: Якщо вам потрібен локальний час, можна змонтувати хостовий /etc/localtime в контейнер (тільки для читання) без пересборки.
Завдання 5: Підтвердіть, що zoneinfo існує в контейнері
cr0x@server:~$ docker exec web-1 ls -l /usr/share/zoneinfo/America/New_York
ls: cannot access '/usr/share/zoneinfo/America/New_York': No such file or directory
Що це означає: tzdata (файли zoneinfo) відсутні. Багато мінімальних образів так роблять.
Рішення: Краще змонтувати хостовий каталог /usr/share/zoneinfo в контейнер, або хоча б один файл /etc/localtime.
Завдання 6: Визначте базовий дистрибутив та очікування libc
cr0x@server:~$ docker exec web-1 cat /etc/os-release
PRETTY_NAME="Alpine Linux v3.19"
NAME="Alpine Linux"
VERSION_ID=3.19
ID=alpine
Що це означає: Alpine використовує musl; поведінка часових поясів може відрізнятися від Debian/glibc, особливо коли файли відсутні.
Рішення: Для Alpine зазвичай достатньо змонтувати /etc/localtime, але застосунки, що очікують /etc/timezone, все ще можуть некоректно працювати.
Завдання 7: Підтвердіть, що контейнер «вважає» своїм часовим поясом (glibc/musl дружнє)
cr0x@server:~$ docker exec web-1 sh -lc 'date; readlink -f /etc/localtime || true; ls -l /etc/timezone 2>/dev/null || true'
Sat Jan 3 06:41:02 EST 2026
/usr/share/zoneinfo/Etc/UTC
Що це означає: Вивід несумісний: date каже EST, але /etc/localtime вказує на UTC; це свідчить про TZ або налаштування на рівні застосунку.
Рішення: Уніфікуйте механізм. Змішування TZ зі старим /etc/localtime ускладнює дебаг.
Завдання 8: Виявлення проблем правил DST за допомогою zdump (якщо є)
cr0x@server:~$ docker exec web-1 sh -lc 'command -v zdump || echo "zdump missing"'
zdump missing
Що це означає: Мінімальний образ; інструментів tzdata немає.
Рішення: Використайте інструменти хоста проти змонтованого zoneinfo, або запустіть тимчасовий дебаг-контейнер у тому ж namespace/мережі.
Завдання 9: Використайте дебаг-контейнер для інспекції поведінки часу без зміни образу застосунку
cr0x@server:~$ docker run --rm --network container:web-1 alpine:3.19 sh -lc 'date; ls -l /etc/localtime; echo ${TZ:-no-TZ}'
Sat Jan 3 11:41:45 UTC 2026
lrwxrwxrwx 1 root root 36 Jan 3 11:41 /etc/localtime -> /usr/share/zoneinfo/UTC
no-TZ
Що це означає: Дебаг-контейнер показує UTC; ваш застосунок — EST. Це не «хост». Це конфігурація вашого контейнера.
Рішення: Виправляйте конфігурацію часового поясу контейнера через маунти/змінні оточення, а не налаштування хоста.
Завдання 10: Підтвердіть маунти запущеного контейнера (чи ми вже монтуємо /etc/localtime?)
cr0x@server:~$ docker inspect web-1 --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/etc/localtime","Destination":"/etc/localtime","Mode":"ro","RW":false,"Propagation":"rprivate"}]
Що це означає: Ви вже монтуєте /etc/localtime. Якщо час все ще неправильний, шукайте TZ або налаштування застосунку.
Рішення: Якщо маунти в порядку, припиніть тикати маунти і перевірте конфігурацію рантайму застосунку.
Завдання 11: Перевірте історію Docker-образу на «оптимізації» з timezone
cr0x@server:~$ docker history --no-trunc myorg/web:prod | head
IMAGE CREATED CREATED BY SIZE COMMENT
a1b2c3d4e5f6 2 weeks ago /bin/sh -c rm -rf /usr/share/zoneinfo ... 0B
...
Що це означає: Хтось видалив zoneinfo, щоб зекономити кілька мегабайт — і купив інцидент натомість.
Рішення: Не пересобирайте зараз (ви ж казали, що не будете), але заплануйте майбутню правку образу: зберігайте tzdata або монтуйте його з хоста.
Завдання 12: Перевірте timezone застосунку (приклад: JVM)
cr0x@server:~$ docker exec api-1 sh -lc 'java -XshowSettings:properties -version 2>&1 | grep -E "user.timezone|java.version"'
java.version = 17.0.10
user.timezone = UTC
Що це означає: JVM прив’язана до UTC незалежно від /etc/localtime контейнера.
Рішення: Якщо вам справді потрібен локальний час (намагайтеся уникати), задайте timezone JVM через змінну оточення або аргумент JVM під час запуску. Інакше тримайте UTC і виправляйте очікування.
Завдання 13: Перевірте налаштування timezone бази даних (приклад: PostgreSQL)
cr0x@server:~$ docker exec pg-1 psql -U postgres -Atc "show timezone; select now();"
UTC
2026-01-03 11:42:33.123456+00
Що це означає: БД в UTC. Якщо ваш застосунок показує локальний час, десь відбувається конвертація.
Рішення: Оберіть один канонічний часовий пояс для зберігання (UTC) і конвертуйте лише на краях (UI/звітність). Якщо відхиляєтесь — документуйте це як небезпечний матеріал.
Завдання 14: Доведіть, чи проблема — «timezone», чи «зміна часу»
cr0x@server:~$ docker exec web-1 sh -lc 'date +%s; sleep 2; date +%s'
1704282160
1704282162
Що це означає: Секунди рухаються нормально. Якби ви бачили стрибки або відкат часу — підозрюйте налаштування хоста, а не файли часових поясів.
Рішення: Якщо епохальні секунди в порядку — зосередьтесь на конфігурації timezone і парсингу/форматуванні. Якщо епоха стрибає — досліджуйте синхронізацію часу хоста і віртуалізацію.
Виправлення без пересборки образів (що насправді працює)
Ви хочете рантайм-виправлення, які оборотні, піддаються аудиту і не потребують нової збірки образу. Добре. Ось патерни, що працюють у продакшні, і застереження,
які не дозволять «виправити» проблему іншим инцидентом.
Виправлення 1: Bind-монтуйте хостовий /etc/localtime (основний метод)
Це найпоширеніше і зазвичай найчистіше рішення. Воно змушує контейнер інтерпретувати локальний час так само, як хост. Це не змінює годинник хоста.
Воно лише надає zoneinfo-файл.
cr0x@server:~$ docker run -d --name web-fixed \
-v /etc/localtime:/etc/localtime:ro \
myorg/web:prod
Що ви отримуєте: libc контейнера використовуватиме конфігурацію зон хоста.
Що може вас підстерігати: Якщо хостовий часовий пояс змінять пізніше (або він різний по хостах), поведінка контейнера теж зміниться. Для когось це — функція, для когось — кошмар.
Думка: Це підходить для «все в тій самій локації з узгодженою конфігурацією хостів». Ризиковано для змішаних флотів.
Виправлення 2: Bind-монтуйте /usr/share/zoneinfo (коли tzdata відсутній)
Якщо у контейнера немає файлів zoneinfo, деяким застосункам потрібно більше, ніж просто /etc/localtime. Вони можуть напряму завантажувати регіональні імена.
Монтування всього каталогу zoneinfo важче, але передбачувано і не вимагає додавання пакетів в образ.
cr0x@server:~$ docker run -d --name api-fixed \
-v /etc/localtime:/etc/localtime:ro \
-v /usr/share/zoneinfo:/usr/share/zoneinfo:ro \
-e TZ=America/New_York \
myorg/api:prod
Що означає валідація виводу: тепер ви зможете розв’язувати регіональні імена всередині контейнера.
cr0x@server:~$ docker exec api-fixed ls -l /usr/share/zoneinfo/America/New_York
-rw-r--r-- 1 root root 3552 Oct 15 2025 /usr/share/zoneinfo/America/New_York
Рішення: Використовуйте це, коли у вас є застосунки, що потребують іменованих зон і ви не можете пересобрати образ цього тижня. Заплануйте додати tzdata в образ пізніше.
Виправлення 3: Задайте TZ як змінну оточення (лише якщо стек її поважає)
TZ може працювати. Може й ігноруватися. Або частково враховуватися. Або враховуватися libc, але перекриватися рантаймом. Потрібно тестувати для вашого стека.
cr0x@server:~$ docker run -d --name worker-fixed \
-e TZ=Etc/UTC \
myorg/worker:prod
Валідація:
cr0x@server:~$ docker exec worker-fixed sh -lc 'echo $TZ; date'
Etc/UTC
Sat Jan 3 11:44:01 UTC 2026
Рішення: Якщо ваш флот гетерогенний (Debian, Alpine, scratch-подібні), віддавайте перевагу монтуванню /etc/localtime перед покладанням на TZ.
Виправлення 4: Монтируйте /etc/timezone для Debian-подібних очікувань (іноді потрібно)
Деякі стеки читають /etc/timezone (простий текстовий файл), навіть коли /etc/localtime існує. Це не універсально, але досить поширено.
cr0x@server:~$ printf "America/New_York\n" | sudo tee /srv/timezone-files/etc.timezone
America/New_York
cr0x@server:~$ docker run -d --name web-fixed2 \
-v /etc/localtime:/etc/localtime:ro \
-v /srv/timezone-files/etc.timezone:/etc/timezone:ro \
myorg/web:prod
Рішення: Якщо застосунки поводяться інакше, ніж date всередині контейнера, додавання /etc/timezone — розумна цілеспрямована заплатка.
Виправлення 5: Не змінюйте часові пояси; змініть формат логів
Якщо реальна проблема — «логи не співпадають між системами», часто вирішення — логувати UTC послідовно і додавати зсув, коли потрібно.
Зміна часових поясів контейнерів лише для зручності перегляду логів — як фарбувати машину, тому що радіо гучне.
Практичний крок: налаштуйте бібліотеку логування на вивід ISO-8601 з інтервалом зсуву. Це конфігурація застосунку, не пересборка образу. Для багатьох систем це змінна оточення або ConfigMap.
Виправлення 6: Sidecar/exec-латка (метод «так, але не треба»)
Інколи можна за допомогою docker exec замінити /etc/localtime в робочому контейнері. Це швидко. Але не декларативно і втрачається після перезапуску.
cr0x@server:~$ docker exec -u 0 web-1 sh -lc 'cp /usr/share/zoneinfo/America/New_York /etc/localtime && date'
Sat Jan 3 06:45:10 EST 2026
Рішення: Використовуйте це лише для короткого форензичного аналізу. Потім імплементуйте правильний маунт/змінну оточення. Якщо ви «гаряче лататимете» часові пояси на місці, забудете і це зламалося знову при наступному деплої.
Жарт №2: Якщо ви думаєте «ми стандартизуємо час пізніше», вітаю — ви винайшли DST, але для інженерних команд.
Docker Compose та Kubernetes: патерни
Docker Compose: оголошуйте маунти для timezone всерйоз
Compose — це місце, де дисципліна щодо часових поясів або зароджується, або загниває. Оголосіть маунти та оточення в YAML, щоб кожний перезапуск відтворював поведінку.
cr0x@server:~$ cat docker-compose.yml
services:
web:
image: myorg/web:prod
environment:
- TZ=America/New_York
volumes:
- /etc/localtime:/etc/localtime:ro
- /usr/share/zoneinfo:/usr/share/zoneinfo:ro
Рішення: Якщо ви монтуєте zoneinfo, можете безпечно встановити TZ як іменовану зону, навіть якщо tzdata відсутній в образі.
Якщо ви не монтуєте zoneinfo, встановлення TZ може мовчки впасти або поводитися по-різному залежно від libc/застосунку.
Kubernetes: можна монтувати файли timezone, але оберіть правильну абстракцію
У Kubernetes под використовує годинник вузла. Обробка часових поясів — питання файлової системи й оточення.
Два поширені варіанти:
- Монтувати hostPath для
/etc/localtime(і за потреби/usr/share/zoneinfo). - Використовувати UTC скрізь і не турбуватися про локальний час всередині pod-ів. Це варіант, який добре старіє.
hostPath-маунти операційно «ріжучі». Вони пов’язують поди з файловою системою вузла і політикою. Деякі кластери забороняють їх з поважних причин.
Якщо ви не можете використовувати hostPath, можна пакувати файли timezone в ConfigMap для однієї зони, але це незграбно й вимагає підтримки оновлень правил.
Моя упереджена за замовчуванням порада для Kubernetes: тримайте все в UTC. Конвертуйте в UI/репортингу. Нехай вузли теж будуть в UTC. Тримайте людський час поза data plane.
Типові помилки: симптом → корінь → виправлення
Розділ, що позбавить вас робити ту саму помилку з додатковою впевненістю.
1) Логи на хості правильні, в контейнері — неправильні
Симптоми: date на хості правильний; date в контейнері показує UTC або інший регіон.
Корінь: У контейнера власний /etc/localtime (часто симлінк на UTC), або відсутній tzdata і за замовчуванням обрано UTC.
Виправлення: Bind-монтуйте /etc/localtime лише для читання. Якщо застосункам потрібні іменовані зони — також монтуйте /usr/share/zoneinfo і задайте TZ.
2) Усе зсувається рівно на годину, тільки під час DST
Симптоми: Час збігається більшу частину року, але зсувається на годину навколо переходу DST.
Корінь: Застарілі правила tzdata в контейнері або рантаймі. Або змішані набори правил між вузлами.
Виправлення: Використовуйте маунт зонinfo хоста, щоб централізувати оновлення правил. Перезапустіть рантайми, що кешують правила (особливо JVM).
3) Мітки часу застосунку правильні, БД правильна, але звіти неправильні
Симптоми: Сировинні дані в порядку; звіти для людей показують «не той день» або «не ту годину».
Корінь: Шар презентації конвертує час двічі або використовує неоднозначні скорочення.
Виправлення: Стандартизувати зберігання в UTC, виконувати конвертацію лише один раз на межі, логувати зсуви, заборонити скорочення типу «CST/IST».
4) Встановив TZ, нічого не змінилося
Симптоми: echo $TZ показує ваше значення; date або застосунок все ще показує стару зону.
Корінь: Застосунок ігнорує TZ; рантайм фіксує часовий пояс; або відсутні файли zoneinfo, і TZ не може розв’язати ім’я.
Виправлення: Монтуйте /etc/localtime і zoneinfo; налаштуйте специфічно для рантайму (наприклад, JVM -Duser.timezone).
5) Контейнери різняться по вузлах у тому ж кластері
Симптоми: Той самий образ, ті самі змінні, але різний локальний час залежно від вузла.
Корінь: Ви монтуєте хостове /etc/localtime, але вузли мають різні часові пояси або різні версії tzdata.
Виправлення: Примусьте вузли використовувати UTC (бажано) або централізовано задайте один часовий пояс через конфігураційний менеджмент. Не дозволяйте «петам» налаштовувати час.
6) «Виправлення» часового поясу ламає TLS, кеші або валідацію токенів
Симптоми: Після зміни, пов’язаної з часом, токени аутентифікації відмовляють, кеші спрацьовують негайно, або TLS скаржиться на ще не дійсні сертифікати.
Корінь: Ви змінили фактичний годинник (час хоста), а не відображення часового поясу. Або годинник стрибнув через корекцію NTP.
Виправлення: Відновіть коректну дисципліну годинника хоста. Уникайте ручних змін часу. Правильно налаштуйте NTP/chrony і моніторте стрибки часу.
Чеклісти / покроковий план
Чекліст A: Тріаж інциденту в продакшні (15 хвилин)
- Підтвердіть синхронізацію часу хоста:
timedatectl status. Якщо не синхронізовано — виправте хост перед дотиком до контейнерів. - Порівняйте епохальні секунди хоста і контейнера:
date +%sна обох. Якщо вони відрізняються — серйозна проблема (контейнери мають відповідати часу хоста). - Перевірте вивід
dateі часовий пояс в контейнері: шукайте UTC проти локального та зсуви. - Інспектуйте
TZв оточенні і ціль/etc/localtime. Уникати змішаних сигналів. - Визначте бажаний стан: UTC скрізь, або конкретна локальна зона з причини.
- Застосуйте рантайм-виправлення декларативно (Compose/K8s маніфести), а не через
docker execхаки. - Перезапустіть лише те, що треба (JVM може потребувати перезапуску для підхоплення нових правил tzdata).
Чекліст B: Стандартизація флоту (нудний, але правильний план)
- Обрати канонічний часовий пояс для зберігання і логів: UTC. Записати. Зробити правилом платформи.
- Забезпечити хости в UTC. Це зменшить «лотерею» з hostPath часовими поясами.
- Для небагатьох робочих навантажень, що справді потребують локального часу, реалізувати це явно через маунти або рантайм-флаги і ізолювати їх.
- Включити оновлення tzdata в процес оновлення ОС на хостах; уникати розплодження версій tzdata в образах.
- Додати CI-перевірку, яка провалює збірки, якщо хтось видаляє zoneinfo або tzdata без плану заміни.
- Додати моніторинг/алерти на зсув синхронізації часу на вузлах (стан chrony/NTP) і на великі стрибки часу.
Три корпоративні міні-історії з фронту часових поясів
Міні-історія 1: Інцидент через хибне припущення
Сервіс, близький до фінансів, запускав нічну звірку і виводив CSV для подальшої обробки. Сервіс був контейнеризований, розгорнутий на невеликому swarm-і
Linux-хостів і «очевидно» використовував локальний час дата-центру, бо люди читали звіти в тім регіоні.
Команда припустила, що контейнери успадковують часовий пояс хоста. Це поширена віра, бо іноді так здається — особливо при тестуванні на ноуті, де базовий образ
випадково співпадає з локальною зоною, або застосунок логуватиме UTC і ви ніколи цього не помічали.
Один хост був перебудований під час рутинного обслуговування. Нова ОС за замовчуванням стояла в UTC. Контейнери перезапустилися, і звіт почав маркувати «сьогоднішні
транзакції» в UTC. Кілька годин кожного дня транзакції потрапляли в «завтра», що викликало тиху каскаду: дублювання імпортів, відсутні записи й багато людей,
що копирсалися в таблицях Excel.
Виправлення було смішно простим: вони змонтували /etc/localtime в контейнер і привели часові пояси хостів до єдиної конфігурації. Важливим було
культурне рішення: вони записали нотатку платформи, що контейнери не успадковують поведінку часового поясу так, як варто покладатися, і стандартизували зберігання
міток часу в UTC.
Міні-історія 2: Оптимізація, що дала зворотний ефект
Платформна команда зменшувала розміри образів. У них були графіки, цілі і таблиці. Хтось помітив, що tzdata і zoneinfo «велике й не потрібне» в більшості сервісів.
З’явився коміт, який видаляв /usr/share/zoneinfo під час збірки образів для кількох базових образів.
Спочатку нічого не зламалося. Це була пастка. Багато сервісів логували UTC. Деякі використали епохальні мітки. Очищення здавалося виграшем. Команда навіть святкувала
швидший pull в деяких середовищах.
Через тижні робочий процес служби підтримки почав планувати зворотні дзвінки з локальних часових поясів профілів користувачів. Сервіс використовував іменовані зони
(наприклад, America/Los_Angeles) для підрахунку наступного робочого дня. При відсутності zoneinfo він тихо падав в UTC в одному рантаймі і кидав виключення
в іншому. Результат — хаотичне планування й часткові відмови. У звіті про інцидент фігурувала фраза «регресія нефункціональної вимоги», що корпоративною мовою означає
«ми прострелили собі ногу, оптимізуючи шнурки».
Практичне тимчасове виправлення — рантайм-маунт хостового zoneinfo (без пересборки), а далі — ретельна зміна базового образу: зберігати tzdata в загальному базовому,
і лише зрізати його в спеціальних «лише UTC» образах з контрактними тестами, що це підтверджують.
Міні-історія 3: Нудна практика, що врятувала день
Внутрішня служба ідентифікації видавала короткоживучі токени. Вона працювала в кількох регіонах. Команда мала політику: усі вузли флоту в UTC, і контейнери монтують
/etc/localtime лише коли потрібно. Для більшості сервісів цього не робили; застосунок писали й тестували в UTC.
Під час інциденту з віртуалізацією деякі хости тимчасово мали нестабільність NTP. Годинник не сильно зсувався, але джиттер був достатній, щоб декілька сервісів
отримали помилки валідації токенів, коли суворо порівнювали «issued at» і «not before».
Команда ідентифікації вже впровадила дві нудні запобіжні міри: моніторинг здоров’я chrony на вузлах і логіку застосунку, що терпляче ставиться до невеликого зсуву годинника.
Їхні логи були в UTC зі зсувами. Коли інцидент почався, вони миттєво корелювали події між сервісами і довели, що проблема — дисципліна годинника, а не форматування часового поясу.
Вони швидко відновилися, зливши уражені вузли і відновивши синхронізацію часу. Ніяких правок часових поясів, ніяких домислів. Просто виміряна поведінка і політика, яку можна
примусити виконувати. Це не було гламурно, але врятувало від безпорядного «підправляння часового поясу», що маскувало справжню проблему.
Виправлення без пересборки образів (глибші поради, які ви справді використаєте)
Ви вже бачили базові виправлення. Тепер поговоримо, як обирати між ними, не створюючи довгострокового безладдя.
Фреймворк рішень: UTC-перший, локальний час — лише виняток
Якщо ви довго працюєте в продакшні, припините сприймати «локальний час» як стандарт. Люди живуть у локальному часі. Розподілені системи живуть в UTC.
Найбезпечніший підхід у довгостроковій перспективі:
- Зберігайте мітки часу в UTC в базах даних, чергах повідомлень і логах.
- Додавайте зсув при генерації таймстампів для людей.
- Конвертуйте на краях (UI, генерація звітів, API-відповіді за запитом).
Коли вам справді потрібен локальний час всередині контейнера? Зазвичай:
- Устарілі застосунки, що читають «локальний північ» від ОС без бібліотек, що знають часові пояси.
- Планувальники типу cron всередині контейнерів (яких варто уникати, але реальність інша).
- Сторонні бінарні файли, які не можна змінити і які припускають локальний час ОС.
Рантайм-зміни: декларативне краще за героїчне
Виправлення часового поясу має пережити перезапуски. Це означає, що він належить у:
- Compose YAML
- systemd override для запуску docker run
- Kubernetes маніфести/Helm values
Не в чиємусь shell-history.
Стратегія маунтів: один файл проти повного zoneinfo
Маунт /etc/localtime мінімальний і працює для перетворень «локальний час» на базі libc. Маунт /usr/share/zoneinfo дозволяє використовувати
іменовані зони. Якщо ви ставите TZ=America/New_York, переконайтеся, що контейнер може розв’язати цей файл.
Є ще тонке питання консистентності: монтування обох гарантує, що /etc/localtime і посиланий файл зон походять з однієї версії tzdata. Змішування версій може
створити дивні «правила DST не узгоджуються самі з собою» в деяких стеках.
Що робити, якщо потрібно «змінити часовий пояс» запущеного контейнера?
Якщо треба зробити це без повторного деплою (тісне вікно інциденту), можна:
- Додати volume mount у наступному перезапуску (найкраще).
- Або запатчити на місці через
docker exec(швидко, не декларативно).
Якщо ви патчите на місці, вважайте це тимчасовою турнікетною мірою. Реальне виправлення має бути в конфігурації деплойменту.
FAQ
1) Чи можуть контейнери мати інші часові пояси, ніж хост?
Так. Вони ділять хостовий годинник, але часовий пояс — це переважно файлова конфігурація й оточення. Два контейнери на тому самому хості можуть показувати різний локальний час.
2) Якщо я змонтую /etc/localtime, чи це виправить «зсув»?
Це виправить невідповідність часового поясу, але не зсув годинника. Якщо годинник хоста неправильний — маунти не допоможуть. Спершу перевірте синхронізацію хоста.
3) Чи достатньо встановити TZ?
Інколи. Це залежить від libc, рантайму і наявності файлів zoneinfo. Якщо ви задаєте іменовану зону, переконайтеся, що /usr/share/zoneinfo присутній або змонтований.
4) Чому я бачу UTC навіть після встановлення TZ?
Або застосунок ігнорує TZ, або він не може розв’язати ім’я зони через відсутність tzdata. Перевірте /usr/share/zoneinfo і налаштування рантайму застосунку.
5) Який найнадійніший дефолт для продакшну?
UTC скрізь для зберігання і логів. Конвертуйте на краях. Це зменшує плутанину між регіонами і усуває «який сервер в якій зоні» як режим відмови.
6) Чи потрібно перезапустити контейнер після зміни маунтів timezone?
Так, бо маунти визначаються при старті контейнера. Деякі рантайми також кешують правила часових поясів; JVM часто потребує перезапуску, щоб надійно підхопити зміни tzdata.
7) Як безпечно працювати з DST?
Тримайте tzdata актуальним (бажано через хост), використовуйте регіональні імена (America/New_York) і не плануйте критичні задачі в неоднозначний локальний час типу 02:30 у день зсуву DST.
8) Що робити, якщо мій Kubernetes кластер забороняє hostPath?
Тоді орієнтуйтеся на UTC. Якщо вам справді потрібна локальна зона, можна доставити необхідний zoneinfo-файл через ConfigMap, але тепер ви відповідаєте за оновлення правил.
9) Чи створює монтування /usr/share/zoneinfo проблеми безпеки?
Монт для читання — низький ризик, але hostPath усе ще створює зв’язок із хостом. У жорстко захищених кластерах це може бути заборонено. На керованих Docker-хостах це поширено прийнятно.
10) Як довести, що проблема — на рівні застосунку, а не ОС?
Порівняйте вивід date з часовими мітками застосунку і перевірте налаштування рантайму (JVM user.timezone, БД show timezone).
Якщо ОС-час в порядку, а застосунок ні — то це конфігурація застосунку/рантайму.
Висновок: наступні кроки, які вас не переслідуватимуть
Зсув часового поясу в контейнерах рідко містить містичний початок. Зазвичай це одне з трьох: відсутній tzdata, конфліктні сигнали часових поясів (TZ vs
/etc/localtime), або застосунок/рантайм, що робить щось своє.
Практичні наступні кроки:
- Вирішіть свій стандарт: UTC для зберігання/логів. Локальний час лише за бізнес-потребою.
- Аудит контейнерів: перевірте
/etc/localtime,TZі наявність zoneinfo за допомогою завдань вище. - Реалізуйте декларативні виправлення: Compose/K8s конфіги з маунтами
/etc/localtime(та за потреби/usr/share/zoneinfo). - Запобігайте регресіям: не вирізайте tzdata «для оптимізації», якщо немає тестів, що доводять можливість цього.
- Моніторте годинник хоста: синхронізація часу — це продакшн-залежність. Ставте її в категорію «нудне, але критичне», як і місце на диску.