Часы сервера синхронизированы, а приложение показывает время на два часа меньше: часовой пояс приложения и часовой пояс системы это разные настройки

Что было. На Ubuntu Server 20.04 включена синхронизация времени, timedatectl показывает System clock synchronized: yes, но PHP-скрипт через date() выводит время в UTC, на два часа меньше местного. Автор сдался и добавил в код strtotime('+2 hours'). Участник предложил проверить date_default_timezone_get().

Почему костыль плох. Смещение относительно UTC меняется дважды в год при переходе на летнее время и различается между регионами: «+2 часа» через полгода станет «+1». А в базе данных при этом хранятся значения, которые уже нельзя будет однозначно интерпретировать.

Как это устроено. Синхронизация (NTP) выравнивает момент времени, всегда в UTC. Часовой пояс это отдельная настройка, которая говорит, как этот момент показывать. И у системы, и у PHP, и у базы данных, и у контейнера часовые пояса настраиваются независимо:

  • система: timedatectl set-timezone Europe/Berlin (файл /etc/localtime);
  • PHP: date.timezone = Europe/Berlin в php.ini для нужного SAPI (у CLI и FPM разные php.ini) либо date_default_timezone_set() в начале приложения; без настройки PHP использует UTC;
  • MySQL/MariaDB: default_time_zone в конфиге, иначе берётся системный; SELECT @@global.time_zone, @@session.time_zone;
  • контейнер: переменная TZ=Europe/Berlin и наличие tzdata в образе; системный пояс хоста в контейнер не наследуется.

Что мы рекомендуем для серверов. Хранить и передавать время в UTC (в базе TIMESTAMP/timestamptz, в API ISO 8601 с суффиксом Z), а в местный пояс переводить только при показе пользователю, зная его пояс. Тогда смена пояса сервера или переезд ничего не ломают. Системный пояс сервера при этом лучше оставить UTC: журналы, cron и время из разных источников совпадают, а сотрудники в разных поясах не спорят о «серверном времени».

Диагностика.

timedatectl                              # системный пояс и синхронизация
php -i | grep -i 'date.timezone'         # для CLI
php-fpm8.3 -i | grep -i 'date.timezone'  # для веб-сервера
mysql -e 'SELECT @@global.time_zone, NOW(), UTC_TIMESTAMP();'

Если date() в браузере и в консоли отличаются, дело в разных php.ini.

Cron и systemd-таймеры тоже живут в системном поясе; задача «в 03:00» после смены пояса сервера сдвинется. В OnCalendar можно указать пояс явно: OnCalendar=*-*-* 03:00:00 Europe/Berlin.

Настройка описана в документации PHP.